1. Definicja
Single Responsibility Principle mówi, że klasa powinna mieć tylko jeden powód do zmiany.
A class should have only one reason to change. — Robert C. Martin
Najprościej rzecz ujmując: jedna klasa = jedna odpowiedzialność.
Oznacza to, że klasa może wykonywać wiele operacji, ale wszystkie powinny dotyczyć jednego sprecyzowanego obszaru odpowiedzialności.
2. Przykład z życia: zobaczmy jak to zrozumieć?
Wyobraźmy sobie pracownika w firmie, który zajmuje się:
- odbieraniem telefonów,
- prowadzeniem księgowości,
- naprawą komputerów,
- rekrutacją nowych pracowników.
Na pierwszy rzut oka może się wydawać, że to oszczędność - jedna osoba zajmuje się wszystkim. Jendak problem pojawia się wtedy, gdy zachodzą zmiany w którymkolwiek z tych obszarów.
Zmiana w księgowości
Firma wprowadza nowe przepisy podatkowe lub zmienia sposób wystawiania faktur. Pracownik musi nauczyć się nowych procedur i zmodyfikować sposób wykonywania obowiązków związanych z księgowością.
Zmiana w rekrutacji
Dział HR postanawia dodać nowy etap rozmów kwalifikacyjnych lub zmienić formularze dla kandydatów. Ponownie ten sam pracownik musi dostosować swoją pracę, mimo że zmiana nie ma nic wspólnego z księgowością itd.
W efekcie jedna osoba staje się zależna od zmian zachodzących w wielu zupełnie różnych obszarach firmy. Każda nowa procedura, regulacja lub wymaganie powoduje konieczność modyfikacji jej pracy.
Dokładnie ten sam problem występuje w programowaniu.
Jeżeli jedna klasa:
- zapisuje dane do bazy,
- wysyła wiadomości e-mail,
- generuje raporty,
- drukuje dokumenty,
to każda zmiana w jednym z tych obszarów wymaga modyfikacji tej samej klasy.
W rezultacie:
- kod staje się coraz większy,
- rośnie ryzyko wprowadzenia błędów,
- trudniej testować zmiany,
- trudniej zrozumieć odpowiedzialność klasy.
Zgodnie z zasadą SRP lepiej zatrudnić czterech specjalistów:
księgowego, rekrutera, informatyka, pracownika obsługi klienta.
W świecie programowania odpowiednikami tych specjalistów są osobne klasy, z których każda odpowiada za jeden konkretny obszar. Dzięki temu zmiana w księgowości nie wpływa na pracę informatyka, a zmiana sposobu rekrutacji nie wymaga modyfikacji procesów obsługi klienta.
To właśnie oznacza „jeden powód do zmiany”.
3. Przykład - naruszenie SRP
Załóżmy, że tworzymy system sklepu internetowego.
Klasa OrderService poniżej narusza SRP, ponieważ ma trzy powody do zmiany:
- zapisuje zamówienie,
- wysyła e-mail,
- drukuje fakturę.
class OrderService {
public void saveOrder(Order order) {
// zapis do bazy danych
}
public void sendConfirmationEmail(Order order) {
// wysłanie maila do klienta
}
public void printInvoice(Order order) {
// drukowanie faktury
}
}
Problem stanowią trzy powody do zmiany klasy:
- jeżeli zmieni się sposób zapisu danych (przejdziemy z MySQL na PostgreSQL),
public void saveOrder(Order order) {
// zapis do bazy danych
}
- jeżeli zmieni się sposób wysyłania maili,
public void sendConfirmationEmail(Order order) {
// wysłanie maila do klienta
}
- jeżeli zmieni się wygląd faktury (wymóg nowych danych na fakturze),
public void printInvoice(Order order) {
// drukowanie faktury
}
Skąd pochodzi zmiana? Koncept aktora
Warto zadać sobie pytanie: kto wymusza zmianę w danej metodzie klasy OrderService?
- Zmianę w
saveOrder()wymusi zespół infrastruktury - np. decyzja o migracji bazy danych z MySQL na PostgreSQL. - Zmianę w
sendConfirmationEmail()wymusi dział marketingu - np. nowy szablon wiadomości powitalnej. - Zmianę w
printInvoice()wymusi dział księgowości - np. wymóg dodania nowego pola na fakturze.
Każdy z tych podmiotów to w terminologii Roberta C. Martina aktor (actor) - konkretna osoba lub zespół, który odpowiada za dany obszar biznesowy i jako jedyny powinien mieć powód, by żądać zmiany w klasie.
Moduł powinien być odpowiedzialny wobec jednego i tylko jednego aktora. — Robert C. Martin, Clean Architecture
Jeżeli trzy różne osoby z trzech różnych działów mogą niezależnie zlecić modyfikację tej samej klasy - to sygnał, że klasa narusza SRP.
Właśnie dlatego definicja mówi o powodzie do zmiany, a nie o liczbie metod. Klasa może mieć wiele metod i nadal respektować SRP
- pod warunkiem, że wszystkie zmiany w niej wynikają z decyzji tego samego aktora.
4. Przykład - zgodny z SRP
Rozdzielamy odpowiedzialności na osobne klasy:
class OrderRepository {
public void save(Order order) {
// zapis do bazy danych
}
}
class EmailService {
public void sendConfirmation(Order order) {
// wysłanie maila do klienta
}
}
class InvoicePrinter {
public void print(Order order) {
// drukowanie faktury
}
}
Co zyskujemy?
Każda klasa odpowiada za jeden obszar:
- Klasa
OrderRepositoryodpowiada za zapis danych, - Klasa
EmailServiceodpowiada za wysyłanie wiadomości, - Klasa
InvoicePrinterodpowiada za generowanie faktur.
Jeżeli zmieni się sposób wysyłki maili - modyfikujemy tylko klasę EmailService.
Jeżeli zmieni się format faktur - modyfikujemy tylko klasę InvoicePrinter.
5. Dlaczego SRP jest ważne?
Kod jest łatwiejszy do zrozumienia - zamiast jednej klasy liczącej 1000 linii kodu i 20 różnych zadań, tworzymy klasy, których nazwy wskazują, za co odpowiadają.
Łatwiej wprowadzić zmiany - zmiana w jednym miejscu nie wywołuje lawiny zmian w innych częściach systemu.
Łatwiej pisać testy - testowanie klasy z jedną odpowiedzialnością jest łatwiejsze niż testowanie klasy, która realizuje wiele różnych funkcji
Mniejsze ryzyko błędów - modyfikując kod odpowiedzialny za wysyłanie wiadomości, nie ryzykujemy przypadkowego uszkodzenia logiki związanej z fakturami.
6. Kiedy stosować?
- Gdy klasa ma więcej niż jeden powód do zmiany.
- Gdy modyfikacja jednej funkcji wymusza zmiany w innych miejscach.
- Gdy testowanie klasy jest trudne, bo robi za dużo.
- Gdy klasa ma wiele niepowiązanych metod.
- Gdy trudno jednym zdaniem opisać odpowiedzialność klasy.
Dobra klasa robi jedną rzecz dobrze. Dzięki temu kod jest prostszy, czytelniejszy i łatwiejszy do rozwijania.