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ę.

Co znaczy DRY?

Zasada „don’t repeat yourself” mówi nam, abyśmy wydzielali w naszym kodzie miejsca, które będą realizowały konkretną logikę. Dzięki odwoływaniu się do takich funkcji czy komponentów, które są jedynym źródłem prawdy dla realizowanych operacji, nie powielamy tego samego kodu, a nasz system staje się spójniejszy.

Kod odpowiedzialny za realizację zadania X nie powinien być nigdy powielany. Umieść go w miejscu najbardziej dla niego odpowiednim i odwołuj się do niego za pomocą funkcji i metod.

Zasada DRY w praktyce!

Wyobraźmy sobie klasę typu serwis, która realizuje pewne zadania operacji na danych. Udostępnia ona między innymi metody: delete()update() czy notify(). W każdej z tych metod musimy najpierw sprawdzić czy wpis istnieje, zanim wykonamy jakąś operację. Jeżeli zasób nie istnieje zwracamy stosowny błąd.

class PostsService {
  constructor(
    private readonly postRepository: IPostRepository,
    private readonly notificationsService: INotificationsService
  ) {}

  async delete(postId: string) {
    const post = await this.postRepository.findOneById(postId);

    if (!post) {
      throw new LogicError(`Selected post ${postId} does not exist.`);
    }

    await this.postRepository.delete({ id: postId });
  }

  async notifyAbout(postId: string) {
    const post = await this.postRepository.findOneById(postId);

    if (!post) {
      throw new LogicError(`Selected post ${postId} does not exist.`);
    }

    await this.notificationsService.notify(
      new PostNotification(post)
    );
  }

  async update({ postId, ...dataToUpdate }: UpdatePostDto) {
    const post = await this.postRepository.findOneById(postId);

    if (!post) {
      throw new LogicError(`Selected post ${postId} does not exist.`);
    }

    await this.postRepository.update({ id: postId }, dataToUpdate);
  }
}

Jak się pewnie domyślasz, w każdej z powyższych metod powieliliśmy operację pobierania zasobu i reagowania na sytuację, kiedy on nieistnieje. Przez to złamaliśmy zasadę DRY!

Programowanie z DRY

Zaprogramowanie powyższego przykładu zgodnie z DRY nie jest trudne i zrobić to możemy na naprawdę wiele sposobów. W naszym repozytorium możemy stworzyć metodę findOneByIdOrFail(), która w przypadku nieistniejącego zasobu zwróci stosowny błąd. Możemy także wydestylować powielaną logikę do prywatnej metody w serwisie.

class PostsService {
  constructor(
    private readonly postRepository: IPostRepository,
    private readonly notificationsService: INotificationsService
  ) {}

  async delete(postId: string) {
    const post = await this.getByIdOrFail(postId);

    await this.postRepository.delete({ id: postId });
  }

  async notifyAbout(postId: string) {
    const post = await this.getByIdOrFail(postId);

    await this.notificationsService.notify(
      new PostNotification(post)
    );
  }

  async update({ postId, ...dataToUpdate }: UpdatePostDto) {
    const post = await this.getByIdOrFail(postId);

    await this.postRepository.update({ id: postId }, dataToUpdate);
  }

  private async getByIdOrFail(postId: string): Promise<Post> {
    const post = await this.postRepository.findOneById(postId);

    if (!post) {
      throw new LogicError(`Selected post ${postId} does not exist.`);
    }

    return post;
  }
}

Osobiście, wolę jednak wersję z repozytorium, ponieważ takie zachowanie, może być potrzebne np. w innym serwisie.

class PostsService {
  constructor(
    private readonly postRepository: IPostRepository,
    private readonly notificationsService: INotificationsService
  ) {}

  async delete(postId: string) {
    const post = await this.postRepository.getByIdOrFail(postId);

    await this.postRepository.delete({ id: postId });
  }

  async notifyAbout(postId: string) {
    const post = await this.postRepository.getByIdOrFail(postId);

    await this.notificationsService.notify(
      new PostNotification(post)
    );
  }

  async update({ postId, ...dataToUpdate }: UpdatePostDto) {
    const post = await this.postRepository.getByIdOrFail(postId);

    await this.postRepository.update({ id: postId }, dataToUpdate);
  }
}

DRY vs. SRP

DRY można mylić z Single Responsibility Principle z SOLID-a. Nie należy ich jednak postrzegać, jako tożsamych samych zasad, ponieważ możemy spełniać SRP, zaś nie spełniać DRY i na odwrót. Metoda może mieć jeden powód do zmiany, jedno zadanie do realizacji i tym samym spełniać solid-owe S, ale może także duplikować kod znajdujący się w trzech innych metodach i nie spełniać DRY.

Podsumowanie

  • DRY to zasada mówiąca o tym, aby nie powtarzać swojego kodu.
  • Zgodnie z „don’t repeat yourself”, każda funkcjonalność powinna posiadać dedykowane dla siebie miejsce w systemie.
  • Nie należy mylić SRP z DRY, ponieważ są to reguły dotyczące innych aspektów.
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