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