Lombok wzbudza wiele emocji. Tych pozytywnych jak i negatywnych. Ma on wiele zalet, ale nie jest też wolny od wad. Na pewno nie można powiedzieć o Lomboku, że przechodzi bez echa wśród programistów Java. W wielu sytuacjach jego funkcjonalności dają efekt pozytywny jak i negatywny. Weźmy na przykład na tapet @RequiredArgsConstructor
. Dzięki niemu nie musimy tworzyć na nowo konstruktora, gdy dochodzi nam kolejne finalne pole w klasie. Natomiast na drugim biegunie jest to, że takie podejście zwalnia programistów z myślenia. Z tego powodu zdarza się sytuacja, że kończymy w projekcie z klasą z nastoma zależnościami…
Jednak dzisiaj nie o tym. W tym wpisie chciałem się skupić na innym specyficznym problem, który możemy napotkać korzystając z Lomboka wszędzie, bez zastanowienia.
Przypadek z życia wzięty
Załóżmy, że mamy klasę, która agreguje nam jedną wartość np. markę samochodu. Pierwsze co przychodzi na myśl to skorzystanie ze wzorca Value Object. No dobra, zróbmy tak.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package io.cezarysanecki.vo;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
@Builder
@EqualsAndHashCode
@RequiredArgsConstructor
public final class VehicleMake {
private final String value;
}
No i super. Teraz możemy tworzyć obiekty tej klasy na dwa sposoby.
1
2
3
4
VehicleMake vehicleMakeNew = new VehicleMake("skoda");
VehicleMake vehicleMakeBuilder = VehicleMake.builder()
.value("skoda")
.build();
Oczywiście umyślnie pominąłem konstrukcję @RequiredArgsConstructor(staticName = "of")
. To rozwiązanie również powodowałaby zamieszanie, do którego zmierzam.
Problem na produkcji
Nasze rozwiązanie się przyjęło. Testy przeszły, więc wrzucamy je na produkcję. Zadowoleni kończymy ciężki dzień pracy. Jednak po jakiś dwóch dniach wpada nam błąd związany z tą funkcjonalnością. Po krótkiej analizie weryfikujemy, że coś nie tak dzieje się z poniższą linijką kodu.
1
2
//obie zmienne są typy VehicleMake
firstVehicleMake.equals(secondVehicleMake);
Wynikiem tej operacji powinna być wartość true
, ponieważ obydwie wartości odnoszą się do marki Skoda. Po zajrzeniu głębiej dowiadujemy się, że o ile u nas w aplikacji wartość to "skoda"
to z zewnętrznego systemu otrzymaliśmy "SKODA"
. Stąd ten zgrzyt.
Pierwsze co może przyjść na myśl to szybkie rozwiązanie sytuacji przez wykorzystanie porównania wartości przez equalsIgnoreCase
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package io.cezarysanecki.vo;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@Builder
@EqualsAndHashCode
@RequiredArgsConstructor
public final class VehicleMake {
private final String value;
}
//...
firstVehicleMake.getValue().equalsIgnoreCase(secondVehicleMake.getValue());
Dobrze wiemy, że to jednak nie tędy droga… Moglibyśmy zrezygnować z @EqualsAndHashCode
i nadpisać equals
, aby nie zwracał uwagi na wielkość liter. Byłoby to faktycznie prawidłowe. Jednak z jakiegoś powodu, w celach audytowych, chcemy mieć unifikację wartości w bazie danych. Przyjmujemy z tego powodu, że wszystko będzie zapisywane wielkimi literami. No to lecimy z poprawką do kodu.
Kończymy z potworkiem
Rozwiązanie mogłoby wyglądać w następujący sposób. Wykorzystując możliwość nadpisywania metod z buildera oraz tworząc metodę wytwórczą kończymy z takim oto potworkiem (bez @RequiredArgsConstructor
, bo musieliśmy sami stworzyć pasujący konstruktor). Oczywiście wszystko działa jak należy. Jednak czy jesteśmy z siebie finalnie zadowoleni?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package io.cezarysanecki.vo;
import lombok.Builder;
import lombok.EqualsAndHashCode;
@Builder
@EqualsAndHashCode
public final class VehicleMake {
private final String value;
public VehicleMake(String value) {
validateValue(value);
this.value = value.toUpperCase();
}
public static class VehicleMakeBuilder {
public VehicleMakeBuilder value(String value) {
validateValue(value);
this.value = value.toUpperCase();
return this;
}
}
private static void validateValue(String value) {
if (value == null || value.isBlank()) {
throw new IllegalStateException("value must be present");
}
}
}
Przykład z tego artykułu to prawdziwa historia. Może moglibyśmy tego uniknąć, gdyby tak nie skupiać się w 100% na Lomboku, a go sobie po prostu w tym przypadku… odpuścić? Można by skończyć z czymś o wiele przyjemniejszym dla oka.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package io.cezarysanecki.vo;
import java.util.Objects;
public final class VehicleMake {
private final String value;
private VehicleMake(String value) {
if (value == null || value.isBlank()) {
throw new IllegalStateException("value must be present");
}
this.value = value.toUpperCase();
}
public static VehicleMake of(String value) {
return new VehicleMake(value);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
VehicleMake that = (VehicleMake) o;
return Objects.equals(value, that.value);
}
@Override
public int hashCode() {
return Objects.hash(value);
}
}
Ewentualnie skorzystać z @EqualsAndHashCode
jeśli ktoś nie chce mieć tego boilerplate kodu.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package io.cezarysanecki.vo;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode
public final class VehicleMake {
private final String value;
private VehicleMake(String value) {
if (value == null || value.isBlank()) {
throw new IllegalStateException("value must be present");
}
this.value = value.toUpperCase();
}
public static VehicleMake of(String value) {
return new VehicleMake(value);
}
}
Podsumowanie
Czasami narzędzia potrafią nami zawładnąć i doprowadzić nas do tworzenia nieczytelnego kodu. Nie można im się tak po prostu dać. Trzeba wpadać samemu na najlepsze i najczytelniejsze rozwiązania w oparciu o nie.
Istotna jest również komunikacja w zespole, aby zwracać uwagę na takie rzeczy. Dodatkowo można wypracować wspólne konwencje, że np. Value Object tworzymy tylko przez metodę wytwórczą of. Nie ma możliwości korzystania jawnie z konstruktorów poza klasą czy też z builderów. Przyjmujemy to postanowienie za standard i za nim podążamy. Na review do checklisty dodajemy sobie kolejny punkt, aby na takie rzeczy zwracać uwagę aż nie wejdą one w krew.
Jaki jest Twój stosunek do Lomboka? Czy korzystasz z niego na projekcie? Jeśli tak, to czy też miałeś/aś ciekawe historie z nim związane?