S.O.L.I.D.ne programowanie – część 2, czyli spoufalamy się
S.O.L.I.D.ne programowanie – część 0, czyli wstęp
S.O.L.I.D.ne programowanie – część 1, czyli monogamia
Witam w drugiej części cyklu “S.O.L.I.D.ne programowanie”, poświęconego zasadom S.O.L.I.D. Dziś przyjrzymy się bliżej Open-Close Principle (OCP).
Ciężko było mi wyszukać jakiś elegancki przykład no i czasu było mało, ale przepraszam za opóźnienia. Jedziemy.
Drogie panie otwieram nasz kram…
Dobry kod obiektowy powinien cechować się dużą elastycznością. Elastyczność to przede wszystkim umiejętność szybkiego dostosowania się do nowych wymagań. Szybkie dostosowanie oznacza przede wszystkim małą liczbę zmian. Wynika to z faktu, że zmieniany kod trzeba testować. W dodatku zmiany wpływają na inne elementy aplikacji i je też trzeba testować. Zasada OCP mówi:
SOFTWARE ENTITIES (CLASSES, MODULES, FUNCTIONS, ETC.) SHOULD BE OPEN FOR EXTENSION, BUT CLOSED FOR MODIFICATION.
Jako, że zmiany są jedyną pewną i niezmienną rzeczą z jaka mamy do czynienia w programowaniu, więc kod, który nie umie się zmieniać jest do wyrzucenia. Sama zasada jest przewrotna, bo cóż oznacza otwartość na zmiany i jednoczesna niemożliwość modyfikacji?
Ta przewrotność jest jednocześnie pewną filozofią. Wraz z kolejnymi zasadami odkryjemy, że celem S.O.L.I.D. jest takie konstruowanie kodu by zmiany w jego działaniu nie wpływały na kod klienta.
OCP można realizować w kilku aspektach. Przyjrzymy się wszystkim po kolei. Każdy z nich dotyczy innego zagadnienia, ale wszystkie należą do zagadnień związanych z organizacją kodu.
Dziedziczenie nie jest złe
Od wielu lat wbija się do głowy kolejnym pokoleniom programistów Java, że nie powinni używać dziedziczenia, ale implementować interfejsy. Jest to o tyle słuszne, że mało kto potrafi zaproponować odpowiedni sposób dziedziczenia. Przez sposób rozumiem tu zarówno realizację dziedziczenia w aplikacji jak i motywację i uzasadnienie tejże.
Powróćmy do naszego przykładu z pierwszej części. Przedstawiony tam przykład modemu będzie nam służył w tym artykule. Załóżmy, że chcemy rozszerzyć funkcjonalność modemu o obsługę WiFi. Co powinniśmy zatem zrobić? Najprościej jest zmienić tak implementację by możliwe było przyjmowanie połączeń nowym kanałem. Zmiany tej dokonujemy przez stworzenie dekoratora, który otoczy standardową implementację i doda odpowiednią funkcjonalność. To jest dobre OCP. Nie zmieniamy już gotowego kodu, a tylko nieinwazyjnie dodajemy do niego odpowiednie funkcjonalności.
Złym OCP będzie grzebanie bezpośrednio w kodzie, ale najgorszym będzie pisanie wszystkiego od nowa.
Na tym poziomie mówimy o OCP w odniesieniu do funkcjonalności kodu. Przyjrzyjmy się teraz samym obiektom.
Gettery i Settery to nie hermetyzacja
Co tu dużo mówić, jeżeli w naszych obiektach wszystkie pola mają metody ustawiające to nie możemy mówić o OCP. Kod tego typu nie jest zamknięty na modyfikacje ponieważ w pełni został ujawniony. Publikować należy zatem tylko to co jest naprawdę niezbędne i robimy to w sposób bezpieczny wielowątkowo.
Pakiet
Poruszyliśmy już temat funkcjonalności kodu i API klasy. Ostatnim zagadnieniem jest odpowiednie publikowanie modułów. Zazwyczaj moduły mają bardzo dużo klas publicznych. OCP jest przeciwnikiem publikowania elementów zmiennych. Prawidłowo skonstruowany moduł powinien umożliwiać jego rozszerzenie w dowolnym punkcie, ale bez możliwości zmiany kodu modułu. Oznacza to, że należy ograniczyć możliwość takiego rozszerzania klas, które realizują zadania wewnątrz modułu, że ingerujemy w moduł np. zmianiając konfigurację fabryk.
Podsumowanie
OCP jest bardzo dziwną zasadą, która nie jest oczywista do puki czegoś nie popsujemy. Zapraszam zatem do dyskusji o różnych aspektach OCP.
co ja mam dziś z tymi aspektami












