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:

SOLID z przykładami w TypeScript

SOLID (Single responsibility principle, Open/closed principle, Liskov substitution principle, Interface segregation principle, Dependency inversion principle), czyli pięć zasad programowania obiektowego, które każdy powinien przestrzegać.

Sprawdź ten wpis

Jak zostać architektem oprogramowania?

Aby projektować dobrą architekturę oprogramowania trzeba najpierw zaznajomić się z jej przesłankami, rodzajami, a także sposobami wdrażania. Architektura nie jest przecież czymś stałym, co wszędzie implementowane jest w jednakowy sposób.

Sprawdź ten wpis

Kompozycja zamiast dziedziczenia (composition over inheritance)

Czym jest podejście composition over inheritance? Dzisiaj o tym, dlaczego każdy powinien przemyśleć czy dziedziczenie klas w jego projekcie jest naprawdę potrzebne. Prawdopodobnie powinieneś ograniczyć dziedziczenie klas na rzecz ich kompozycji.

Sprawdź ten wpis

Command-Query Separation (CQS)

Command-Query Separation, czyli zasada o rozdzielaniu zadań metod tak, aby były jedynie komendami lub zapytaniami. Jeżeli Twoja metoda realizuje logikę i zwraca wyniki, lub podczas pobierania danych wykonuje side taski, to łamie ona CQS.

Sprawdź ten wpis

Prawo Demeter (The Law of Demeter)

Omawiam dzisiaj mało znaną zasadę, której stosowanie skutkuje posiadaniem łatwo utrzymywalnego i testowalnego kodu. Mowa o prawie demeter, które w najprostszym ujęciu zakłada, że obiekty powinny operować jedynie na najbliższym im otoczeniu.

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.