Powrót do bloga

SOLID: Pierwsze 5 zasad projektowania obiektowego?

SOLID: Pierwsze 5 zasad projektowania obiektowego?

Wprowadzenie

SOLID to akronim mnemotechniczny oznaczający pięć zasad projektowania obiektowego, które zostały wprowadzone przez Roberta C. Martina, powszechnie nazywanego Uncle Bob. Zasady te mają na celu pomóc projektantom oprogramowania, architektom, inżynierom i programistom w tworzeniu bardziej elastycznego, łatwego w utrzymaniu i skalowalnego oprogramowania. Przestrzegając tych zasad, można projektować klasy, które są łatwiejsze do testowania, refaktoryzacji, ponownego użycia i rozszerzania.

Akronim SOLID oznacza:

S – Zasada jednej odpowiedzialności (Single Responsibility Principle)

O – Zasada otwarte-zamknięte (Open-Closed Principle)

L – Zasada podstawienia Liskov (Liskov Substitution Principle)

I – Zasada segregacji interfejsów (Interface Segregation Principle)

D – Zasada odwrócenia zależności (Dependency Inversion Principle)

W tym artykule wyjaśnimy każdą zasadę z osobna, aby zrozumieć, jak może ona pomóc w pisaniu lepszego kodu. Dodatkowo dodamy fragmenty kodu dla każdej zasady, aby pokazać, jak można je zastosować w swojej pracy programistycznej, a także czego należy unikać w architekturze czystego kodu. Aby zademonstrować te pojęcia, użyjemy języka programowania Kotlin stworzonego przez JetBrains i współtwórców open-source.

Zasada jednej odpowiedzialności

Zasada jednej odpowiedzialności (SRP) to zasada projektowania oprogramowania, która mówi, że każda klasa lub moduł w programie powinien mieć jedną, dobrze zdefiniowaną odpowiedzialność. Oznacza to, że klasa powinna mieć tylko jeden powód do zmiany i powinna być odpowiedzialna tylko za jedną część funkcjonalności programu.

Rozważmy poniższy fragment kodu jako przykład tego, jak tę zasadę można zastosować w języku Kotlin:

Słowo kluczowe data  w języku Kotlin wskazuje, że ta klasa jest klasą danych (data class), co oznacza, że jej przeznaczeniem jest przechowywanie danych i nie posiada ona żadnych złożonych zachowań. Używając tej klasy danych, możemy utworzyć klasę UserService  do zarządzania użytkownikami, jak pokazano poniżej:

W tym przykładzie klasa UserService ma jedną odpowiedzialność: zarządzanie użytkownikami w bazie danych. Każdy użytkownik jest reprezentowany przez data class User  z kodu udostępnionego wcześniej. Wszystkie metody w klasie UserService są powiązane z tą odpowiedzialnością, a zatem są spójne. Sprawia to, że klasa jest łatwiejsza do zrozumienia i utrzymania, ponieważ jasne jest, że wszystkie metody w klasie są powiązane z jednym, dobrze zdefiniowanym zadaniem.

Metoda naruszająca zasadę jednej odpowiedzialności (SRP) w klasie UserService byłaby taką, która nie jest powiązana z główną odpowiedzialnością klasy, jaką jest zarządzanie użytkownikami w bazie danych. Na przykład rozważmy następujący wariant klasy UserService :

W tym przykładzie metoda sendEmail  nie odnosi się do głównej odpowiedzialności klasy UserService, która polega na zarządzaniu użytkownikami w bazie danych. Ta metoda jest odpowiedzialna za wysyłanie wiadomości e-mail, co jest osobną kwestią od zarządzania użytkownikami. W rezultacie metoda ta narusza zasadę SRP, ponieważ wprowadza drugi powód do zmiany klasy UserService.

Aby przestrzegać zasady pojedynczej odpowiedzialności (Single Responsibility Principle), lepiej byłoby wydzielić funkcjonalność wysyłania wiadomości e-mail do osobnej klasy, takiej jak EmailService . Pozwoliłoby to klasie UserService skupić się na jej głównej odpowiedzialności, jaką jest zarządzanie użytkownikami, a klasie EmailService skupić się na odpowiedzialności za wysyłanie wiadomości e-mail.

Należy zauważyć, że zasada pojedynczej odpowiedzialności nie dotyczy liczby metod, jakie posiada klasa, ale raczej spójności tych metod i wyraźnego podziału odpowiedzialności wewnątrz klasy.

Zasada otwarte-zamknięte (Open-Closed Principle)

