Pracując nad implementacją krzyżowania i mutacji omawianych w ostatnim poście, natrafiłem na kolejną okazję, aby zahaczyć nieco temat zasad SOLID. Tym razem w easyGALib pojawia się zasada pojedynczej odpowiedzialności.
Być może przy okazji ostatnich postów dotyczących implementacji samego serca biblioteki, z ciekawości zerknąłeś na kod źródłowy projektu. Pojawia się tam folder o nazwie Algorithm, gdzie znajdziesz właśnie klasy realizujące podstawę działania algorytmu genetycznego.
To co nas w tej chwili interesuje, to klasa abstrakcyjna GABase i dziedziczące po niej: GAChar, GADouble, GAInt i GAString. Dlaczego właśnie takie rozwiązanie powinniśmy zastosować? Wspominałem już, że to po to, aby wspólna logika znajdowała się w klasie bazowej, a wszystkie szczegółowe działania w klasach pochodnych, ponieważ będą one zależały od konkretnej reprezentacji chromosomu. Dla przykładu mutacja będzie inaczej wyglądać w przypadku osobnika będącego listą intów, a inaczej dla listy znaków tekstowych. Mamy tutaj więc do czynienia z pewnego rodzaju specjalizacją omawianych klas – jedna będzie odpowiedzialna za działanie na intach, a inna na stringach. Oczywiście równie dobrze moglibyśmy stworzyć jedną klasę i w niej kilka metod operujących na różnych chromosomach, jednak istnieje zasada, która nas przed tym przestrzega.
Zasada pojedynczej odpowiedzialności
Mówi ona o tym, że powinniśmy ograniczać zakres odpowiedzialności klas. Sprowadza się to do tego, że nie powinny one zajmować się więcej niż jednym zadaniem, aby w przyszłości nie wystąpiła konieczność modyfikowania ich z kilku różnych powodów. W celu zobrazowania tego problemu porównać możemy przykładową klasę do noża szefa kuchni – jest jeden wyspecjalizowany do danego produktu i tak powinny też działać klasy w projekcie. Szef kuchni nie korzysta przecież ze szwajcarskiego scyzoryka do przygotowania mięsa. Nie powinno się więc tworzyć klas, które są używane do wszystkiego jak np.:
public class Scyzoryk { public void KroimyMieso(); public void KroimyChleb(); public void KroimySer(); public void KroimyZiolo(); }
Wszystko to z jednego powodu – przy każdej zmianie tego dotyczącej działania dowolnej z metod, będziemy modyfikować tę klasę. Oczywiście tutaj mamy dosyć prosty przypadek i nie jest to dla nas problem, ale w bardziej skomplikowanych klasach, przy wielu zależnościach, modyfikacja kodu staje się nie lada wyzwaniem. Najlepiej jest więc upraszczać klasy jak się tylko da stosując zasadę pojedynczej odpowiedzialności. Tutaj mogłoby to wyglądać następująco:
public abstract class Noz { protected abstract void Kroimy(); } public class NozDoMiesa : Noz { protected override void Kroimy() { Jakiś specyficzny sposób krojenia mięsa... } } public class NozDoChleba : Noz { protected override void Kroimy() { Jakiś specyficzny sposób krojenia chleba... } }
Tym sposobem mamy klasy, z których każda odpowiedzialna jest za krojenie jednego konkretnego produktu. Zmieniając coś w jednej nie musimy się przejmować czy w drugiej czasem coś nie zepsujemy.
Zasada pojedynczej odpowiedzialności dba więc, by rozdzielać odpowiedzialność na tyle klas, ile różnych procesów nasz kod realizuje. W pośredni sposób dba też o to, aby nasz kod był przejrzysty, ponieważ zdecydowanie łatwiej odnaleźć się w klasie, która nie ma setek linijek kodu.
7 kwietnia 2016 at 23:26
Nie wiem czemu, ale przykład z nożem kłóci mi się z rozumieniem Single Responsibility Principle. W sensie dla mnie klasa nie może mieć za dużo odpowiedzialności, ale żeby zaraz robić dziedziczenie z tego powodu. To znów kłóci się z composition over inheritance. Swoją drogą do tworzeni noży wypadałoby użyć wzorca projektowego, ale to zbyt skomplikowane.
Proponuję przykład:
class Samochód
void jazda
void naprawa
void tankowanie
No to na pewno za dużo odpowiedzialności jak na klasę i do tego auto przecież się nie tankuje. Wypadałoby zrobić:
class Samochod
void jedzie
class Mechanik
void naprawa
class StacjaBenzynacja
void tankowanie
Swoją drogą – ja zarzuciłem pisanie klas po Polskiemu, bo użycie Polskich liter to zło, a z drugiej pisanie bez nich nie jest lepsze. Angielski sprawdza się tu najlepiej niestety.
8 kwietnia 2016 at 09:10
Dzięki wielkie za przykład! Chciałem zachować jakieś powiązanie układu klas z easyGALib, stąd wpadłem na coś takiego.
8 kwietnia 2016 at 13:45
Tez przyklad z nozem slabo mi pasuje.
Moze bardziej jakby byly w nim metody.
Kroj()
Umyj()
12 kwietnia 2016 at 16:23
Zgadzam się z przedmówcą! Rzeczywiście to lepiej zobrazowałoby wszystko 🙂