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 – redundantnych 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 backend-u – 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 Controller-a?

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

  • Obiekty wyjściowe w zależności od technologii określane są najczęściej jako: Response, ResponseObject, ViewModel, ResponseModel itp.
  • W celu zachowania kontroli nad wyjściem systemu, zaleca się stosowanie osobnych formatów zwrotek.
  • Mapowanie do postaci wynikowej powinno odbywać się w jak najwyższej warstwie, np. w kontrolerze.
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