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