Zasada otwarte-zamknięte (OCP) to zasada projektowania oprogramowania, która mówi, że encje oprogramowania (takie jak klasy, moduły czy funkcje) powinny być otwarte na rozszerzanie, ale zamknięte na modyfikację. Oznacza to, że powinno być możliwe dodanie nowej funkcjonalności do klasy lub modułu bez zmiany jego istniejącego kodu.

Rozważmy klasę UserService z poprzedniego przykładu. Załóżmy, że chcemy dodać nową funkcję do klasy UserService , która pozwala nam wyszukiwać użytkowników po adresie e-mail. Jednym ze sposobów na zrobienie tego byłoby dodanie nowej metody do klasy UserService , jak wyróżniono w poniższym kodzie:

To podejście działa, ale narusza zasadę otwarte-zamknięte (Open Closed Principle), ponieważ musieliśmy zmodyfikować istniejącą klasę UserService w celu dodania nowej funkcji. Lepszym podejściem byłoby użycie dziedziczenia lub kompozycji w celu rozszerzenia funkcjonalności klasy UserService bez modyfikowania jej kodu.

Aby to osiągnąć, moglibyśmy utworzyć nową klasę o nazwie UserSearchService , która rozszerza klasę UserService i dodaje funkcjonalność wyszukiwania po adresie e-mail:

W tym przykładzie klasa UserSearchService jest otwarta na rozszerzanie, ponieważ zapewnia dodatkową funkcjonalność wykraczającą poza to, co oferuje UserService klasy. Jednocześnie klasa UserService pozostaje zamknięta na modyfikacje, ponieważ nie musieliśmy modyfikować jej kodu, aby dodać funkcję wyszukiwania po adresie e-mail.

Zasada podstawienia Liskov

Zasada podstawienia Liskov (LSP) to zasada projektowania oprogramowania, która mówi, że obiekty klasy nadrzędnej powinny być zastępowalne obiektami klasy podrzędnej bez wpływu na poprawność programu. Oznacza to, że klasa podrzędna powinna być odpowiednim zamiennikiem dla swojej klasy nadrzędnej i powinna zachowywać się w ten sam sposób, co klasa nadrzędna, gdy jest używana w tym samym kontekście.

Będziemy kontynuować demonstrację, używając klas User oraz UserService z poprzednich przykładów. Aby umożliwić rozszerzenie klasy User, używamy słowa kluczowego open  w języku Kotlin:

Oto oryginalna klasa UserService używająca klasy danych User powyżej:

Załóżmy, że chcemy utworzyć podklasę klasy User o nazwie AdminUser reprezentującą użytkowników z uprawnieniami administracyjnymi. Możemy to zrobić w ten sposób:

W tym przykładzie klasa AdminUser jest odpowiednim zamiennikiem dla klasy User, ponieważ zachowuje się w ten sam sposób co klasa User i może być używana wszędzie tam, gdzie oczekiwany jest obiekt klasy User. Na przykład możemy użyć klasy AdminUser z klasą UserService  w ten sposób:

Ten kod jest poprawny, ponieważ klasa AdminUser jest odpowiednim zamiennikiem dla klasy User i może być używana w ten sam sposób co obiekt klasy User.

Ważne jest, aby zauważyć, że zasada podstawienia Liskov dotyczy czegoś więcej niż tylko dziedziczenia. Chodzi o zapewnienie, że obiekty podklasy zachowują się w ten sam sposób, co obiekty klasy nadrzędnej, niezależnie od sposobu implementacji podklasy. Na przykład, jeśli klasa AdminUser miałaby zachowywać się w jakiś sposób inaczej niż klasa User, naruszyłoby to zasadę podstawienia Liskov, ponieważ nie byłaby ona odpowiednim zamiennikiem dla klasy User.

Zasada segregacji interfejsów

Zasada segregacji interfejsów (ISP) to zasada projektowania oprogramowania, która mówi, że klienci nie powinni być zmuszani do zależności od interfejsów, których nie używają. Oznacza to, że ogólnie dobrym pomysłem jest tworzenie małych, wyspecjalizowanych interfejsów, które dobrze robią jedną rzecz, zamiast tworzyć duże, ogólnego przeznaczenia interfejsy, które próbują robić wiele rzeczy.

Oto przykład, jak można zastosować tę zasadę, przepisując klasy User  oraz UserService  z poprzednich przykładów:

W tym przykładzie UserService interfejs definiuje cztery metody związane z zarządzaniem użytkownikami w bazie danych. Klasa DatabaseUserService  implementuje ten interfejs i dostarcza konkretne implementacje dla tych metod.

Załóżmy, że chcemy dodać nową funkcję do interfejsu UserService , który pozwala nam wyszukiwać użytkowników po adresie e-mail. Jednym ze sposobów byłoby dodanie nowej metody do interfejsu UserService :

