Usługa – Service Class

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.

Logika biznesowa

W większości wzorców architektonicznych w ten czy inny sposób stosujemy podejście warstwowe. Dzielimy cykl życia zapytania na kilka kroków – warstw przez które musi ono przejść. Wspominałem już o warstwie UI czyli początkowym miejscu, w którym rozbija się zapytanie do naszego systemu. Warstwa UI po zwalidowaniu danych i ich przeparsowaniu deleguje zazwyczaj kolejne zadania do niższych warstw. Ilość tych poziomów jest różna, w zależności od zastosowanego wzorca architektonicznego, ale w większości przypadków mamy miejsce w którym realizowana jest logika biznesowa. Takim miejscem mogą być klasy typu usługa: service.

Problem z Service Classes

Na początek, czym jest klasa serwisowa? Odpowiedź jest bardzo prosta: to zależy. Dla jednych jest to pośrednik do warstwy dostępu do danych, dla innych odzwierciedlenie pewnej grupy problemów za pomocą metod. Ci pierwsi bardzo często dedykują rozwiązania serwisów per encja, per repozytorium. Jest dla nich czymś oczywistym, iż dostęp do warstwy danych musi zostać otoczony klasą serwisową. Takie rozwiązanie może wyglądać następująco:

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.

Ładna usługa

Przedstawiony powyżej kod możemy poprawić na bardzo wiele sposobów i nie ma złotego środka na to, jak to zrobić. Możemy użyć wzorca fasady (powiem o nim w przyszłości) i wywołać w niej operacje na usługach, a ją samą w kontrolerze. Możemy także przyjąć, że klasa serwisowa będzie odzwierciedlała konkretny problem biznesowy, a do jego rozwiązania użyje innych mechanizmów. 

Serwisy odpowiedzialne są za zadania, które zostały im zlecone. Fasada wywołuje usługi, a następnie – w przypadku udanej rezerwacji – wywołuje event, który zostanie obsłużony w tle, aby wysłać powiadomienie.

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);
    }
  }
}

Clue serwisów

Tworzenie małych usług zorientowanych na jeden, konkretny problem biznesowy, ułatwia proces testowania, a także utrzymanie takiego kodu w przyszłości. Pozostaje jeszcze jedna kwestia, używanie jednego serwisu w drugim. Powinniśmy się wystrzegać takiego kodu, jednak zależy to oczywiście od ortodoksyjności programisty. Argumenty za tym, aby nie łączyć ze sobą serwisów są proste: niższa złożoność i łatwe testy, a także transparentność procesów biznesowych.  Starajmy się postrzegać klasy serwisowe, jako odizolowane jednostki obliczeniowe do których wstrzykujemy jak najmniej zależności, a za ich pomocą realizujemy logicznie spójną część operacji biznesowej. 

Podsumowując

  • Usługa to klasa realizująca założenia logiki biznesowej.
  • Klasy serwisowe stosowane są w wielu wzorcach architektonicznych.
  • Powinniśmy tworzyć małe, atomowe serwisy, które skupione będą na jednym problemie, bądź grupie połączonych ze sobą problemów biznesowych.
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