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

  • Klasy mapujące to bardzo elastyczna forma przejścia z postaci jednego, do postaci drugiego obiektu.
  • Logika mapowania może być zrealizowana na różne sposoby i w dużej mierze zależy od technologii.
  • Dla osób którym przedstawione rozwiązanie nie przypadło do gustu, polecam skorzystanie ze wzorców Factory Method +/ Builder, o których powiem w przyszłości.
Autor wpisu

blog@orbisbit.com

Komentarze

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Sprawdź również
  • CQRS – Command Query Responsibility Segregation

    Command Query Responsibility Segregation czyli CQRS. Jest to wzorzec projektowy, który rozdziela zadania odczytu i zapisu do osobnych modeli. Sprawdź ten wpis, aby dowiedzieć się kiedy i jak z niego skorzystać.

    Zobacz wpis

  • GRASP – kolejny zbiór zasad Clean Code do zapamiętania

    Pewnie większość z Was słyszała o zasadach SOLID. Są one bardzo rozpowszechnione i dosyć często stosowane, ale czy słyszeliście o GRASP? General Responsibility Assignment Software Patterns, to kolejna dawka zasad czystego kodu do zapamiętania.

    Zobacz wpis

  • Wzorzec strategia (strategy pattern)

    Jeżeli masz dość if-ologii 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.

    Zobacz wpis