Twój kod się nie uruchomi, chyba że zaimplementujesz tę metodę również we wszystkich klasach implementujących interfejs UserService :

Choć to podejście działa, narusza ono zasadę segregacji interfejsów (Interface Segregation Principle), ponieważ zmusza klasę DatabaseUserService  do zaimplementowania metody ( searchUsersByEmail ), której może nie potrzebować ani nie używać.

Lepszym podejściem byłoby stworzenie osobnego interfejsu  dla funkcji wyszukiwania po adresie e-mail:

Teraz mamy osobne, małe i wyspecjalizowane interfejsy, tj. UserService  oraz UserSearchServicektóre mają jedną odpowiedzialność. Klasa wymagająca wszystkich funkcjonalności tych interfejsów może je zaimplementować w sposób pokazany w poniższym fragmencie kodu:

Jest to zgodne z zasadą segregacji interfejsów (Interface Segregation Principle), ponieważ gwarantuje, że klienci (tacy jak DatabaseUserService klasa) nie są zmuszani do zależenia od interfejsów, których nie używają.

To understand this concept better, suppose we have another class called MemoryUserService  która implementuje UserService  interfejs, ale nie potrzebuje funkcji wyszukiwania po adresie e-mail, możemy zapisać kod w ten sposób:

W tym przykładzie klasa MemoryUserService musi jedynie zaimplementować metody zdefiniowane w interfejsie UserService i nie musi martwić się o funkcjonalność wyszukiwania e-mail. Pozwala to klasie MemoryUserService skupić się na jej głównej odpowiedzialności, jaką jest zarządzanie użytkownikami w pamięci, zamiast być zmuszaną do implementowania niepowiązanych funkcjonalności.

Dependency Inversion Principle

Zasada odwrócenia zależności (Dependency Inversion Principle - DIP) to zasada projektowania oprogramowania, która mówi, że moduły wysokiego poziomu nie powinny zależeć od modułów niskiego poziomu, lecz jedne i drugie powinny zależeć od abstrakcji. Oznacza to, że ogólnie dobrą praktyką jest projektowanie oprogramowania w taki sposób, aby komponenty wysokiego poziomu nie były powiązane z konkretnymi implementacjami komponentów niskiego poziomu, lecz zależały od abstrakcji (takich jak interfejsy lub klasy abstrakcyjne), które można zaimplementować na różne sposoby.

Przyjrzyjmy się przykładowi, jak tę zasadę można zastosować do klas User oraz UserService użytych w poprzednich fragmentach kodu:

W tym przykładzie klasa UserService zależy od abstrakcji nazywanej interfejsem UserRepository, zamiast zależeć od konkretnej implementacji repozytorium użytkowników. Pozwala nam to na implementację interfejsu UserRepository na różne sposoby, na przykład za pomocą bazy danych lub pamięci podręcznej (in-memory), bez wpływu na klasę UserService.

Na przykład, oto implementacja interfejsu UserRepository korzystająca z bazy danych:

Oto inna implementacja interfejsu UserRepository korzystająca z pamięci podręcznej (in-memory):

To sprawia, że system jest bardziej elastyczny i łatwiejszy w utrzymaniu, ponieważ pozwala nam na zmianę implementacji repozytorium bez wpływu na resztę systemu. Ułatwia to również testowanie klasy UserService, ponieważ możemy mockować zależność UserRepository w naszych testach.

Podsumowanie

W tym artykule omówiliśmy pięć zasad SOLID  kod i udostępnione fragmenty kodu spełniające każdą z zasad. Przestrzeganie tych zasad może pomóc w projektowaniu systemów oprogramowania, które są bardziej elastyczne, łatwiejsze w utrzymaniu i skalowalne. Należy jednak pamiętać, że zasady te są wytycznymi, a nie sztywnymi regułami, i to do programisty należy decyzja, kiedy i jak je zastosować w kontekście konkretnego projektu. Kontynuuj naukę, odwiedzając nasz blog, aby znaleźć bardziej szczegółowe i aktualne artykuły oraz samouczki na temat chmury obliczeniowej i DevOps, projektowania i tworzenia oprogramowania, trendów technologicznych, na które warto zwrócić uwagę, i nie tylko.

Miłego kodowania!

author

Preslav Dobrev

Autor · CloudSigma

Preslav Dobrev jest projektantem kreatywnym w CloudSigma, skupiającym się na spójnej tożsamości biznesowej przy wykorzystaniu tradycyjnych i innowacyjnych kanałów marketingowych. Biegle łączy wizję artystyczną ze strategicznym marketingiem, tworząc wywierające wpływ narracje marki.

Komentarze

Brak komentarzy. Bądź pierwszy.