Blog
Single Responsibility Principle
Powrót do Artykułów Dodał(a) Bartłomiej Zagórski w dniu 29.12.2021 w kategorii Zasady SOLID
Zasada pojedynczej odpowiedzialności jest to pierwsza z pięciu zasad programowania obiektowego zawarta w skrócie SOLID. Polega ona pokrótce na tym, żeby moduły, klasy bądź metody zajmowały się jednym, konkretnym zadaniem. Zatem nie należy projektować klas w taki sposób, żeby robiły jak najwięcej, a wręcz przeciwnie, idealnym rozwiązaniem jest takie przypisywanie im tylko jednego zadania.
Spis Treści:
- Definicja
- Przykład z życia
- Aplikacja niezgodna z SRP
- Refactoring dla przejrzystości
- Aplikacja zgodna z SRP
- Aplikacja po zmianie przebiegu procesu kalkulacji
- Korzyści płynące ze stosowania SRP
- Podsumowanie
Definicja
Zasada pojedynczej odpowiedzialności stanowi, że każdy moduł, klasa lub metoda w programie komputerowym powinna być odpowiedzialna za jedną część funkcjonalności programu i powinna ją hermetyzować. Wszystkie usługi tegoż modułu, klasy bądź metody powinny być ściśle dopasowane do tej odpowiedzialności. Innymi słowy każdy moduł, klasa lub metoda powinna mieć tylko jeden powód do zmiany.
Przykład z życia
Rozważmy sobie jakiś popularny market budowlany. Można w nim zakupić niemal wszystko co jest potrzebne do budowy, ocieplenia, wykończenia oraz wyposażenia domu. Mamy zarówno materiały budowlane typu bloczki, pustaki, cegły, jak również wełnę mineralną i styropian do ocieplenia. Market oferuje również panele oraz płytki do wykończenia podłóg oraz farby do pomalowania ścian. Można również dobrać armaturę łazienkową oraz asortyment do oświetlenia. Dla ułatwienia zadania zarówno pracownikom jak i klientom market podzielił całe swoje wyposażenie na odpowiednie działy, zajmujące się wąską częścią świadczonych usług. Wyobraźmy sobie jak trudno byłoby poruszać się po takim sklepie, gdyby wszystko było ze sobą wymieszane, a pracownicy nie byliby przydzieleni do specyficznych działów. Byłby to istny koszmar zarówno dla tychże pracowników jak i dla klientów. I to dlatego wszystko jest podzielone na wyspecjalizowane działy, w których pracują ludzie znający się doskonale na wąskiej części sklepowego asortymentu. O tym właśnie traktuje pierwsza zasada programowania obiektowego kryjąca się pod literą S ze skrótu SOLID.
Aplikacja niezgodna z SRP
Żeby pokazać na czym polega projektowanie i programowanie zgodnie z zasadą Single Responsibility Principle pokażę przykład jak napisać prostą apkę niekorzystając z zasad dobrego programowania obiektowego, a konkretnie z zasady SRP a następnie poprzez refactoring poprawię ją w taki sposób, żeby była zgodna z zasadą pojedynczej odpowiedzialności. Zatem do dzieła:
Poniżej przedstawiam kod aplikacji napisanej bez stosowania się do Single Responsibility Principle:
public class Calculator {
public void calculate(int a, int b, MathOperation mathOperation) {
switch (mathOperation) {
case ADDITION:
System.out.println(a+b);
break;
case SUBTRACTION:
System.out.println(a-b);
break;
case MULTIPLICATION:
System.out.println(a*b);
break;
case DIVISION:
if (b!=0) {
System.out.println(a/b);
} else {
System.out.println("You mustn't divide by 0 !!!");
}
break;
}
}
}
Mamy tutaj klasę Calculator, która posiada jedną publiczną metodę calculate, przyjmującą 3 argumenty: dwie liczby oraz enum, określający jak dana metoda ma się zachować. W ciele metody widzimy switcha, który w zależności od rodzaju działania, wykonuje stosowne obliczenia a następnie wyświetla wynik. Jak widać klasa ta ma dwie odpowiedzialności: Wykonuje obliczenia oraz Wyświetla rezultat na ekranie. Ponadto wszystko upchane jest w jednej metodzie, co prowadzi do tego, że kod traci na czytelności. Zerknijmy na schemat UML naszej skromnej aplikacji.

