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.
Dodaj komentarz