Skupimy się dzisiaj na klasach typu service, które najczęściej stosowane są do obsługi logiki biznesowej w różnych wzorcach architektonicznych. Co należy do kompetencji serwisu? Jak poprawnie wydzielić zadania takiej klasy? Sprawdź ten wpis.
export class ReservationsService {
constructor(
private readonly reservationRepository: ReservationRepository,
) {}
delete = (reservation: ReservationEntity): Promise<ReservationEntity> =>
this.reservationRepository.remove(reservation);
getOneById = (id: string): Promise<ReservationEntity> =>
this.reservationRepository.findOne(id);
getAll = (): Promise<ReservationEntity[]> =>
this.reservationRepository.get();
create(createReservationDTO: CreateReservationDTO): Promise<ReservationEntity> {
const reservation = ReservationEntity.create(createReservationDTO);
return this.reservationRepository.save(reservation);
}
update(reservation: ReservationEntity, updateReservationDTO: UpdateReservationDTO): Promise<ReservationEntity> {
const updated = { ...reservation, ...updateReservationDTO };
return this.reservationRepository.save(updated);
}
}
Pomijając brak spełnienia reguły CQS, mamy tutaj pośrednika, który w ciałach metod realizuje jeszcze jakieś dodatkowe, proste działania, oprócz używania repozytorium. Problem z takimi serwisami pojawia się, gdy logika, którą trzeba zaimplementować jest troszkę bardziej skomplikowana. Powiedzmy, że przed utworzeniem rekordu rezerwacji, chcemy sprawdzić czy wybrane miejsce jest wciąż dostępne, a w przypadku udanego procesu poinformować klienta o tym fakcie.export class ReservationsService {
constructor(
private readonly reservationRepository: ReservationRepository,
private readonly resourceReservationsService: ResourceReservationsService,
private readonly notificationsService: NotificationsService,
private readonly messagesService: MessagesService,
) {}
async create(createReservationDTO: CreateReservationDTO): Promise<void> {
const resourceReservations = await this.resourceReservationsService.getReservations(
createReservationDTO.resourceId,
{ from: createReservationDTO.from, to: createReservationDTO.to },
);
if (resourceReservations.length > 0) {
throw ReservationException.resourceNotAvailable();
}
const reservation = ReservationEntity.create(createReservationDTO);
await this.reservationRepository.save(reservation);
const notification = new ReservationMadeNotification(reservation);
await this.notificationsService.notify(createReservationDTO.userId, notification);
}
}
Widoczny tutaj problem jest bardzo powszechny - potrzeba dodać nowy fragment logiki do istniejącego kodu? Pyk, nowa zależność w konstruktorze i zwiększanie objętości oraz złożoności usługi. Takie serwisy łamią zasady SOLID - głównie, mam na myśli tutaj podatność na modyfikacje w przypadku chęci dodania czegoś do procesu, a także brak pojedynczej odpowiedzialności.export class ResourceReservationsService {
constructor(
private readonly resourceReservationRepository: ResourceReservationRepository,
) {}
async validateResourceAvailability(id: string, range: ResourceDatesRange): Promise<void> {
const resourceReservations = await this.resourceReservationRepository.findByResourceIdAndDatesRange(
id,
range,
);
if (resourceReservations.length > 0) {
throw ResourceException.resourceNotAvailable(id, range);
}
}
}
export class ReservationsService {
constructor(
private readonly reservationRepository: ReservationRepository,
) {}
create(createReservationDTO: CreateReservationDTO): Promise<void> {
const reservation = ReservationEntity.create(createReservationDTO);
await this.reservationRepository.save(reservation);
}
}
export class ReservationFacade {
constructor(
private readonly reservationsService: ReservationsService,
private readonly resourceReservationsService: ResourceReservationsService,
private readonly eventBus: EventBus
) {}
async create(createReservationDTO: CreateReservationDTO): Promise<void> {
await this.resourceReservationsService.validateResourceAvailability(createReservationDTO.resourceId, {
from: createReservationDTO.from,
to: createReservationDTO.to,
});
await this.reservationsService.create(createReservationDTO);
await this.eventBus.publish(new ReservationMade(createReservationDTO.id));
}
}
Jeżeli nie chcemy używać fasady, możemy pozostać przy service classes. Ta wersja zakłada utworzenie prywatnej metody walidującej, która wywoływana jest przed kreacją rezerwacji. export class ReservationsService {
constructor(
private readonly reservationRepository: ReservationRepository,
private readonly resourceReservationRepository: ResourceReservationRepository,
private readonly eventBus: EventBus,
) {}
async create(createReservationDTO: CreateReservationDTO): Promise<void> {
await this.validateResourceAvailability(createReservationDTO.resourceId, {
from: createReservationDTO.from,
to: createReservationDTO.to,
});
const reservation = ReservationEntity.create(createReservationDTO);
await this.reservationRepository.save(reservation);
await this.eventBus.publish(new ReservationMade(reservation));
}
private async validateResourceAvailability(id: string, range: ResourceDatesRange): Promise<void> {
const resourceReservations = await this.resourceReservationRepository.findByResourceIdAndDatesRange(
id,
range,
);
if (resourceReservations.length > 0) {
throw ResourceException.resourceNotAvailable(id, range);
}
}
}
SOLID (Single responsibility principle, Open/closed principle, Liskov substitution principle, Interface segregation principle, Dependency inversion principle), czyli pięć zasad programowania obiektowego, które każdy powinien przestrzegać.
Sprawdź ten wpisAby projektować dobrą architekturę oprogramowania trzeba najpierw zaznajomić się z jej przesłankami, rodzajami, a także sposobami wdrażania. Architektura nie jest przecież czymś stałym, co wszędzie implementowane jest w jednakowy sposób.
Sprawdź ten wpisCzym jest podejście composition over inheritance? Dzisiaj o tym, dlaczego każdy powinien przemyśleć czy dziedziczenie klas w jego projekcie jest naprawdę potrzebne. Prawdopodobnie powinieneś ograniczyć dziedziczenie klas na rzecz ich kompozycji.
Sprawdź ten wpisCommand-Query Separation, czyli zasada o rozdzielaniu zadań metod tak, aby były jedynie komendami lub zapytaniami. Jeżeli Twoja metoda realizuje logikę i zwraca wyniki, lub podczas pobierania danych wykonuje side taski, to łamie ona CQS.
Sprawdź ten wpisOmawiam dzisiaj mało znaną zasadę, której stosowanie skutkuje posiadaniem łatwo utrzymywalnego i testowalnego kodu. Mowa o prawie demeter, które w najprostszym ujęciu zakłada, że obiekty powinny operować jedynie na najbliższym im otoczeniu.
Sprawdź ten wpisFanatyk 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.