Klasa / funkcja mapująca (Mapper)

Niejednokrotnie zachodzi potrzeba zmiany jednej postaci danych do drugiej. Manualne tworzenie klas i ustawianie i właściwości "gdzie popadnie" może skończyć się źle, gdy zechcemy dodać nowe pole. Jak temu zaradzić? Sprawdź ten wpis.


Zamiana postaci danych

Klasa, bądź funkcja mapująca jest to kod odpowiedzialny za przerobienie jednego obiektu do postaci innego. W idealnym świecie powinna ona dodatkowo używać klas typu fabryka, aby tworzyć nowe, docelowe obiekty. Klasy mapujące są ostoją prostoty. Mogą posiadać kilka metod, które przyjmują pewien rodzaj obiektu i zwracają ten porządany. Jak zawsze, sposób użycia będzie się różnił od technologii i tym razem także od ortodoksyjności programisty, ponieważ naziści programowania powiedzą, że słuszniejsze z perspektywy czystego kodu będzie użycie fabryk i budowniczych. Zgadza się, jednak w kwestii czystości kodu trzeba mieć na uwadze także to, że powinien on być prosty w zrozumieniu. Nie sztuką jest stworzenie dwudziestu dodatkowych klas (fabryka + builder) dla dziesięciu encji, sztuką jest utrzymanie takiego kodu.

Przykład Mappera

Spójrzmy więc na ten przykład, który ukazuje najczęstszy case dla mapperów - zamiana wyniku logiki do postaci obiektu wyjściowego. W technologii, w której pracuję funkcja plainToInstance pochodzi ze specjalnej biblioteki transformującej i bardzo ułatwia życie. Może rodzić się teraz pytanie, to po co mi mapper, skoro mogę użyć wspomnianej funkcji? W celu uniknięcia problemów związanych ze zmianą, bądź zrezygnowaniem z tej biblioteki w przyszłości. Dodatkowo, w myśl zasad GRASP, powinniśmy posiadać miejsce, które najlepiej nadaje się do tworzenia danych obiektów i tylko w tym miejscu je tworzyć.

export class UserResponseObject {
  id: string;
  firstName: string;
  lastName: string;
  recommender?: UserResponseObject;
}

export class UserResponseObjectMapper {
  static mapUserEntity = (user: UserEntity): UserResponseObject =>
    plainToInstance(UserResponseObject, {
      ...user,
      recommender: plainToInstance(UserResponseObject, user.recommender),
    });
}

Klasa mapująca bez magii

Nie widzę przeszkód (i z drugiej strony w innych technologiach tak właśnie będą wyglądały mappery), aby stworzyć w ciele metody mapującej obiekt docelowy i manualnie ustawić wymagane pola. Pewnie wiele osób w tym momencie się oburzy, ponieważ złamałem conajmniej dwie zasady SOLID, a dodatkowo metoda mapująca jest statyczna. Zgadzam się, jednak dla mnie ważniejsze jest to, aby kod był prosty, poza tym refaktoryzacja tego fragmentu nie jest żadnym rocket-science. Wystarczy zwiększyć abstrakcję tej prostej operacji o kolejne klasy. W kwestii tego, że metoda jest statyczna, tutaj jest pewna słuszność i najprawdopodobniej gdyby nie fakt, iż w technologii w której pracuję testowanie metod statycznych jest bardzo proste, to użyłbym wstrzykiwania zależności dla klasy mapującej, a sama metoda nie byłaby statyczna. Warto także zwrócić uwagę, iż w innych językach programowania klasy te prawdopodobnie nazywać się będą fabrykami, jednak ogólnie rzecz biorąc mapper od fabryki różni się pod kilkoma aspektami, które postaram się w przyszłości (przy okazji omawiania wzorca factory) pokazać. 

export class UserResponseObjectMapper {
  static mapUserEntity(user: UserEntity): UserResponseObject {
    const response = new UserResponseObject();
    
    if (user.recommender) {
      response.recommender = UserResponseObjectMapper.mapUserEntity(user.recommender);
    }
    
    response.id = user.id;
    response.firstName = user.firstName;
    response.lastName = user.lastName;
    
    return response;
  }
}

Podsumowując




Polecane wpisy:

Wzorzec stan (state)

Dziś prezentuję kolejny wzorzec projektowy, który pozwala uniknąć drabinek if-else if. Mowa tutaj o wzorcu state, który idealnie nada się, jeśli posiadasz różne stany w swoim systemie oraz chcesz mieć możliwość płynnego przechodzenia pomiędzy nimi.

Sprawdź ten wpis

Wzorzec pełnomocnik (virtual proxy)

Wzorzec virtual proxy służy do tworzenia obiektów pośrednich, które nadzorują dostęp do obiektów dla których są pełnomocnikami. Dzięki temu, że pełnomocnik i klasa bazowa udostępniają jednakowy interfejs, możemy ukryć w pośredniku dodatkową logikę.

Sprawdź ten wpis

Wzorzec strategia (strategy pattern)

Jeżeli masz dość ifologii w swoim kodzie, to konieczne sprawdź czym jest czynnościowy wzorzec projektowy strategia. Pozwala on mądrze obsługiwać różne scenariusze w procesie i jednocześnie być fancy pod względem zasad SOLID.

Sprawdź ten wpis

Wzorzec adapter

Strukturalny wzorzec projektowy adapter to spore udogodnienie w walce z systemami legacy. Pozwala użyć niepasującego interfejsu w innym. Dzięki temu zabiegowi pomimo, iż nie posiadamy spójnej abstrakcji możemy zastosować pożądaną logikę.

Sprawdź ten wpis

Wzorzec dekorator

Strukturalny wzorzec projektowy dekorator, to lekarstwo na problemy z mnogością różnych podtypów. Pozwala on na dynamiczne generowanie kolejnych typów obiektów bazujących na pierwotnym typie, bez konieczności deklarowania nowych, dedykowanych klas.

Sprawdź ten wpis

Autor wpisu:

Gabriel Ślawski

Fanatyk czystego i prostego kodu. Zwolennik podejść DDD oraz Modular Monolith. Na codzień pracuje jako programista i architekt. Po godzinach spełnia się w projektach open source, udziela się na blogach oraz czyta książki o kosmosie i astrofizyce.