W moim poprzednim poście Piramida Refaktoryzacji – przykład mówiłem o poziomach piramidy refaktoryzacji na przykładzie transformacji dokonanych na przykładowym kodzie w języku Java. Na warsztat wziąłem najprostszy przypadek z książki „Refaktoryzacja do Wzorców” aby wyjaśnić pierwsze cztery poziomy tej piramidy. Jeśli nie przeczytałeś tego artykułu – przeczytaj go wcześniej, ponieważ obecny artykuł jest kontynuacją.

Ten post uzupełnia temat, wyjaśniając powiązania między Piramidą Refaktoryzacji a zasadami S.O.L.I.D. Poniżej przedstawię, jak każda z reguł SOLID-a staje się i jest podstawą dla każdego poziomu transformacji w piramidzie. Rozważania zakończymy na najwyższym poziomie piramidy odpowiadającym za architekturę.

  • ZASADA POJEDYNCZEJ ODPOWIEDZIALNOŚCI

Przede wszystkim przyjrzyjmy się bliżej, co oznacza zasada pojedynczej odpowiedzialności. Czy jest to jeden powód zmiany? Kiedy słyszę taką definicję to jest ona dla mnie niejasna… Kiedy więc sam ją wyjaśniam, zawsze mówię o niej w kontekście poziomów abstrakcji. Pojedyncza odpowiedzialność oznacza wiele rzeczy wyrażonych / zawartych w jednym słowie na wyższym poziomie abstrakcji ale może także odnosić się do pojedynczej małej rzeczy na niższym poziomie abstrakcji.

W pierwszym przypadku rodzaj klasy menedżera / kontrolera to najwyższy wpis, który deleguje do podklas do wykonywania szczegółów. W drugim przypadku wykonywane są małe działania.

Metody powinny zrobić jedną rzecz, jak pisze wujek Bob w swojej książce “Czysty Kod”. Kiedy jego powyższe zdanie łączę z moim wyjaśnieniem to całość nabiera większego sensu. Metoda powinna odnosić się do pojedynczego poziomu abstrakcji. Metoda nie powinna zawierać kodu, który deleguje logikę do klasy niższego poziomu i jednocześnie sama zawiera detale dotyczące niskiego poziomu. Metoda nigdy nie powinna zawierać dwóch różnych poziomów abstrakcji. W takim przypadku powinna delegować wykonanie obu części do klasy z niższego poziomu.

To prowadzi nas do rzeczy takich jak

  • podział pętli na 2 osobne pętle
  • zastąpienie pętli strumieniem
  • ekstrakcja mniejszych metod
  • ekstrakcja metody złożonej
  • ekstrakcja mniejszych klas

Dokonajmy więc kilku z powyższych przekształceń. Przyzwolenie na eksperymenty to pierwszy krok w rozwoju!

   private boolean checkMaxPrice(float price, Product product) {
       return product.getPrice() < price;
   }
   private boolean checkMinAge(float age, Product product) {
       return product.getMinAge() < age;
   }

Kontynuujmy eksperyment w kierunku uzyskania mniejszych klas.

public class BelowPriceSpec {
   private float price;

   public BelowPriceSpec(float price) {
       this.price = price;
   }

   public boolean isSatisfiedBy(Product product) {
       return product.getPrice() < price;
   }
}
  • ZASADA SEGREGACJI INTERFEJSÓW

Kiedy mamy już kilka podobnych klas, możemy pomyśleć o wspólnym / jednolitym sposobie, w jaki z nimi będzie się komunikować. Prowadzi to do ukrycia ich za interfejsem, który ukryje za sobą pewien poziom abstrakcji.

Kontynuując podejście do refaktoryzacji z poprzedniego artykułu, zakończymy na zestawie klas takich jak MinAgeSpec, DeliveryTimeSpec, AndSpec.

Ostatecznie wszystkie te klasy będą implementować interfejs Spec.

public interface Spec {
   boolean isSatisfiedBy(Product product);
}
  • ZASADA OTWARTE / ZAMKNIĘTE

Gdy mamy już konfigurację interfejsów, to wspólna logika może się opierać na kontraktach z nich wynikających. Logika oparta na założeniach kontraktów wynikających z interfejsów raczej się nie zmieni i będzie działać prawidłowo z każdą kolejną prawidłową implementacją takiego interfejsu. Implementacja interfejsu może być dostarczana jako parametr konstruktora takiej klasy albo jako parametru metody która go potrzebuje.

Kontynuując ten tok myślenia można zobaczyć jak Open Closed Principle ujawnia się jako podstawa następnego poziomu Piramidy Refaktoryzacji. Spójrz poniżej na metodę „bySpec”, która prawdopodobnie już nie zostanie zmieniona w przyszłości a swoją logikę opiera się na kontrakcie interfejsu Spec.

public class ProductFinder {
   private List<Product> repository;

   public ProductFinder(List<Product> repository) {
       this.repository = repository;
   }

   public List<Product> bySpec(Spec spec) {
       return repository.stream()
               .filter(spec::isSatisfiedBy)
               .collect(Collectors.toList());
   }

   @Deprecated
   public List<Product> byBelowPrice(float price) {
       return bySpec(new BelowPriceSpec(price));
   }
}

Wkrótce prawdopodobnie pojawi się kolejna implementacja interfejsu Spec. Na przykład wymaganie ograniczenia na podstawie czasu dostawy przy wyborze przedmiotu do kupienia.

public class DeliveryDeadlineSpec implements Spec{
   private Datetime deadline;

   public DeliveryDeadlineSpec(Datetime  dealine) {
       this.deadline = deadline;
   }

   public boolean isSatisfiedBy(Product product) {
       Datetime latestDelivery =
           timeProvider.getTime().plusDays((product.getDeliveryTime())
       return deadline.before(latestDelivery);
   }
}
  • ZASADA ODWRÓCONYCH ZALEŻNOŚCI

Wreszcie nadszedł czas, aby nakreślić powstającą architekturę. Architektury pojawiają się na poziomie

  • klas
  • zestawów API
  • modułów
  • bibliotek
  • mikroserwisów
  • domen
… i zapewne wielu innych

Następnie przez pewien czas koncepcja rozwoju będzie jasna i dlatego pomysł zostanie kopiowany (ugh…). Podczas gdy kodu będzie coraz więcej, koncepcja może stać się ponownie niejasna i mglista. Wtedy będzie czas na ponowny przegląd i zmiany, aby powrócić do czytelności.

Wróćmy więc jeszcze do klasy ProductFinder i zobaczmy, w jaki sposób w niej użyta jest Zasada Odwróconych Zależności.

package pl.refactoring.ProductFinder
package pl.refactoring.Spec

package pl.refactoring.specs.BelowPriceSpec
package pl.refactoring.specs.MinAgeSpec
package pl.refactoring.spesc.DeliveryDeadlineSpec

Na koniec podkreślam, że to wyższy poziom abstrakcji (klasa ProductFinder) definiuje specyfikację interfejsu / kontraktu Spec. Następnie implementacje interfejsów mogą być dostarczane w różnych pakietach, różnych modułach lub nawet w różnych bibliotekach.

  • ZASADA PODSTAWIANIA LISKOV

W artykule nie zawarłem wyraźnego przypadku użycia Zasady Podstawiania Liskov, ale z pewnością znajduje ona swoje miejsce na poziomie klas. Wynika to z faktu, że zazwyczaj stosowanie tej zasady kończy się zastąpieniem dziedziczenia delegacją czy zmiany relacji pomiędzy dwoma klasami.

Spread the word. Share this post!