Spis treści
Zasady SOLID
Dobre oprogramowanie na najniższym poziomie powinno być zgodne z zasadami SOLID. Znajomość tych zasad przyda się również przy projektowaniu całej architektury. Można je bowiem rozszerzyć na komponenty, moduły i paczki.
Poniżej przedstawiam zasady SOLID.
SRP — Zasada pojedynczej odpowiedzialności – (ang. Single Responsibility Principle)
Każdy moduł powinien odpowiadać dokładnie przed jednym aktorem (mieć dokładnie jedną zmian). Należy oddzielać od siebie fragmenty kodu, na który wpływ mają różni aktorzy.
OCP — Zasada otwarte-zamknięte – (ang. Open-Closed Principle)
Fragment oprogramowania powinien być otwarty na rozbudowę, ale zamknięty na modyfikacje.
LSP — Zasada podstawienia Liskov – (ang. Liskov Substitution Principle)
Funkcje, które używają wskaźników lub referencji do klas bazowych, muszą być w stanie używać również obiektów klas dziedziczących po klasach bazowych, bez dokładnej znajomości tych obiektów.
(źródło: https://pl.wikipedia.org/wiki/Zasada_podstawienia_Liskov). Klasy dziedziczące powinny jedynie rozszerzać klasę bazową i nie zmieniać jej działania.
ISP — Zasada segregacji interfejsów – (ang. Interface Segregation Principle)
Wiele dedykowanych interfejsów jest lepsze niż jeden ogólny. (źródło: https://pl.wikipedia.org/wiki/SOLID_(programowanie_obiektowe)). Należy unikać budowania zależności od niepotrzebnych elementów, ponieważ wnosi to niepotrzebne zależności i może powodować nieprzewidziane problemy.
DI — Zasada odwracania zależności – (ang. Dependency Inversion Principle)
Wysokopoziomowe moduły nie powinny zależeć od modułów niskopoziomowych — zależności między nimi powinny wynikać z abstrakcji. (źródło: https://pl.wikipedia.org/wiki/SOLID_(programowanie_obiektowe)). Elastyczny system zbudowany jest w taki sposób, że wszelkie zależności dotyczą abstrakcji, a nie konkretnych elementów. Instrukcje import i using powinny odnosić się do abstrakcji.
Przykład zastosowania zasady SRP
Rozważmy przypadek klasy pracownika (Employee), która liczy czas pracy, jego wynagrodzenie oraz zapisuje te dane w bazie danych. Ma na nią wpływ trzech aktorów: na liczenie czasu pracy wpływ mają kadry, na liczenie wypłaty wpływ ma księgowość, a na zapis do bazy danych wpływ ma dział techniczny. Należy rozdzielić te trzy odpowiedzialności do trzech różnych klas.
Problem 1 – odpowiedzialność
W klasie Employee jest metoda do liczenia godzin pracy pracownika (RegularHours), z której korzysta metoda ReportHours oraz CalculatePay. Jeżeli dział księgowości zechce inaczej liczyć wypłatę pracownika poprzez zmianę sposobu liczenia godzin pracy w metodzie RegularHours, to przez to zmieni się również działanie metody ReportHours. A tego mieliśmy nie zmieniać. Dział kadr, kiedy to odkryje, będzie bardzo zaskoczony. Może on jednak odkryć to dopiero po wielu latach. A to jest duży problem.
Problem 2 – łączenie zmian (mergowanie)
W klasie Employee dwóch programistów jednocześnie zmienia kod z dwóch różnych powodów (ta klasa zależna jest od trzech aktorów) na własnych branchach. Podczas złączenia kodu do głównego brancha mogą natrafić na konflikty oraz złączony kod może nie działać tak, jak się tego od niego oczekuje.
Rozwiązanie
Rozwiązaniem jest wydzielenie z klasy Employee trzech klas, które będą realizowały konkretne działania związane z klasą Employee dla poszczególnych aktorów. Poniższy obrazek prezentuje ten podział.
Źródło strona 85 — Rysunek 7.3 – Trzy klasy nie wiedzą o istnieniu pozostałych.
Można to rozwiązać na dwa sposoby:
Sposób 1: Za pomocą wzorca Fasada. Klasa Employee odpowiada tylko za przekazywanie sterowania do odpowiednich klas. Przedstawia to poniższy obrazek.
Źródło: Czysta architektura. Struktura i design oprogramowania. Przewodnik dla profesjonalistów — Robert C. Martin. — s. 86 — Rysunek 7.4 – Wzorzec projektowy Fasada.
Sposób 2: Klasa Employee ma w sobie swoje dane oraz implementacje najważniejszej dla niej metody. Pozostałe metody zaimplementowane są w innych klasach. Przedstawia to poniższy obrazek.
Źródło: Czysta architektura. Struktura i design oprogramowania. Przewodnik dla profesjonalistów — Robert C. Martin. — s. 86 — Rysunek 7.4- Najważniejsze metody są w klasie Employee, która jednocześnie służy za fasadę dla mniejszych funkcji.
Przykład zastosowania OCP
Poniższy rysunek przedstawia architekturę aplikacji prezentującej raporty, których dane zapisane są w bazie danych. Najważniejszym komponentem (najwyższego poziomu) jest Interaktor. Zawiera on logikę biznesową. Inne komponenty to moduły peryferyjne. Jest tutaj zaprezentowana hierarchia oparta o poziomy. Interaktor jest poziomy najwyższego (ponieważ zawiera reguły biznesowe) i z tego powodu jest najbardziej chroniony. To od niego zależy cała reszta aplikacji. Należy zauważyć, że wszystkie zależności komponentów skierowane są od najniższego poziomu, do najwyższego (czyli do Interaktora). Warto zaznaczyć, że interfejs FinancialReportRequester służy do ochronienia klasy FinancialReportController przed zbytnią wiedzą o komponencie Interaktor, np. sprawia, że nie ma on wiedzy o klasie FinancialEnitites. Gdyby FinancialReportController wiedział też o FinancialEnitites, to zmiana tej klasy wpływałaby niepotrzebnie na komponent Kontroler. Nie używa on bezpośrednio FinancialEnitites, więc nie powinien go w ogóle znać. Oprócz ochrony Interaktora przed zmianami w komponentach niższego poziomu należy również chronić inne komponenty przed zmianami w komponentach wyższego poziomu poprzez ukrywanie ich szczegółów.
Źródło: Czysta architektura. Struktura i design oprogramowania. Przewodnik dla profesjonalistów — Robert C. Martin. — s. 92 — Rysunek 8.2 – Podział zadań na klasy i zgrupowanie ich w komponenty.
Przykład zastosowania DIP
Stosowanie Zasady odwrócenia zależności opiera się na założeniu, że abstrakcje (a z pewnością powinny) są stabilniejsze niż ich implementacja. Tworząc oprogramowanie, warto zastosować się do poniższych wytycznych:
- Nie należy odnosić się do konkretnych (ulotnych) klas.
- Nie należy dziedziczyć po konkretnych (ulotnych) klasach.
- Nie należy nadpisywać metod klas, lecz stworzyć funkcję abstrakcyjną i ją implementować na różne sposoby.
- Nie należy powoływać się na konkretne (ulotne) elementy.
Poniżej przykład podziału komponentów. Czarna linia pokazuje ich podział. Komponent z górnej części rysunku jest abstrakcyjny i zawiera wszystkie wysokopoziomowe reguły biznesowe. Dolna część rysunku przedstawia komponent konkretny, który zawiera szczegóły implementacji. Przepływ sterowania przecina czarną linię w przeciwnym kierunki do zależności w kodzie. Jest tu zastosowana zasada odwrócenia zależności — zależności odwrócone są względem przepływu sterowania.
Źródło: Czysta architektura. Struktura i design oprogramowania. Przewodnik dla profesjonalistów — Robert C. Martin. — s. 110 — Rysunek 11.1 Użycie wzorca projektowego fabryka abstrakcyjna w celu ujarzmienia zależności
Wszystkie posty związane z mini projektem: Budowa czystej architektury:
- Początek mini projektu: Budowa czystej architektury
- Architektura
- Paradygmaty programowania
- Zasady SOLID w kontekście architektury
- Spójność komponentów
- Łączenie komponentów
- Struktura oprogramowania
- Zasady i poziomy
- Czysta architektura
- Budowanie Czystej architektury
- Podsumowanie projektu: Budowanie czystej architektury
- Moje notatki z nauki szybkiego czytania
Źródła
Obrazy
Materiały
- Czysta architektura. Struktura i design oprogramowania. Przewodnik dla profesjonalistów — Robert C. Martin.
- https://pl.wikipedia.org/wiki/Zasada_podstawienia_Liskov
- https://pl.wikipedia.org/wiki/SOLID_(programowanie_obiektowe)