Obiekt wyjściowy (Response Object)

Czy zastanawiałeś/aś się kiedyś co powinien zwracać kontroler? Czyli o tym co (i dlaczego) powinna zawierać zwrotka z klasy kontrolującej. Dlaczego bezpośrednie zwracanie encji jest złym pomysłem oraz jak tego uniknąć?


Response Object

Obiekt wyjściowy, popularnie zwany: Response czy ResponseObject, jest ostateczną formą wyniku działania pewnej logiki, która zwracana jest do klienta. W różnych technologiach możemy spotkać się także z określeniem ViewModel lub ResponseModel. Wiele osób zadaje sobie pytanie, jaki jest sens tworzenia tego typu - na pierwszy rzut oka - redundatnych klas. Odpowiedź jest bardzo prosta, aby mieć pełną kontrolę nad tym, co zostaje udostępnione poza system. 

Zwracanie encji

Mamy tutaj przykład encji, która zawiera pewne pola. Klasa serwisowa odpowiedzialna za tworzenie tego typu rekordów, jako wynik, zwraca nowo utworzone obiekty. Następnie kontroler oddaje do klienta dane otrzymane bezpośrednio z serwisu, ponieważ frontend w celu uniknięcia kolejnego zapytania chce od razu wyświetlić nowo dodany rekord na pewnym listingu. Pomijając złamanie zasad SOLID, może wydawać się, że nie ma tutaj żadnego problemu. Problem jednak jest, bo wyobraźmy sobie teraz sytuację, że do wyżej pokazanej encji dołożymy jakieś bardzo wrażliwe dane, np. numer PESEL, a na naszym wspomnianym listingu nie musimy, a nawet nie możemy wyświetlić tej informacji. My jednak zwracamy dane, których raz, że nie powinniśmy zwracać, a dwa, że nie są one nikomu potrzebne. Ktoś może użyć narzędzi deweloperskich swojej przeglądarki i przejrzeć ruch sieciowy, tym samym zobaczy numer PESEL, którego tak naprawdę tam być nie powinno. Inna sytuacja to otwarte API, gdzie zewnętrzny klient może korzystać z usług naszego systemu, ale nie powinien widzieć pewnych danych. Kolejny przykład, jeżeli frontend w jakiś sposób automatycznie wyświetla wszystkie pola, które przyjdą z backendu - wyświetli wtedy numer PESEL, którego założenia biznesowe nie obejmowały. Może Ci się wydawać, że takie sytuacje to skrajność, ale uwierz mi na słowo, że tak nie jest. Zawsze, jeśli konieczne jest zwrócenie czegoś na zewnątrz, to powinniśmy określić postać "tego czegoś". 

@Entity('customers')
export class CustomerEntity extends BaseEntity {
  firstName: string;
  lastName: string;
  phoneNumber: string;
}

@Controller('customers')
export class CustomersController {
  constructor(private readonly customersService: CustomersService) {}

  @Post()
  create(@Body() request: CreateCustomerRequest): Promise<CustomerEntity> {
    return this.customersService.create(request);
  }
}

Co powinien zawierać response z Controllera?

Wiemy już więc, że obiekty wyjściowe są nam potrzebne, jednak teraz powiedzmy sobie jak je tworzyć i co najważniejsze - gdzie? Spójrzmy więc na ten przykład:

export class CustomerResponse {
  firstName: string;
  lastName: string;
  phoneNumber: string;
}

@Controller('customers')
export class CustomersController {
  constructor(private readonly customersService: CustomersService) {}

  @Post()
  async create(@Body() request: CreateCustomerRequest): Promise<CustomerResponse> {
    const customer = await this.customersService.create(request);
    
    const response = CustomerResponseMapper.mapEntity(customer);
    
    return response;
  }
}
Mamy tutaj jakąś ogólną klasę obiektu wyjściowego, dla wyniku działania logiki tworzącej nowego klienta. Klasa serwisowa zwraca nowo utworzony obiekt, ale w kontrolerze ma miejsce wywołanie mappera, który konwertuje obiekt customer do postaci obiektu wyjściowego, a następnie zwraca jego wynik.

Nasuwa się pytanie, dlaczego nie zrobić tego mapowania w klasie serwisowej i od razu zwrócić gotowy wynik. Nie jest to dobre podejście. Klasy serwisowe z założenia powinny być osobnymi bytami, które nie mogą znać swojego kontekstu wywołania - powiem o tym więcej podczas omawiania zagadnień z warstwy logiki. Niemniej jednak, wyobraźmy sobie sytuację, że ta klasa serwisowa używana jest także w innym miejscu, bo potrzebujemy stworzyć nowego klienta np. ad-hoc, podczas pewnego procesu, lecz format wyjścia tego procesu jest inny od wyjścia zaprezentowanego powyżej, wtedy ma miejsce ifologia lub duplikowanie kodu. 

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.