Wednesday, July 6, 2011

Poza wzorcami projektowymi

To jest odległa kontynuacja artykułu Język wzorców.

Nie pamiętam już u kogo, ale wyczytałem, że jedną z przyczyn powstania wzorców projektowych, było rozwiązanie różnego rodzaju bolączek technologicznych albo bolączek konkretnych języków programowania. Zacząłem się nad tym zastanawiać. I tak:
  • Null Object - bo w C++ referencja musi być zainicjowana; wyeliminowane przez Java, C# (nieścisłość, @see Komentarz pod postem Chrisphera Daniela)
  • State - bo implementacja maszyny stanów oparta o tablicę wskaźników do funkcji oraz serię switch/case
  • jest dość trudna w utrzymaniu
  • Interpreter - jaki tam wzorzec;)? sposób na parsowanie ustrukturyzowanych gramatyk i tyle; wyeliminowane przez tony bibliotek narzędziowych
  • Flyweigh - bo nie ma kasy na więcej pamięci
  • Command - w C# wykoszony przez delegaty (mam na myśli ten kontekst użycia, w którym robimy execute(), a nie przechowujemy komendę, aby ją potem handle() )
  • Właściwie wszystkie wzorce opisane w Core J2EE Patterns i PoEAA - zastąpione przez biblioteki, frameworki i technologie


Czym zatem są wzorce projektowe? Mówi się, że rozwiązaniami typowych problemów. Rozwiązaniami, które wg Alexandra, można stosować za każdym razem nieco inaczej (nieco inna implementacja), ale otrzyma się poprawne rozwiązanie konkretnego problemu. W porządku - może postęp technologiczny modyfikuje tylko implementacje wzorców, lecz idea zostaje podobna. Może jednak nie - czy w wzorzec zamknięty w wszytywne API biblioteki wciąż jest wzorcem, czy tylko zwyczajnym narzędziem pomocniczym?

Jak powstają?
Skąd się biorą i jak powstają? Istnieją trzy elementarne zasady postępowania, które prędzej czy później doprowadzą do dobrego rozwiązania obiektowego:
Przestrzegaj opodwiedzialności
Jeśli nie potrafimy czegoś nazwać, to prawdopodobnie nie rozumiemy jak to działa. Nie znamy odpowiedzialności tego czegoś. W swoim kodzie dbaj o to, aby każda: zmienna, metoda, klasa, interfejs, pakiet, moduł miały określoną tylko jedną opodwiedzialność, która będzie wyrażona nazwą
Enkapsuluj
To, co podlega zmianom, powinno zostać opakowane (w metodę lub w klasę - w zależności od potrzeby). Enkapsulacja pozwala na łatwiejsze zarządzanie zmianą w kodzie. Nie mówiac już o testowaniu.
Preferuj kompozycję ponad dziedziczenie
Związki używania czynią kod bardziej elastycznym, czytelnym i testowalnym. Dodatkowo pozwalają na rozszerzanie funkcjonalności kodu run-time, a nie tylko poprzez przekompilowanie.


Jeśli przestrzegamy tych zasad, prawie na pewno powstanie dobry kod. Jest jeden warunek: trzeba stosować je bardzo skrupulatnie. Żadnego wkuwania nazw wzorców, diagramów, definicji, itd. Zamiast tego: żelazna dyscyplina w stosowaniu w/w zasad.

Wymienione reguły są dość niskopoziomowe, w całościowy proces ujmuje koncept, który nazwaliśmy roboczo Nautralnym porządkiem refaktoryzacji.
Koncept Nautralnego porządku refaktoryzacji


Pisał już o tym sporo Mariusz w artykułach: NPR - Idea rysunkowa, NPR - pod lupą, NPR: Compose Method, NPR: Extract Method, NPR: Refaktoryzacja do wzorców, NPR: Ewolucja architektury, więc nie będę się powtarzał.

Powtarzając konsekwentnie ten proces i stosując wspomniane już trzy metody, otrzymujemy naprawdę porządny kod. Zaczynają pojawiać się dobre rozwiązania. Dość często są to znane wzorce projektowe.

Morfizmy w kodzie
Zauważmy, że w charakterystyka wzorca projektowego zawiera między innymi Motywację/Intencję, Kontekst, Problem. Według definicji wzorzec to rozwiązanie problemu w danym kontekście. Ale przecież nieustanie zmienia się zarówno kontekst, jak i nasze zrozumienie problemu. Wszystko się zmienia!

Aktualne dobre rozwiązanie, które przywykliśmy nazywać wzorcem projektowym, jest tak naprawdę lokalnym optimum, jakieś funkcji (powiedzmy, że Funkcji kodu) w której zmiennymi są przynajmniej: Kontekst oraz Problem.

