Ten post uzupełnia temat refaktoryzacji, wyjaśniając powiązania między Piramidą Refaktoryzacji a zasadami SOLID. 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ę.
W moim poprzednim poście Jak czyścić kod według koncepcji piramidy refaktoryzacji? 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ą.
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.
– nie powinna zawierać kodu, który deleguje logikę do klasy niższego poziomu i jednocześnie sama zawiera detale dotyczące niskiego poziomu.
– 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
- utworzenie 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);
}
[FM_form id=”2″]
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
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.