author-avatar
Dominik Martyniak
Generyczność 6 minut

Elastyczna koncepcja generycznych eventów

Jak skutecznie zarządzać różnorodnymi eventami, które posiadają swoje własne dane? Zaproponuje generyczny sposób na tworzenie obiektów-wrapperów, które doskonale sprawdzą się jako kontenery dla tych wydarzeń. Dzięki temu rozwiązaniu można by elastycznie przechowywać i przekazywać eventy wraz z ich zróżnicowanymi danymi. Brzmi ciekawie, prawda? Właśnie dlatego chciałbym podzielić się z wam tym pomysłem.

 

Generyczny wrapper. 

 

Stwórzmy sobie nasz głowny obiekt:

//Getters, Constructor
public class Event<T extends EventData> {
    private Instant timestamp;
    private T data;

    public Event(T data) {
        Verify.verifyNotNull(data, "data is null");
        this.timestampt = Instant.now();
        this.data = data;
    }
}

 

Warto zaznaczyć, że w przypadku deserializacji obiektu w Jacksonie może pojawić się pewien problem z Instant.class . Aby to naprawić, wystarczy dodać do projektu: jackson-datatype-jsr310


W koncepcji naszej uniwersalnej klasy Event, stawiamy na prostotę i elastyczność. Ta klasa będzie przyjmować generyczny typ, ale z jednym istotnym zastrzeżeniem - ten typ musi rozszerzać klasę EventData. Dzięki temu ograniczeniu, mamy pewność, że nasza klasa Event będzie operować tylko na strukturach danych, które posiadają określone cechy.  Pomyślcie o naszej klasie Event jak o kontenerze.

 

Abstrakcja Kontentu


Skupmy sie teraz na stworzenie naszego mięska, zdefiniujemy  EventData:

public abstract class EventData {
}

 

W naszym uproszczonym scenariuszu, abstrakcyjna klasa staje się strażnikiem interfejsu. Choć może wydawać się bezskuteczna, to właśnie ona kieruje ruchem, kontrolując dostęp do naszych eventów. Ta skromna klasa na przyjęciu zdarzeń jest jak bilet kontrolny – bez niej, żadna klasa nieposiadająca specjalnego znaku nie może wstąpić na teren naszego eventowego pola.

 

Implementacja Eventu



Załóżmy że bedziemy wysyłać event informujący o rejestracji nowego użytkownika

//Getters, Constructor
public class NewPlayerEvent extends EventData {

    Long userId;
    String firstName;
    String lastName;

}

 

Aktualnie, poprzez wprowadzenie abstrakcji EventData, rozszerzamy strukturę naszego wydarzenia o możliwość tworzenia standaryzowanych i łatwo propagowalnych zdarzeń w całym systemie, wykorzystując na przykład platformę Apache Kafka. 

Serializacja i deserializacja danych 

 

Stwrzówmy pomocnika który pozowli nam na serializacje na Jsona lub deserializacje generycznego obiektu Event z polami np klasy NewPlayerEvent

public class EventSerializer {
    private final ObjectMapper mapper = new ObjectMapper();


    public <T extends EventData> Event<T> readFromJson(String json, Class<T> clazz) {
        Objects.requireNonNull(json, "json is null");
        try {
            return mapper.readValue(json, TypeFactory.defaultInstance().constructParametricType(Event.class, clazz));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public String createJson (EventData data) {
        try {
            return mapper.writeValueAsString(new Event<>(data));
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }
}

 

Pokolei co tu sie dzieje:

Warto zainicjować ObjectMappera tylko raz, ponieważ umożliwia on tworzenie JSON-ów oraz operacje serializacji i deserializacji danych. Inicjacja ta jest procesem kosztownym, dlatego zaleca się jej dokonanie na początku i wykorzystanie tej samej instancji w trakcie działania aplikacji.

Kolejnym istotnym elementem jest generyczna metoda o nazwie readFromJson, która przyjmuje jako swój pierwszy argument naszą wiadomość. Drugi argument to klasa, na którą chcemy zmapować pole "data" w naszym evencie. Warto zaznaczyć, że generyczny typ T musi być rozszerzalny przez klasę EventData, co skutecznie uniemożliwia przekazywanie innych klas. To zabezpieczenie jest kluczowe dla poprawnego działania mechanizmu.

Kolejna metoda createJson to prosta funkcja służąca do tworzenia zwykłych obiektów JSON. Nie ma potrzeby tu nadmiernego rozwodzenia się.



Szybkie sprawdzenie:

 

@Test
    public void should_deserialize_events() {
        //given
        var userEvent= new NewPlayerEvent(1L,"Dominik","Martyniak");
        EventSerializer eventHelpers = new EventSerializer();

        //when
        var jsonValue = eventHelpers.createJson(userEvent);
        Event<NewPlayerEvent> response = eventHelpers.readFromJson(jsonValue, NewPlayerEvent.class);

        //then
        Assert.assertEquals(expect.getData().getLastname(), userEvent.getLastName());
        Assert.assertEquals(expect.getData().getFirstName(), userEvent.getFirstName());
        Assert.assertEquals(expect.getData().getId(), userEvent.getId());
 
    }

 

Podsumowanie


Powyższy mechanizm umożliwia nam sprawnie tworzyć nowe eventy, na przykład w przypadku dodawania nowego postu, poprzez stworzenie nowej klasy o nazwie NewPostEvent. Ta klasa będzie rozszerzać ogólną klasę EventData. Dzięki temu nowo utworzony event może być swobodnie deserializowany i serializowany, zachowując jednocześnie spójną strukturę eventów. To pozwala nam elastycznie reagować na różne sytuacje, nie ograniczając się do stałego contentu eventów, który może się różnić w zależności od kontekstu.

4
[java, jackson, generyczność, koncepcja]

Więcej od Dominik Martyniak

Więcej artykułów