November 16th, 2009 at 17:01
[...] – część 0, czyli wstęp S.O.L.I.D.ne programowanie – część 1, czyli monogamia S.O.L.I.D.ne programowanie – część 2, czyli spoufalamy się S.O.L.I.D.ne programowanie – część 3, czyli podkładamy [...]
November 17th, 2009 at 20:06
Dziedziczenie chyba posiada inny cel niż stosowanie interfejsów i jako takie jedno nie przeczy używaniu drugiego.
, chyba lepiej jest wpierw pomyśleć o zastosowaniu kompozycji niż tworzeniu hierarchii klas. Jeśli będzie się trzymać SRP to konieczność używania dziedziczenia powinna być mniejsza.
Pomijając sytuacje ‘oczywiste’
November 17th, 2009 at 20:53
To zależy. Dziedziczenie pozwalana częściową implementację pewnych wspólnych funkcjonalności. Taki przykład. Mamy jakieś moduły UI. Kilka tabelek, które wyświetlają różne dane, ale w ogólności pozwalają na wykonanie tych samych operacji. Sortowanie, usuwanie rekordów, edycja. Jak zaczniemy to pisać to szybko okaże się, że pewne funkcjonalności, po zastosowaniu genericsów, można uwspólnić. Tworzymy wtedy klasę abstrakcyjną i przenosimy tam te uwspólnione elementy. Mamy w ten sposób wzorzec klasy/metody szablonowej.
Często jest też tak, że jakieś obiekty logicznie po sobie dziedziczą np. manager po robotniku, a różnią się szczegółami implementacji jednej metody. Wtedy nie ma sensu dziedziczenie “do góry” przez wyłączenie wspólnego kodu do klasy abstrakcyjnej. Bardziej opłacalne jest dziedziczeni “w dół”, czyli rozszerzenie klasy robotnik i wymiana implementacji jakiejś metody względnie dodanie nowych metod. OCP i SRP świetnie się uzupełniają. Dobre OCP pozwala na prototypowanie kodu i późniejszą jego refaktoryzację. Sam proces refaktoryzacji jest wtedy bardzo przyjemny i stosunkowo szybki.
November 25th, 2009 at 23:33
Główny problem z dziedziczeniem pojawia się wówczas gdy używamy go do modelowani “ról”. Przykładowo Robotnik i Manager, albo lepiej Klient i Pracownik. Co jeżeli klient z czasem pewien klient stanie się pracownikiem?
Drugi problem gdy mamy kilkustopniową hierarchię dziedziczenia z powodu tego, że modelujemy być zawierający ortogonalne odpowiedzialności. Np rendering i logikę.
Co do getterów i setterów to oczywiśće racja – “dziwne”, że mainstream jakoś się tym nie przejmuje. Jedyne usprawiedliwienie dla getterow/setterow widzę gdy klasa z założenia ma odpowiedzialność bycia tępą paczką danych – jakieś DTO. Tudzież sytuacja – system z natury jest przeglądarką danych.
November 26th, 2009 at 09:32
Niekoniecznie jest tak, że modelowanie rol jest złe. Jeżeli klient stanie się naszym pracownikiem to pomimo, że w realnym świecie jest to jedna osoba to w naszej systemowej rzeczywistości będą to dwa osobne twory. Rzecz w tym, że obiekt w języku opisuje jakiś aspekt obiektu rzeczywistego. Wiele obiektów w języku może opisywać jeden obiekt fizyczny uwzględniając całkowicie inne jego aspekty.
Należy też pamiętać, że opisując problem nazwy klas takie jak robotnik, manager, klient to bardzo abstrakcyjne podejście. W rzeczywistym systemie Kowalski może być robotnikiem w przypadku płacenia pensji i managerem w odniesieniu do linii produkcyjnej. Bardzo dużo zależy od kontekstu.