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:

DRY: Don't Repeat Yourself

Zasada DRY, to po przełożeniu na język polski: nie powtarzaj się. Brzmi bardzo banalnie, jednak dosyć często okazuje się, że mamy problem z jej stosowaniem w kodzie. Dziś dowiesz się, jak nie łamać tej reguły, a co za tym idzie nie powtarzać się.

Sprawdź ten wpis

YAGNI: You aren't gonna need it

YAGNI to kwintesencja zasad clean code. Dotyczy ona bezużyteczności kodu, a dokładniej, konieczności usuwania tych fragmentów, które nie są potrzebne. W myśl "You aren't gonna need it" nie powinniśmy tworzyć niczego więcej, niż to, co jest potrzebne.

Sprawdź ten wpis

KISS: keep it simple, stupid!

Zasada KISS: "Keep it simple, stupid", może zostać dosłownie przetłumaczona na: "rób to prosto, głupku". Mówi ona o tym, abyśmy tworzyli kod w jak najprostszy i najbardziej czytelny sposób. Już dziś sprawdź, czego się wystrzegać, aby spełniać KISS.

Sprawdź ten wpis

Wzorce projektowe

Wzorce projektowe zostały stworzone po to, aby nie wymyślać przysłowiowego koła na nowo. Znajomość wzorców projektowych i umiejętność ich stosowania pozwala na szybkie rozwiązywanie problemów. Wpis ten radzi, jakie wzorce zastosować u siebie.

Sprawdź ten wpis

Wzorzec singleton

Kolejny kreacyjny wzorzec projektowy omawiany na łamach tego bloga: singleton. Wzorzec dookoła którego narosło wiele mitów i legend. Dziś o tym dlaczego singleton jest antywzorcem, jakie problemy powoduje oraz kiedy warto po niego sięgnąć.

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.