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);
}
}
}
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 wpisYAGNI 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 wpisZasada 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 wpisWzorce 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 wpisKolejny 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 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.