Nie ma tutaj na razie nic interesującego, klasa Calculator ma zależność od enuma, Wykorzystuje go do podjęcia decyzji jakie obliczenie wykonać.
Refactoring dla przejrzystości
Aby poprawić jakość kodu w pierwszym kroku wykonałem tzw. refaktoryzację dla przejrzystości. Polega ona na wydzieleniu z jednej metody publicznej metod prywatnych dla uporządkowania kodu i uczytelnienia go. Zobaczmy jak teraz prezentuje się aplikacja.
public class Calculator {
public void calculate(int a, int b, MathOperation mathOperation) {
String calculationResult = makeCalculations(a, b, mathOperation);
printResult(calculationResult);
}
private void printResult(String result) {
System.out.println(result);
}
private String makeCalculations(int a, int b, MathOperation mathOperation) {
switch (mathOperation) {
case ADDITION:
return add(a, b);
case SUBTRACTION:
return subtract(a, b);
case MULTIPLICATION:
return multiply(a, b);
case DIVISION:
return divide(a, b);
default:
return "";
}
}
private String divide(int a, int b) {
try {
return String.valueOf(a/b);
} catch (ArithmeticException e) {
return "Yoy mustn't divide by 0 !!!";
}
}
private String multiply(int a, int b) {
return String.valueOf(a * b);
}
private String subtract(int a, int b) {
return String.valueOf(a - b);
}
private String add(int a, int b) {
return String.valueOf(a + b);
}
}
I jeszcze schemat UML:

Zarówno liczba linii kodu jak i schemat UML trochę się powiększyły, ale dzięki temu kod nabrał czytelności. Nie zmieniło się jednak nic jeżeli chodzi o odpowiedzialność klasy. Nadal wykonuje ona 2 zadania. Już za chwilę zostanie to poprawione.
Aplikacja zgodna z SRP
W celu doprowadzenia aplikacji do przestrzegania zasady pojedynczej odpowiedzialności należy rozdzielić jej dwa zadania do osobnych klas. Wówczas jedna z nich będzie odpowiedzialna za obliczenia, a druga za wyświetlanie wyników. Nasza dotychczasowa klasa zmieni zaś swój charakter. Od tej pory jej jedyną odpowiedzialnością będzie delegowanie zadań. Klasa Calculator stanie się zarządzającym procesem kalkulacji. Zobaczmy kod:
public class Calculator {
private final CalculationMaker calculationMaker = new CalculationMaker();
private final ResultPrinter resultPrinter = new ResultPrinter();
public void calculate(int a, int b, MathOperation mathOperation) {
String calculationResult = calculationMaker.makeCalculations(a, b, mathOperation);
resultPrinter.printResult(calculationResult);
}
}
A gdzie się podziała implementacja?! Otóż jest przeniesiona do dwóch nowych klas: CalculationMaker oraz ResultPrinter:
public class CalculationMaker {
public String makeCalculations(int a, int b, MathOperation mathOperation) {
switch (mathOperation) {
case ADDITION:
return add(a, b);
case SUBTRACTION:
return subtract(a, b);
case MULTIPLICATION:
return multiply(a, b);
case DIVISION:
return divide(a, b);
default:
return "";
}
}
private String divide(int a, int b) {
try {
return String.valueOf(a/b);
} catch (ArithmeticException e) {
return "Yoy mustn't divide by 0 !!!";
}
}
private String multiply(int a, int b) {
return String.valueOf(a * b);
}
private String subtract(int a, int b) {
return String.valueOf(a - b);
}
private String add(int a, int b) {
return String.valueOf(a + b);
}
}
public class ResultPrinter {
public void printResult(String result) {
System.out.println(result);
}
}
A poniżej schemat UML:

