SOLID: Single Responsibility
Jeżeli jesteś już chwilę w branży, to na pewno słyszałeś o SOLID. Jest to słynny mnemonik opisujący pięć zasad projektowania oprogramowania w paradygmacie obiektowym. W dzisiejszym wpisie przyjrzymy się pierwszej z nich, czyli zasadzie pojedynczej odpowiedzialności (Single Responsibility)
SOLID
Na początek warto powiedzieć sobie coś więcej o samym skrócie SOLID. Tak jak wspomnieliśmy we wstępie, jest to mnemonik pięciu zasad. Jego ojcem jest słynny Uncle Bob, czyli Robert C. Martin.
Co to za reguły?
- S -> Single responsibility principle (Zasada pojedynczej odpowiedzialności)
- O -> Open/closed principle (Zasada otwarte-zamknięte)
- L -> Liskov substitution principle (Zasada podstawienia Liskov)
- I -> Interface segregation principle (Zasada segregacji interfejsów)
- D -> Dependency inversion principle (Zasada odwracania zależności)
Po co wprowadzono te założenia?
Miały one na celu sprawić, że projekty, w których będą one przestrzegane, staną się elastyczne, otwarte na zmiany oraz łatwiejsze w utrzymaniu.
A po co ta seria?
Z dwóch powodów:
-
Pracując już chwilę w branży, zauważyliśmy, że często pojawia się problem z zastosowaniem powyższych zasad. Momentami są one wprowadzane na siłę, bez większego zrozumienia. Nie zawsze wszystkie da się zastosować w projekcie. Zdarza się, że zasady trzeba naginać, tak jak zawsze powtarzamy zdrowy rozsądek przede wszystkim.
-
A drugi powód to popularność tego tematu na rozmowach kwalifikacyjnych. Pojawia się on praktycznie na każdej rozmowie.
Pojedyncza odpowiedzialność
“A class should have one, and only one, reason to change.”
Według powyższej, oryginalnej definicji Wujka Boba klasa powinna mieć tylko jeden powód do zmiany. Częściej jednak mówiąc o tej zasadzie mamy na myśli (co jest praktycznie równoznaczne), że klasa powinna mieć tylko jedną odpowiedzialność.
Co to znaczy?
Posłużmy się przykładem i ponownie zajrzymy do naszej fabryki samochodów, która pojawiała się we wpisach dotyczących wzorców projektowych. Wyobraźmy sobie sytuację, że oprócz samochodów, firma rozszerzyła również profil działalności o produkcję statków morskich. Wszystkie części były produkowane w ramach oszczędności na tych samych liniach produkcyjnych, z użyciem tych samych materiałów. Okazało się jednak, że statki wymagają zastosowania innych metali, które będą mniej podatne na korozje oraz zapewnią lepszą wypornosć. W tym momencie firma musiała zdecydować się na budowę osobnej fabryki, która będzie miała osobne procesy i linie dedykowane tylko statkom.
Jak widać w powyższym uproszczonym przykładzie, czasem jak coś jest do wszystkiego to jest do niczego :). Podobnie jest z klasami. Wyobraźmy sobie, że mamy klasę, która oprócz procesów biznesowych, zajmuje się również zarządzaniem użytkownikami i konfiguracją bazy danych. Współczujemy temu, który będzie musiał tam coś zmienić.
Klasa powinna mieć tylko jedną odpowiedzialność, a jej logika odpowiednio ukryta (hermetyzacja) i dostępna jedynie za pośrednictwem interfejsu (API) klasy.
Implementacja
Masz wątpliwości, czy Twój kod jest zgodny z zasadą single responsibility?
Pokażemy Ci na przykładzie, jak możesz spróbować go zrefaktoryzować, przenosząc nadmiarowe odpowiedzialności do osobnych jednostek.
Przyjrzyj się poniższym fragmentom kodu:
Uproszczony model Car
class Car { model: string; color: string; constructor(model: string) { this.model = model; this.color = color; } }
Uproszczony model klienta, który dokonuje zakupów w naszej fabryce:
class Customer { name: string; constructor(name: string) { this.name = name; } buyCar(car: Car) { // logika kupna samochodu console.log("Klient kupił samochód, gratulacje!"); } }
Klasa wytwarzająca samochody:
class CarFactory { createCar(make: string, model: string): Car { // logika tworzenia nowego samochodu return new Car(make, model); } sellCar(car: Car, customer: Customer): void { // logika sprzedaży samochodu customer.buyCar(car); } }
Jak widać, oprócz wytwarzania nowych samochodów CarFactory zajmuje się również ich sprzedażą, więc gdyby coś w logice sprzedażowej wymagało zmiany np. obniżka cen, wyprzedaż sezonowa etc. musielibyśmy te zmiany wprowadzać również w tej samej fabryce. Zdecydowanie tego nie chcemy!
Spójrzmy teraz na kod po małym refactoringu:
class CarFactory { createCar(make: string, model: string): Car { // logika tworzenia nowego samochodu return new Car(make, model); } } class SellService { sellCar(car: Car, customer: Customer): void { // logika sprzedaży samochodu customer.buyCar(car); } }
Wprowadziliśmy nową klasę SellService, która zamyka w sobie logikę sprzedaży, dzięki czemu nie wycieka ona na pozostałe klasy w projekcie. CarFactory może w spokoju zając się tym, co potrafi najlepiej, czyli produkcja nowych instancji klasy Car. Zredukuje to złożoność systemu oraz jego podatność na błędy.
Podsumowanie
Jak widać sama reguła, jest bardzo prosta, a jednocześnie jej wdrożenie daje znaczące efekty. Pomaga unikać zbędnych zależności między różnymi funkcjonalnościami oraz ułatwia debugowanie i refaktoryzację kodu. Warto pamiętać, że tak naprawdę ta zasada nie powinna być zastosowana tylko w odniesieniu do klas, również tworząc moduły, metody powinieneś mieć ją z tyłu głowy. Oczywiście, wszystko z głową, wprowadzaj ją tylko wtedy gdy widzisz, że przyniesie korzyści.