Typy proste jako identyfikatory obiektów to nie jest najlepszy pomysł. To działa, jednak można to zrobić lepiej niewielkim nakładem pracy.
Prawdę mówiąc, jedyne miejsce, w którym ma to rację bytu to encje bazodanowe w ORM którego używasz.
Jeżeli nie typy proste, to co w takim razie?
Stwórz własne klasy na identyfikatory. Każdy rodzaj identyfikatora będzie miał własną klasę.
Na przykład tak (wykorzystując Lombok
):
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
@EqualsAndHashCode
@ToString
public class EventId {
private final String value;
public static EventId of(String value) {
return new EventId(value);
}
public String asString() {
return value;
}
}
Mamy klasę EventId
, a w niej:
- prywatny konstruktor ze źródłowym natywnym identyfikatorem – w tym przypadku
String
, - wewnętrzne, nie mutowalne pole na przechowanie natywnego identyfikatora,
- publiczną, statyczną metodę (
of(String)
) tworzącą nasz identyfikator na podstawie jego natywnej wartości, - publiczną metodę pozwalającą przekształcić identyikator do natywnej wartości (
asString()
) - do tego standardowe metody
equals
ihashcode
na potrzeby porównywania, - opcjonalnie,
toString
aby móc ładnie reprezentować obiekt w logach
Jak widzisz, nie jest to dużo pracy, tylko po co to wszytko? Już tłumaczę.
Czytelność
Spójrz na dwa poniższe przykłady, który jest dla Ciebie bardziej czytelny?
public void subscribe(String userId, String eventId) {
eventStorage.subscribe(eventId, userId);
}
public void subscribe(UserId userId, EventId eventId) {
eventStorage.subscribe(eventId, userId);
}
Dla mnie zdecydowanie wygrywa druga opcja. Jeszcze więcej zyskuje od strony użycia tych metod, IDE jest w stanie dużo łatwiej mi podpowiedzieć, którą wartość powinienem przekazać.
Dodatkowo jestem zabezpieczony przed pomyleniem kolejności argumentów. W pierwszym przypadku, jak się pomylę, to dopiero testy mają szanse mi wskazać błąd.
Walidacja
W metodach tworzących instancję naszego identyfikatora możemy zawrzeć logikę sprawdzającą poprawność, która zabezpieczy nas przed złym formatem identyfikatora. Czyli możemy zastosować podejście Fail-fast, czyli wykrywanie błędów i niepoprawności w działaniu jak najszybciej.
Własna logika generowania
Jeżeli mamy taką potrzebę, to możemy zawrzeć w klasie dowolnie skomplikowaną logikę generowania wartości identyfikatora. Możemy oprzeć go o inne identyfikatory, tak aby wygenerować jakąś formę identyfikatora-childa w stosunku do innego i w ten sposób zbudować hierarchię już na poziomie identyfikatorów.
Wygodniejszy refactor
Kolejna sprawa to dużo wygodniejszy refactor. Opieramy się ta klasach, które są unikalne i zawsze znaczą to samo. Jesteśmy w stanie bardzo łatwo namierzyć wszystkie użycia naszego identyfikatora i dokonać zmian tak jak z każdą inną klasą. Nie musimy się bawić w Find and Replace na podstawie nazw zmiennych i żmudnego przeglądania każdego kawałka znalezionego kodu.
A czy mogę wystawić takie typy na REST endpoincie?
Oczywiście, jednak trzeba wykonać tutaj jeszcze kilka rzeczy.
Pierwsze co, to po prostu używamy tych typów na naszym Controllerze
.
@PostMapping("/{id}/subscribe")
public void subscribe(@PathVariable("id") EventId eventId, @RequestHeader(HEADER_USER_ID) UserId userId) {
eventService.subscribe(userId, eventId);
}
Następne co musimy zrobić, to nauczyć Springa je tworzyć na podstawie wartości tekstowych, które trafiają na endpoint w postaci JSONa
w body, @PathVariable
czy też @RequestParam
w URLu.
Do tego przede wszystkim potrzebujemy Converter
, który będzie konwertował String
na EventId
:
public class EventIdFromStringConverter implements Converter<String, EventId> {
@Override
public EventId convert(String value) {
return EventId.of(value);
}
}
Tak utworzony konwerter (i wszystkie inne konwertery, jakie utworzymy) musimy jeszcze zarejestrować w konfiguracji WebMvc Springa – czyli konfiguracji warstwy REST.
Aby to zrobić, potrzebujemy klasy konfiguracji, rozszerzającej WebMvcConfigurer
i w niej dodajemy własną implementację metody addFormaters
, w której dodajemy nasz Convert
do registry
.
Na przykład w taki sposób:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new EventIdFromStringConverter());
registry.addConverter(new EventIdToStringConverter());
registry.addConverter(new UserIdFromStringConverter());
}
}
No i to wszystko, nasze identyfikatory zostaną powołane do życia automatycznie i przekazane dalej. Jeśli tworzenie identyfikatorów będzie zawierało logikę walidacyjną, to od razu na etapie Convertera
możemy to obsłużyć wyjątkiem i zwrócić z naszego endpointa błąd 400 Bad request
.
Dodatkowe materiały
- GitHub – repozytorium z aplikacją wykorzystującą własne obiekty jako identyfikatory