Zasady SOLID #1 – Single Responsibility Principle

pocket knife
„Jak coś jest do wszystkiego, to jest do niczego” – chyba każdy zna to popularne powiedzenie. I myślę, że większość z nas przekłada tę myśl na życie codzienne – lubimy korzystać z przedmiotów, które mają tylko jedno zastosowanie, ale wykonują dobrze to, czego od nich oczekujemy. Nie inaczej jest z programowaniem, gdyż można powiedzieć, iż „klasa, która jest do wszystkiego, jest do niczego”.

Wprowadzenie

Pierwsza z zasad SOLID to Zasada Pojedynczej Odpowiedzialności (ang. Single Reponsibility Principle). Zgodnie z jej treścią, jedna klasa powinna być odpowiedzialna za tylko jedną rzecz. Innymi słowy, nigdy nie powinien istnieć więcej niż jeden powód do zmiany istniejącej klasy.
Każda klasa, zgodnie z tą zasadą, nie powinna być odpowiedzialna za wykonywanie więcej niż jednej czynności. Jeżeli w naszym kodzie zlokalizujemy taką klasę, powinniśmy rozbić ją na kilka pomniejszych klas.
Zasada ta dotyczy nie tylko klas. Jej założenia powinno się także stosować w stosunku do interfejsów, metod itp.

Objaśnienie zasady

W tym podrozdziale mam zamiar do każdej z zasad SOLID przytaczać przykłady poszczególnych zasad w odniesieniu do życia codziennego, nie związanego z programowaniem. Jednak w przypadku Zasady Pojedynczej Odpowiedzialności jest to chyba zbędne, gdyż każdy z nas pewnie wie o co chodzi i na czym ona polega.
Niech za przykład złamania zasady SRP przysłuży scyzoryk. Jest to niewielki nóż kieszonkowy wyposażony w wiele dodatkowych narzędzi, jak np. nożyczki, piła, otwieracz do butelek, korkociąg, pilnik do paznokci i wiele innych. Można by rzec, urządzenie do wszystkiego. Jednak w praktyce jest to bardzo nieużyteczny przedmiot. Kto z nas, mając potrzebę użycia noża albo piły, wyciągnie niewielki scyzoryk o kilkucentymetrowych narzędziach? Podobnie ma się sprawa np. z otwieraczem do butelek albo korkociągiem. Te narzędzia w scyzoryku są na tyle małe, że ich używanie jest bardzo niewygodne.
Zdecydowanie lepszym rozwiązaniem jest skorzystać z narzędzi dedykowanych do konkretnej pracy. Takie przedmioty wprawdzie wykonują tylko jedną rzecz, ale wykonują to dobrze i właśnie tego od nich oczekujemy.

Przykład

Naszym zadaniem będzie napisanie oprogramowania do drukarki wielofunkcyjnej. Urządzenie to będzie miało możliwość skanowania i drukowania dokumentów oraz wysyłania skanów poprzez e-mail.

Kod niezgodny z SRP

Kod takiej drukarki mógłby wyglądać następująco:
public class Printer {

    public void print(Document document) {
        System.out.println("Printing document");
    }

    public Document scan() {
        System.out.println("Scanning document");
        return new Document();
    }

    public void send(Document document) {
        System.out.println("Sending document to e-mail");
    }
} 
Główną klasą naszego oprogramowania jest Printer. Mamy tutaj metody print(), scan() i send() odpowiedzialne za drukowanie wybranego dokumentu, skanowanie do pliku oraz wysyłanie wybranego dokumentu.
Obiekty klasy Document są cyfrową reprezentacją zeskanowanych dokumentów.
Problem tym kodem jest taki, iż klasa Printer jest odpowiedzialna jednocześnie za skanowanie, drukowanie i wysyłanie. Powoduje to, że klasa ta ma aż trzy powody do zmiany. Zdecydowanie nie jest to klasa napisana zgodnie z Zasadą Pojedynczej Odpowiedzialności.
Klasa Printer póki co nie jest jest zbyt rozbudowana. Gdyby jednak miała być rozszerzana niezgodnie z SRP, to trudno byłoby się połapać jakie jest jej przeznaczenie. Po dodaniu kilku dodatkowych metod klasa Printer mogłaby wyglądać tak:
Printer class UML diagram
Dodanie nowych metod powoduje, że w klasie Printer zaczyna się pojawiać coraz większy bałagan. Dodatkowo, w tym przykładzie wszystkie metody zostały uszeregowane wg kolejności ich dodania, co dodatkowo utrudnia odbiór takiej klasy.

Kod zgodny z SRP

Po refaktoryzacji zgodne z Zasadą Pojedynczej Odpowiedzialności, kod będzie wyglądał następująco:
public class Printer {

    public void print(Document document) {
        System.out.println("Printing document");
    }
}

public class Scanner {

    public Document scan() {
        System.out.println("Scanning document");
        return new Document();
    }
}

public class Sender {

    public void send(Document document) {
        System.out.println("Sending document to e-mail");
    }
} 
Jak widać, rozdzieliliśmy odpowiedzialności na trzy osobne klasy, Printer, Scanner i Sender. Od teraz każda z nich jest odpowiedzialna za jedną rzecz. Kod stał się czytelniejszy.
Stosując Zasadę Pojedynczej Odpowiedzialności, wszystkie nowe funkcjonalności będziemy dodawać do osobnych klas. Natomiast jeśli będziemy chcieli rozszerzyć funkcjonalność istniejącej klasy, zrealizujemy to poprzez dodanie do niej nowych pól i metod.
Stosując Zasadę Pojedynczej Odpowiedzialności, wszystkie nowe funkcjonalności będziemy dodawać do osobnych klas. Natomiast jeśli będziemy chcieli rozszerzyć funkcjonalność istniejącej klasy, zrealizujemy to poprzez dodanie do niej nowych pól i metod. Takie podejście spowoduje, że nasze oprogramowanie może po czasie rozrosnąć się do pokaźnych rozmiarów, ale będzie wciąż jasne i czytelne.

Podsumowanie

Zasada Pojedynczej Odpowiedzialności jest prosta do zrozumienia. Jednak okazuje się, że jest to jedna z najczęściej łamanych zasad SOLID. W trakcie pracy nad swoimi projektami warto poświęcić nieco czasu na przeanalizowanie napisanego kodu i wydzielenie do osobnych klas tego, co powiększa nam odpowiedzialność naszych klas. Spowoduje to, że będziemy mieli w projektach większą ilość mniejszych klas, ale nasz kod stanie się dzięki temu o wiele bardziej czytelny i zrozumiały.

Znajdź mnie również na:

arrow