Popularne frameworki #1: Lombok

Język Java często jest krytykowany za konieczność pisania kodu, który mógłby być automatycznie wygenerowany przez JDK. Ciągłe pisanie getterów i setterów do każdej klasy POJO, tworzenie toStringów, czy projektowanie builderów, choć początkującemu programiście daje wiele satysfakcji, to po pewnym czasie staje się bardzo nudne. W takiej sytuacji na pomoc przychodzi biblioteka Lombok, której celem jest uprościć kod Java.

Instalacja

Aby wykorzystać bibliotekę w kodzie korzystającym z Mavena, należy dodać zależność poprzez wpis:

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.24</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

Jeśli korzystasz z innego menadżera projektu (np. Gradle’a) to zajrzyj na oficjalną stronę projektu Lombok. Dodatkowo, jeśli korzystasz z IDE (a zakładam, że tak ;)), potrzebna Ci będzie poprawnie zainstalowana wtyczka do biblioteki. W przypadku IntelliJ nie powinno być problemu, bowiem twórcy oprogramowania zaprojektowali całkiem dobrze działający plugin. Niestety trochę gorzej jest, jeśli korzystasz z drugiego popularnego IDE Eclipse. Tutaj należy ją samodzielnie pobrać i skonfigurować*.

Generowanie składowych klasy

Lombok pozwala na wygenerowanie wiele powtarzalnych składowych klasy, stosując odpowiednie adnotacje. Największą zaletą tego rozwiązania jest możliwość zastosowania różnej ich kombinacji. Dlatego, jeśli chcesz napisać klasę z samymi getterami i jednym konstruktorem, to bez problemu możesz to zrobić. Dodatkowo Twój kod będzie dużo czytelniejszy i krótszy.

Spójrz na poniższy przykład:

public class RealEstate {
    private static final java.util.logging.Logger LOGGER = java.util.logging.Logger.getLogger(RealEstate.class.getName());
    private Long id;
    private String code;
    private String address;

    public RealEstate() {
    }

    public RealEstate(Long id, String code, String address) {
        this.id = id;
        this.code = code;
        this.address = address;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public void writeFile() {
        Path filePath = Paths.get("real_estate.txt");
        try (BufferedWriter bufferedWriter = Files.newBufferedWriter(filePath, UTF_8)) {
            bufferedWriter.write(toString() + "\n");
        } catch (IOException e) {
            LOGGER.warning("Can't write result to file with content:" + e.getMessage());
        }
    }

    public void showMessage() {
        Logger.getGlobal().info("Real estate class");
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        RealEstate that = (RealEstate) o;
        return Objects.equals(id, that.id) && Objects.equals(code, that.code);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, code);
    }

    @Override
    public String toString() {
        return "RealEstate{" +
                "id=" + id +
                ", address='" + address + '\'' +
                '}';
    }
}

Powyższa klasa jest bardzo długa. Zawiera pola, konstruktory, metody equals, hashcode, toString, użyty logger oraz metodę obsługującą wyjątek.

Akcesory

Jedną z najczęściej powtarzających się składowych klasy są gettery i settery dla pól. Często te proste metody zajmują połowę lub więcej miejsca, a z reguły nie posiadają żadnych wewnętrznych reguł poza przypisaniem wartości lub jej zwracaniem. Adnotacje Getter i Setter nakładane nad klasą wygenerują wszelkie gettery i settery do pól (oczywiście można zastosować także tylko jedną z nich)

@Getter i @Setter

Po zastosowaniu adnotacji:

@Getter
@Setter
public class RealEstateLombok {
    private Long id;
    private String code;
    private String address;
}

@Accessors

Ta adnotacja jest rozwinięciem @Getter i @Setter. Posiada kilka ciekawych flag. Omówię tu dwie najczęściej używane. Pierwsza o nazwie fluent umożliwia, używanie akcesorów bez prefiksu get i set. Kolejna chain, pozwala stosować akcesory w jednej linii, podobnie jak w wzorcu projektowym „budowniczy”.

Klasa RealEstate zaktualizowana o adnotację Accessors:

@Getter
@Setter
@Accessors(fluent = true, chain = true)
public class RealEstateLombok {
    private Long id;
    private String code;
    private String address;
}

Przy przykład użycia flagi fluent ustawionej na true:

realEstateLombok.address("ul. Nowowiejska 54/3 Katowice");

Flaga chain ustawiona na true:

realEstateLombok.address("ul. Nowowiejska 54/3 Katowice").code("123");

Konstruktory

Kolejnym często schematycznym blokiem kodu w klasie są konstruktory. Bardzo rzadko zachodzi tu coś więcej, niż zwykłe przypisanie danych do pól. Lombok pozwala generować zarówno konstruktor zawierający wszystkie pola, jak i konstruktor bezargumentowy. Jeśli dziwisz się, do czego może służyć ten drugi, to przypominam Ci, że w przypadku napisania jakiegokolwiek konstruktor Java nie będzie już niejawnie tworzyć konstruktora bezargumentowego. Niestety czasami posiadanie takiego konstruktora jest wymagane przez niektóre frameworki.**

