SOLID - Single Responsibility Principle (SRP) - Wszystko Co Powinieneś Wiedzieć o Zasadzie Pojedynczej Odpowiedzialności
W polskim tłumaczeniu jest to zasada pojedynczej odpowiedzialności. Mówi ona o tym, że każda klasa powinna mieć tylko jedną odpowiedzialność. To znaczy, że jeżeli klasa ma więcej niż jedną odpowiedzialność, to wtedy istnieje więcej niż jeden powód do tego, aby ją w przyszłości zmienić. A nigdy nie powinien istnieć więcej niż jeden powód do modyfikacji klasy. Reguła SRP zwraca przede wszystkim uwagę na to, żeby oddzielać od siebie fragmenty kodu, które są zależne od różnych "aktorów". Jeżeli klasa jest odpowiedzialna za więcej niż jeden obszar w naszym projekcie, to często może być to problemem, ponieważ w przyszłości robiąc zmiany w jednym obszarze, może zepsuć coś w innym, który wydawać się może nie jest z nim powiązany. Reguła pojedynczej odpowiedzialności dotyczy nie tylko klas czy interfejsów (bo na nich powinniśmy się skupić), ale również modułów i metod. Klasy i metody powinny się skupiać na jednej konkretnej rzeczy i koniecznie robić tę rzecz dobrze. Klasa powinna mieć jedną funkcjonalność, którą realizuje, czyli jedną właśnie wspomnianą odpowiedzialność. Najlepiej będzie to przedstawić na przykładzie.
Kod niestosujący się do zasady pojedynczej odpowiedzialności:
public interface IEmailSender
{
void Send();
void LogError(Exception exception);
void AddToStats();
}
public class EmailSender : IEmailSender
{
public void Send()
{
throw new NotImplementedException();
}
public void AddToStats()
{
throw new NotImplementedException();
}
public void LogError(Exception exception)
{
throw new NotImplementedException();
}
}
Dla prostoty przykładu metody nie są zaimplementowane, nie dodawałem też żadnych właściwości. Aby przedstawić to, co w tym artykule jest najważniejsze, czyli regułę pojedynczej odpowiedzialności, implementacja tych metod nie jest nam potrzebna. Na powyższym przykładzie można zauważyć, że klasa EmailSender ma więcej niż jedną odpowiedzialność. To znaczy, że istnieje więcej niż jeden powód, by w przyszłości zmienić tę klasę. Po pierwsze ów klasa odpowiada za wysyłanie maili, oprócz tego odpowiada jeszcze za dodawanie do statystyk, a także logowania informacji o błędzie. Powodów do zmiany jest stanowczo za dużo. Jeżeli klasa wysyła maila, to powinna robić tylko to, na pewno nie powinna oprócz tego jeszcze logować błędu i prowadzić statystyk. Czyli ten przykład ewidentnie łamie zasadę pojedynczej odpowiedzialności. No dobrze, ale jak zatem powinien wyglądać ten kod, stosując zasadę pojedynczej odpowiedzialności?
Przede wszystkim trzeba podzielić każdą odpowiedzialność do osobnej klasy. Czyli w tym przypadku, trzeba utworzyć 3 klasy, będą to:
-klasa do wysyłania maili,
-klasa do logowania błędów,
-klasa to prowadzenia statystyk.
Kod #1 stosujący zasadę pojedynczej odpowiedzialności:
public interface IEmailSender
{
void Send();
}
public interface ILogger
{
void LogError(Exception exception);
}
public interface IStatistics
{
void Add(IEmailSender emailSender);
}
public class EmailSender : IEmailSender
{
public void Send()
{
throw new NotImplementedException();
}
}
public class Logger : ILogger
{
public void LogError(Exception exception)
{
throw new NotImplementedException();
}
}
public class Statistics : IStatistics
{
public void Add(IEmailSender emailSender)
{
throw new NotImplementedException();
}
}
public class Program
{
private IEmailSender _emailSender;
private ILogger _logger;
private IStatistics _statistics;
public Program(IEmailSender emailSender, ILogger logger, IStatistics statistics)
{
_emailSender = emailSender;
_logger = logger;
_statistics = statistics;
}
public void SendEmail()
{
try
{
_emailSender.Send();
_statistics.Add(_emailSender);
}
catch (Exception exceptpion)
{
_logger.LogError(exceptpion);
throw;
}
}
}
Jak widzisz, obecny kod został podzielony na więcej klas i każda z nich ma tylko jedną odpowiedzialność, to znaczy jeden powód do zmiany. Klasy są mniejsze, przez co łatwiejsze w czytaniu i ewentualne przyszłe zmianą będą dużo prostsze. Każda klasa ma konkretną funkcję, którą spełnia. Klasa do wysyłania maili - tylko wysyła maile. Klasa do logowania - loguje błędy, a klasa do prowadzenia statystyk - dodaje jakieś szczegóły do statystyk.
Nie chcesz, żeby Twoje klasy były jak scyzoryk, to znaczy miały bardzo dużo różnych funkcji. Chcesz, żeby Twoja klasa była jak nóż czy nożyczki i odpowiadała za tą jedną konkretną funkcję. Zazwyczaj jest tak, że jeżeli coś jest do wszystkiego, to tak naprawdę jest do niczego.
Czy zatem klasy powinny mieć tylko jedną metodę?
Akurat w powyższym przykładzie klasy mają tylko jedną metodę, ale niekoniecznie zawsze tak musi być. Chodzi o to, żeby metody te były konkretne dla danego obiektu. Nie chodzi o to, żeby klasy miały jedno pole czy metodę, ale o to, żeby były odpowiedzialne za jedną konkretną rzecz, za jedną czynność. Zademonstruje Ci to jeszcze na jednym przykładzie.
Kod #2 stosujący zasadę pojedynczej odpowiedzialności:
public interface IEmployee
{
void SetFirstName(string firstName);
void SetLastName(string lastName);
decimal GetSalary();
}
Na powyższym przykładzie interfejs IEmployee może mieć metody SetFirstName, SetLastName czy GetSalary, a nie powinien mieć na przykład metody Save. Bo jeśli na przykład zmieni się sposób zapisu danych pracownika, to będzie to kolejny powód do zmiany.
Czyli wtedy klasa implementująca ten interfejs miałaby dwa powody do zmian:
-Zmiana sposobu zapisu danych,
-Dodanie nowej właściwości (na przykład adresu e-mail) do pracownika.
Należy wydzielić metodę Save do osobnej klasy, która będzie miała właśnie metodę SaveEmployee. Wtedy każda z tych naszych nowo powstałych klas, będzie miała swoje pojedyncze odpowiedzialności. Musisz pamiętać, że jeżeli dwa obszary są ze sobą połączone, zmiany jednego z nich mogą zniszczyć funkcjonowanie również tego drugiego. Gdy wymagana jest zmiana, to powinna ona objąć tylko jedną klasę. To samo dotyczy metod, bo jeżeli na przykład jedna metoda wykonuje obliczenia i wyświetla wynik obliczeń, to nie spełnia również zasady pojedynczej odpowiedzialności, ponieważ ma więcej niż jeden powód do zmiany. Może się zmienić sposób obliczeń, jak również sposób wyświetlania wyniku.
Zalety zasady pojedynczej odpowiedzialności:
Stosując zasadę pojedynczej odpowiedzialności, zapewniamy lepszą czytelność kodu, a także minimalizujemy ryzyko wystąpienia problemów w przyszłości przy modyfikacji takiej klasy. To znaczy, że nasz kod jest łatwiejszy w utrzymywaniu i ciągłym rozwoju. Chcąc wprowadzić jedną zmianę, nie musimy robić zmian w kilku klasach, wystarczy tylko w jednej.
PODSUMOWANIE
W dzisiejszym artykule starałem Ci się wytłumaczyć, jak należy rozumieć pierwszą zasadę SOLID, a mianowicie S jak Single Responsibility Priniciple. Mam nadzieję, że dzięki przedstawionym przykładom ta wiedza przyswoi Ci się jeszcze lepiej. Zasada SRP wydaje się na pierwszy rzut oka dosyć prosta, ale w rzeczywistości jest trudna do zaimplementowania, często programiści mają z nią problemy. Myślę, że wraz z doświadczeniem będziesz tę zasadę rozumiał coraz lepiej. A dzięki zasadzie pojedynczej odpowiedzialności Twój kod będzie prostszy do modyfikacji i utrzymania. W kolejnych artykułach będę opisywał pozostałe reguły SOLID.
Poprzedni artykuł - 10-Minutowy Przewodnik Po C# i .NET.
Następny artykuł - SOLID - Open-Closed Principle (OCP) - Wszystko Co Powinieneś Wiedzieć o Zasadzie Otwarte-Zamknięte.