SOLIDny tydzień – S jak Single Responsibility
Pierwszą literą ze zbioru SOLID jaką omówimy będzie S czyli „Single responsibility”. Bo jak coś jest do wszystkiego to jest do niczego.
Tak ogólnie mówiąc
Zasada pojedynczej odpowiedzialności wydaje się prosta jednak z doświadczenia wiem, że bardzo wiele osób ma z nią problem. Jest tak prawdopodobnie dlatego, że jest to wg mnie najbardziej abstrakcyjna z tych 5 zasad. Bo co to jest jedna odpowiedzialność? Dla każdego taki zwrot może oznaczać coś innego.
Zasada pojedynczej odpowiedzialności mówi, że klasa powinna mieć tylko jeden powód do zmiany. I taka definicja nadal niewiele wyjaśnia. Spotkałem się nawet z pytaniami czy skoro ma być jeden powód do zmiany to znaczy, że klasa powinna mieć tylko jedno pole albo jedną metodę.
Czyli, że co?
Kluczowe w definicji jest słowo „odpowiedzialność”. A jest to coś zupełnie innego niż wartość czy funkcja. Ta odpowiedzialność klasy odnosi się do wyższego poziomu.
Wyobraź sobie, że jesteś w fabryce samochodów, która wyposażona jest w roboty przemysłowe. Każdy taki robot to Twoja klasa. I każdy z nich jest odpowiedzialny za jedną czynność. Jeden łączy konkretne elementy, drugi je przenosi, inny zaś maluje itd. Podłoga utrzymuje je w zadanym miejscu, a hala służy za kontener dla robotów. Ale czy to oznacza, że każdy robot ma tylko jedną właściwość, np. wagę? Albo wykonuje tylko jeden ruch? No nie jest tak. A mimo to mają one pojedyncze odpowiedzialności. I jedynym powodem modyfikacji każdego z nich jest zmiana w tej jednej czynności za jaką są odpowiedzialne.
Innym przykładem niech będą stanowiska w firmie. Każdy ma w pracy jakąś odpowiedzialność. Jeden jest programistą, inny testerem, znowu ktoś inny managerem lub księgową. Każdy z nas ma swoją odpowiedzialność. Programista odpowiada za kod, tester za jakość produktu itd. Czy dobrze by Ci się pracowało w firmie gdzie każdy pracownik jest też jednocześnie księgowym, a manager programuje i testuje aplikację? Nie sądzę. I tak samo jest z odpowiedzialnością klas – lepiej się z nimi pracuje kiedy są odpowiedzialne za jedną konkretną czynność.
Podziel to
Nie chodzi o to żeby klasy miały jedno pole czy funkcję. Chodzi o to, żeby wykonywały jedną „czynność”. Jeśli miałbyś klasę służącą do łączenia innych elementów to czy powinna też zajmować się przechowywaniem ich na liście? Nie. Bo są to dwa oddzielne procesy. Listy nie obchodzi czy elementy są łączone w inny sposób nagle. To, że coś się zmieniło w procesie łączenia nie ma żadnego powiązania ze sposobem przechowywania gotowych elementów na liście. A gdybyś zrobił klasę odpowiedzialną za obie czynności to musiałbyś tą klasę modyfikować przy zmianie obu tych procesów. W dodatku ciężko by Ci było używać jej tylko w jednym z kontekstów.
Najczęściej podawanym i zadziwiająco często pojawiającym się w kodzie początkujących programistów przykładem jest wyświetlanie wartości. Bo kto nie spotkał się z podobnym kodem?
class User { private string _firstName; private string _LastName; public User(string firstName, string lastName) { _firstName = firstName; _lastName = lastName; } public string GetFullName() { return string.Format("{0} {1}. _firstName, _lastName); } public void PrintUser() { Console.WriteLine(GetFullName()); } }
Mam nadzieję, że od razu widzisz gdzie jest problem. Klasa User ma w tym wypadku dwie odpowiedzialności – zarządza danymi User i wyświetla Usera na ekranie. I teraz niezależnie od tego czy zmieni się sposób albo miejsce wyświetlania danych użytkownika czy też zmieni się ilość danych albo sposób ich modyfikacji to klasa User będzie musiała być zmodyfikowana. A tak nie powinno być.
W powyższym przypadku powinieneś posiadać oddzielne klasy dla tych dwóch zadań. Np. takie jak pokazane poniżej:
class User { private string _firstName; private string _LastName; public User(string firstName, string lastName) { _firstName = firstName; _lastName = lastName; } public string GetFullName() { return string.Format("{0} {1}. _firstName, _lastName); } }
class Printer { public void PrintUser(string fullName) { Console.WriteLine(fullName); } }
Jako anty-wzorce można by tutaj pokazywać wszelkiej maści klasy z nazwą „manager” itp. Które są często pchane przez całą aplikację wzdłuż i wszerz tylko dlatego, że w różnych miejscach potrzebne są ich różne kawałki.
Zalety
Na pierwszy rzut oka drugie rozwiązanie to po prostu więcej kodu. I to powoduje, że zasada pojedynczej odpowiedzialności jest zbyt rzadko stosowana przez programistów w dostatecznym stopniu – bo program zdaje się puchnąć.
Jednak w dłuższej perspektywie zapewnia nam to większą elastyczność i pozwala uniknąć duplikacji kodu. Po pierwsze chcąc wyświetlić pełne nazwisko z różnych źródeł (z różnych kontekstów w aplikacji) możemy wykorzystać do tego tą samą klasę, która jest odpowiedzialna za wyświetlanie i nie obchodzi jej skąd ma te dane. Tak samo chcąc zmienić formatowanie wyświetlania nazwiska albo miejsce jego zapisu robimy to w jednym miejscu, a nie przeszukujemy każdą klasę, która może zawierać taką metodę. Bo czemu użytkownik, wyciągnięty np. z bazy danych, ma wiedzieć jak zaprezentuje się jego nazwisko na ekranie? Tym bardziej jeśli nasz system może go przepchnąć do aplikacji mobilnej, na stronę internetową albo do drukowanego raportu.
Podsumowując reguła pojedynczej odpowiedzialności jest sposobem na pisanie kodu zdatnego do ponownego wykorzystania i prostego w modyfikacji, a więc i utrzymaniu.
Leave a Comment