Zastosowanie: @NoArgsConstructor, @AllArgsConstructor

@Getter
@Setter
@Accessors(fluent = true, chain = true)
@AllArgsConstructor
@NoArgsConstructor
public class RealEstateLombok {
    private Long id;
    private String code;
    private String address;
}

Teraz możesz stworzyć zarówno instancję klasy, wypełniając wszystkie pola:

RealEstateLombok realEstateLombok = new RealEstateLombok(1L, "12345", "ul. Nowowiejska 54/3 Katowice");

jak i „pusty” obiekt:

RealEstateLombok emptyRealEstate = new RealEstateLombok();

Equals, hashCode oraz toString

Jak wiesz z poprzednich lekcji, aby poprawnie korzystać ze struktur opierających się o algorytmy haszujące, potrzebne są właściwie zaimplementowane metody hashCode i equals. Często ich wewnętrzna struktura jest podobna do siebie, więc dlaczego ich również nie generować? Za pomocą frameworku Lombok można to wykonać używając @EqualsAndHashCode.

Używając tej adnotacji należy uważać na pola, będące typami referencyjnymi. Powodem jest po pierwsze częste niepotrzebne spowolnienie działanie aplikacji, po drugie w przypadku cyklu (referencja do tej samej referencji) framework może zwariować. Dlatego polecam wypełnić adnotacje stosujące opcje exclude lub of. Do pierwszej z nich, wpisuje się pola, które mają być pominięte podczas generacji. Analogicznie flaga of, będzie korzystać tylko z tych pól, które zostaną w niej zawarte.

Podobnie jak @EqualsAndHashCode można używać @ToString.

Klasa po dodaniu powyższych dwóch adnotacji:

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@ToString(of={"id", "address"})
@EqualsAndHashCode(exclude={"address"})
public class RealEstateLombok {
    private Long id;
    private String code;
    private String address;
}

Efekt skorzystania z toString z wykorzystaniem flagi of:

RealEstateLombok(id=1, address=ul. Nowowiejska 54/3 Katowice)

Testowanie kolizji za pomocą kolekcji HashSet:

Set<RealEstateLombok> realEstates = new HashSet<>();
realEstates.add(new RealEstateLombok(1L, "12345", "ul. Nowowiejska 17 Katowice"));
realEstates.add(new RealEstateLombok(1L, "12345", "ul. Nowowiejska 17 Katowice"));
System.out.println(realEstates);

Prawidłowy wynik (brak kolizji) na konsoli

[RealEstateLombok(id=1, address=ul. Nowowiejska 17 Katowice)]

Generacja gotowych klas

Klasa typu builder

@Builder tworzy z Twojego POJO klasę typu budowniczy. Pozwala ona na szybkie tworzenie rozbudowanych obiektów i jednoczesne unikanie pisania wielu konstruktorów lub setterów. Niestety sama implementacja wzorca budowniczy zajmuje trochę czasu. Na szczęście w projekcie Lombok wystarczy jedna adnotacja, która robi robotę.

@Builder
public class CarBuilder {
    private String brand;
    private Long age;
    private String colour;
}

Przykład użycia:

CarBuilder.builder()
        .brand("Opel")
        .age(5)
        .colour("red")
        .build();

Klasa typu data

@Data to kombinacja wielu znanych już adnotacji: @Getter, @Setter, @ToString, @EqualsAndHashCode oraz @RequiredArgsConstructor.

@Data
public class CarDataObject {
    private String brand;
    private Long age;
    private String colour;
}

Powyższe adnotacje były już omawiane, więc nie będę wrzucał dodatkowych przypadków użycia.

Klasa typu immutable

Analogicznie do @Data, @Value to adnotacja tworząca obiekt immutable za pomocą kombinacji adnotacji, takich jak: @ToString @EqualsAndHashCode @AllArgsConstructor @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) @Getter. Dodatkowo klasa będzie klasą finalną. Ciekawą adnotacją jest też FieldDefaults, która pozwala zaoszczędzić czasu przy pisaniu modyfikatorów dostępu (flaga level), a także podczas ustawianiu pól jako finalne (flaga makeFinal).

@Value
public class CarValueObject {
    private String brand;
    private Long age;
    private String colour;
}

Użycie:

CarValueObject carVO = new CarValueObject("Opel", 5, "red");

Jako klasa niemodyfikowalna CarValueObject musi inicjować wszystkie pola w konstruktorze (są typu final).

Inne ciekawe adnotacje

Walka z nullpointerami