Aktualnie klasa Calculator odpowiada za nadzór nad procesem kalkulacji, która składa się z dwóch części: wykonania obliczeń oraz prezentacji ich na ekranie. Wykonaniem obliczeń zajmuje się jadnak klasa CalculationMaker a prezentacją ResultPrinter. Calculator nie ma pojęcia jak implementowane są metody makeCalculations i printResult, gdyż są one enkapsulowane przez odpowiednie klasy. Powodem do zmiany klasy CalculationMaker jest zmiana sposobu obliczania. Klasę ResultPrinter zmodyfikujemy gdy zmieni się sposób wyświetlania. Natomiast klasa Calculator zmieni się tylko wtedy, gdy zmieni się przebieg procesu kalkulacji. Zobaczmy taki scenariusz w następnej sekcji.
Aplikacja po zmianie przebiegu procesu kalkulacji
Załóżmy, że oprócz obliczenia i wyświetlenia wyniku chcemy odegrać też jakąś melodyjkę. Zobaczmy jak łatwo będzie to teraz przeprowadzić. Wystarczy stworzyć nową klasę i dodać kolejne pole do klasy Calculator:
public class Calculator {
private final CalculationMaker calculationMaker = new CalculationMaker();
private final ResultPrinter resultPrinter = new ResultPrinter();
private final SoundMaker soundMaker = new SoundMaker();
public void calculate(int a, int b, MathOperation mathOperation) {
String calculationResult = calculationMaker.makeCalculations(a, b, mathOperation);
resultPrinter.printResult(calculationResult);
soundMaker.makeSound();
}
}
public class SoundMaker {
public void makeSound() {
System.out.println("imagine amazing sound here\n");
}
}

Jak widać klasa Calculator nadal odpowiada tylko za przebieg procesu kalkulacji. W tym celu oddelegowuje poszczególne zadania do innych klas, które również mają już tylko pojedynczą odpowiedzialność.
Korzyści płynące ze stosowania SRP
Najważniejszą zaletą stosowania zasady pojedynczej odpowiedzialności jest porządek. Każda klasa jest wyspecjalizowana i odpowiada za wąski wycinek oprogramowania. Widać wyraźnie podobieństwo do prawdziwego życia, gdzie również na każdym kroku ludzie próbują się specjalizować i utrzymywać porządek, ponieważ wtedy praca jest bardziej efektywna.
Ponadto stosowanie SRP zapobiega powstawaniu gigantycznych klas, które bardzo trudno utrzymać. Ciągły rozwój oprogramowania, dodawanie nowych funkcjonalności bez stosowania SRP spowoduje obecność w kodzie klas liczących kilkaset a nawet tysiące linii kodu. W takiej sytuacji bardzo trudno jest coś zmienić lub poprawić, ponieważ nigdy nie ma pewności, że zmiana w 137 linii nie spowoduje nieoczekiwanych efektów linii 1534. W ogromnych komercyjnych projektach taka sytuacja byłaby niedopuszczalna.
Wreszcie możemy również ułatwić zadanie klientom, którzy nie chcą wykorzystywać całego potencjału takiej przerośniętej klasy. W naszym przykładzie za wyświetlanie odpowiada klasa ResultPrinter. Jeżeli klient chciałby jedynie wyświetlać jakiś tekst nie będzie zmuszony do użycia jednej z funkcji klasy Calculator a zwyczajnie deleguje zadanie do obiektu klasy ResultPrinter.
Podsumowanie
Single Responsibility Principle jest pierwszą z 5 zasad programowania obiektowego zaszytych w skrócie SOLID. Mówi ona o tym, że klasa powinna zajmować się jedną funkcjonalnością i enkapsulować ją. Projektowanie i programowanie aplikacji w zgodzie z tą zasadą niesie ze sobą wiele korzyści. Mamy porządek, klasy nie są przerośnięte, a zatem nie jest trudno je utrzymać lub modyfikować, a także nie musimy tworzyć dużych obiektów tylko po to by skorzystać z niewielkiego procentu ich możliwości. Nie należy jednak przesadzać z trzymaniem się zasady w 100%. Generalnie zawsze powinna być możliwość odpowiedniego określenia tylko jednej odpowiedzialności klasy i oddelegowania wszystkich składowych do mniejszych klas, ale czasami może to wymagać uzależnienia obiektu od kilkunastu lub kilkudziesięciu mniejszych obiektów, co również może być problemem. Należy stosować ją z umiarem i rozsądkiem i znaleźć złoty środek.