Tak zwany wzorzec rozwiązuje problem tylko na chwilę, dopóki Funkcja kodu nie ulegnie zmianie, a optimum nie wystrzeli w inną stronę. Mamy wiec do czynienia nie tyle z wzorcami, co z optymalnymi morfizmami w kodzie (niech, Ockham mi wybaczy). Zmiana Funkcji kodu, po której programując się ślizgamy powoduje, że dany morfizm przestaje być optymalny i musimy poszukać nowego optimum właśnie poprzez refaktoryzację.
Optymalne morfizmy


Wnioski
Czy poza gimnastyką umysłu wypływają stąd jakieś wnioski? Owszem, kilka.
  1. Istnieje nieskończenie wiele optymalnych morfizmów w kodzie - ponieważ zakres zmienności Problemów oraz Kontekstów jest również nieskończony; w związku z tym publikacje i dywagacje na temat nowych wzorców projektowych nigdy się nie skończą
  2. Refaktoryzacja musi trwać - ponieważ Funkcja kodu wciąż się przekształca, wciąż musimy poszukiwać optymalnych morfizmów; zaprzestanie tych działań spowoduje takie zniekształcenie tej funkcji, że znalezienie optymalnych morfizmów będzie niemożliwe i trzeba będzie ją wyrysować na nowo - czytaj napisać system raz jeszcze
  3. Ważniejsze jest zdyscyplinowane posługiwanie się wspomnianymi wyżej zasadami i procesem refaktoryzacji niż szastanie kolekcjami nazw wzorców
  4. Nie jest możliwe stworzenie czegoś takiego jak patterns language (por: On Patterns and Pattern Languages), bo optymalne morfizmy wciąż się przekształcają podążając za zmianami Funkcji Kodu

5 comments:

  1. Null Object nie został stworzony ze względu na wymagania jakiegokolwiek języka, tylko w celu uproszczenia kodu, w myśl zasady, że nie istniejące zachowanie też jest jakimś zachowaniem, wobec czego może być odwzorowane przez pewien kontrakt.
    Dobrym przykładem tego wzorca jest NullProgressMonitor w Eclipse. Nie trzeba za każdym razem pisać
    if(progressMonitor != null).
    Kod zyskuje na czytelności i jest łatwiejszy do testowania (mniej możliwych przebiegów wewnątrz funkcji).

    ReplyDelete
  2. Dzięki, dodaję notatkę

    pozdr,
    mb

    ReplyDelete
  3. This comment has been removed by the author.

    ReplyDelete
  4. Według Alexandra wzorzec projektowy miał poprawiać szeroko pojęte user experience. Oryginalnym wzorem było 'instalowanie w pomieszczeniu co najmniej dwóch źródeł światła naturalnego' i chyba nie podlega dyskusji jak takie coś wpływa pozytywnie na jakość życia mieszkańca (na experience usera). Wzorce w programowaniu zostały nie tyle wypaczone, co pojęte zdecydowanie inaczej (na innym poziomie). Tutaj użytkownikiem nie jest 'odbiorca systemu' ale programista, a wzorce mają na celu poprawić jego odczucia - możliwości radzenia sobie z problemami otaczającego świata.

    Problemy, jak świat, nieustannie się zmieniają - a to pociąga także zmiany wzorców. Naturalną koleją rzeczy jest to że niektóre przestają przystawać do obecnej codzienności, np o wysokich budynkach i sąsiedztwie nie przekraczającym 500 osób (czy to dobrze czy źle to kwestia na inną dyskusję). Nie powinno być zatem zaskoczeniem że to samo zaczyna dziać się w IT.

    Myślę że z biegiem czasu najzwyczajniej przestajemy pamiętać o wzorcach, nieustannie stosując wymienione przez Ciebie 'elementarne zasady postępowanie'. Wtedy, niezależnie od języka, wzorce zaczynają wyłaniać się same, czasem zaskakując samych autorów. Sądzę że każdy miał w życiu sytuację kiedy ktoś łechtał ego wychwalając roztropne użycie 'chain of responsibility', podczas gdy podczas pisania tego nikt się nie zastanawiał że to wzorzec. Tak jakoś samo wyszło, pasowało, było lokalnym optimum problemu.

    Świetny artykuł Michał
    Kuba

    ReplyDelete
  5. Bardzo ciekawy wpis. Dodalbym od siebie obserwacje, ze czesto to co nazywamy wzorcem obiektowym w jednym jezyku (np. wzorzec strategia), jest integralna czescia innego jezyka (traktowanie funkcji jako obiektow). Czyli wzorzec (albo idea ktora reprezentuje) staje sie narzedziem do rozwiazania roznych problemow w roznych kontekstach.

    ReplyDelete