Częstym problemem w programowaniu obiektowym jest brak kontroli nad występującymi wyjątkami typu nullpointer. Pewnym rozwiązaniem tego problemu może być stosowanie adnotacji NonNull (podobne adnotacje występują nie tylko w bibliotece Lombok). Bardzo dobrze ta adnotacja sprawdza się razem z adnotacją RequiredArgsConstructor, która tworzy konstruktor na podstawie pól, które nie mogą być puste (nullem).

Wykorzystanie @NonNull oraz @RequiredArgsConstructor

@RequiredArgsConstructor
public class NonNullCar {
    @NonNull private String brand;
    private Integer age;
    @NonNull private String colour;
}

Wyjątki sprawdzane (checked)

Biblioteka Lombok zawiera też „featury”, które są uważane za eksperymentalne (używaj na własne ryzyko). Jedną z takich adnotacji jest @SneakyThrows, która pozwala obejść regułę wymagalność obsługiwania wyjątków typu „checked”.

Przykładowa obsługa wyjątku sprawdzalnego:

    public void writeFile() {
        Path filePath = Paths.get("real_estate.txt");
        try (BufferedWriter bufferedWriter = Files.newBufferedWriter(filePath, UTF_8)) {
            bufferedWriter.write(toString() + "\n");
        } catch (IOException e) {
            LOGGER.warning("Can't write result to file with content:" + e.getMessage());
        }
    }

Po zastosowaniu adnotacji SneakyThrows:

    @SneakyThrows
    public void writeFile() {
        Path filePath = Paths.get("real_estate.txt");
        try (BufferedWriter bufferedWriter = Files.newBufferedWriter(filePath, UTF_8)) {
            bufferedWriter.write(toString() + "\n");
        }
    }

Logger

Kolejną bardzo przydatnymi adnotacjami są wszelkie adnotacje dotyczące logowania. Są nimi m. in. @Log, @Log4j, @Slf4j. Upraszczają one odrobinę kod. Tutaj przykład działania adnotacji Log, która jest tożsama z użyciem standardowego loggera Javy.

private static final java.util.logging.Logger LOGGER = java.util.logging.Logger.getLogger(RealEstate.class.getName());

    public void showMessage() {
        Logger.getGlobal().info("Real estate class");
    }

Z wykorzystaniem @Log:

    public void showMessage() {
        log.info("Real estate class");
    }

Klasa pomocnicza

Ostatnią ciekawą klasą jest możliwość tworzenia tzw. klasę pomocniczą (ang. helper class). Klasa tego typu jest z reguły klasą finalną, co zabezpiecza przed dziedziczeniem, oraz każda metoda publiczna powinna być statyczną. Taka klasa powinna też posiadać jedynie stałe, jak i prywatny konstruktor. Dzięki takim założeniom zastosowane jest tu bardziej funkcjonalne podejście do programowania. Możesz utworzyć taką klasę automatycznie poprzez użycie @UtilityClass

Wersja podstawowa:

public final class FibonacciCalculator {
    private static final int FIRST_POSITION = 0;
    private static final int SECOND_POSITION = 1;

    private FibonacciCalculator() {
    }

    public static int countValue(int position) {
        int previouspreviousNumber, previousNumber = FIRST_POSITION, currentNumber = SECOND_POSITION;

        for (int i = 1; i < position ; i++) {
            previouspreviousNumber = previousNumber;
            previousNumber = currentNumber;
            currentNumber = previouspreviousNumber + previousNumber;
        }
        return currentNumber;
    }
}

Z wykorzystaniem biblioteki:

@UtilityClass
public class FibonacciCalculatorLombok {
    private final int FIRST_POSITION = 0;
    private final int SECOND_POSITION = 1;

    public int countValue(int position) {
        int previouspreviousNumber, previousNumber = FIRST_POSITION, currentNumber = SECOND_POSITION;

        for (int i = 1; i < position ; i++) {
            previouspreviousNumber = previousNumber;
            previousNumber = currentNumber;
            currentNumber = previouspreviousNumber + previousNumber;
        }
        return currentNumber;
    }
}

To wszystkie omawiane przeze mnie adnotacje, dostarczane przez projekt Lombok. Nie są to jednak jedyne przykłady, bo sam framework jest dość rozbudowany. W kwestii rozszerzenia sobie wiedzy o nim tradycyjnie polecam przeczytać oficjalną dokumentację twórców.

*Instrukcja konfiguracji Lomboka na przykładzie Eclipse: https://projectlombok.org/setup/eclipse

**Przykładem jest tworzenie encji w Hibernate

Kod do lekcji: https://github.com/developeronthego/java-frameworks/tree/main/src/main/java/frameworks/lombok

Stay in the Loop

Get the daily email from CryptoNews that makes reading the news actually enjoyable. Join our mailing list to stay in the loop to stay informed, for free.

Ostatnio dodane

- Advertisement - spot_img

Powiązane wpisy