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 dopóki czegoś nie popsujemy. Zapraszam zatem do dyskusji o różnych aspektach OCP.

co ja mam dziś z tymi aspektami

6 myśli na temat “S.O.L.I.D.ne programowanie – część 2, czyli spoufalamy się

  1. Dziedziczenie chyba posiada inny cel niż stosowanie interfejsów i jako takie jedno nie przeczy używaniu drugiego.
    Pomijając sytuacje ‚oczywiste’ :), 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.

  2. 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.

  3. 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.

  4. 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.

Napisz odpowiedź

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax