26-11-2008

Agile OOD w skrócie

Dziś trochę o jakości kodu. Oczywiście jasne jest, że powinna być jak najwyższa. Tylko nie jest już tak jasne jak to osiągnąć. Zwinne techniki, głównie pochodzące z XP zalecają TDD jako mechanizm wspierający jakość kodu. I faktycznie tak rozwijany kod ma dużo większe szanse na powodzenie (poprawność, utrzymywalność, itp) m.in. ze względu na jego refaktoryzację, a więc wielokrotne myślenie o tym samym kodzie (często przez wiele osób przy programowaniu w parach i współwłasności kodu) i poprawianie jego jakości. Tylko skąd wiedzieć, że refaktoryzacja przez nas stosowana faktycznie poprawia jakość kodu? Jak przewidzieć czy kod będzie dzięki temu bardziej utrzymywalny, łatwiejszy do zrozumienia, elastyczniejszy w użyciu? No więc tu przychodzi nam z pomocą parę podstawowych zasad strukturyzowania kodu obiektowego. No to po kolei:

Zasada pojedynczej odpowiedzialności (single responsibility principle):
Każda klasa powinna mieć tylko jedną odpowiedzialność. Wszystkie usługi które udostępnia powinny być z nią ściśle związane. Czasem mówi się, że to oznacza, że powinien być dokładnie jeden powód do zmiany klasy. Na przykład jeśli mamy klasę zarządzającą użytkownikami, która definiuje ich uprawnienia i dokonuje odpowiednich wpisów w bazie danych, to są przynajmniej dwa powody do jej zmiany (dwie odpowiedzialności): struktura użytkownika i mechanizm komunikacji z bazą danych. W takim przypadku powinniśmy mieć oddzielne klasy: regulującą uprawnienia i realizującą komunikację z bazą danych.

Zasada otwartości-zamknięcia (open-closed principle):
Klasy powinny być otwarte na rozszerzenia i zamknięte na modyfikacje. Z tym związane są podstawowe zasady obiektowości: hermetyzacja - to dzięki niej możemy zamknąć klasę na modyfikacje (metody prywatne) - oraz dziedziczenie, dzięki któremu możemy klasę rozszerzyć.

Zasada podstawiania Liskov:
Klasy dziedziczące muszą móc być wykorzystywane tak jak ich klasa bazowa. Z tego wynika, że klasy dziedziczące nie powinny zmieniać kontraktu metod przy ich przeładowywaniu (redefiniowaniu w klasie dziedziczącej). To może spowodować, że klient klasy bazowej nieświadomy tego jaką jej implementację dostaje może czynić jakieś założenia co do jej funkcjonowania (i się rozczarować...) Jak tego uniknąć mówi kolejna zasada:

Design by Contract:
Warunki wstępne (pre-conditions) dla przeładowywanych metod w podklasie (czyli założenia co do wartości ich parametrów) być takie same albo słabsze niż dla metody która jest przeładowywana w nadklasie. Odwrotnie dla warunków wyjściowych (post-conditions - wynik metody, wyjątki) - te powinny być takie same bądź mocniejsze/węższe niż dla tej metody w nadklasie. Dzięki temu metody podklasy wykorzystywane przez klienta jako metody nadklasy (przez polimorfizm) zawsze będą spełniać kontrakt ich oryginałów w klasie bazowej, nie spowodują więc nieoczekiwanych efektów po stronie klienta.
Zasada odwrócenia zależności:
Tam gdzie to tylko możliwe bądź zależny od abstrakcji a nie konkretów. Czyli jeśli chcesz mieć w klasie pole typu lista, to nie deklaruj LinkedList, ArrayList ani nic takiego, tylko po prostu List. To ułatwi ci refaktoryzację jeśli okaże się, że trzeba skorzystać z innej implementacji.

Zasada segregacji interfejsów:
Klasa powinna udostępniać drobnoziarniste interfejsy dostosowane do potrzeb jej klienta. Czyli, że klienci nie powinni mieć dostępu do metod których nie używają. Ta zasada powoduje, że kod klienta twojej klasy będzie bardziej czytelny. Nie koniecznie należy wiązać te interfejsy z tym co oznacza interface w javie (z resztą zasada jest ogólna, a nie tylko dla javy, więc dotyczy również języków w których nie ma interface'ów). Często dużo lepszym interfejsem specyficznym dla klienta jest klasa dziedzicząca po tej którą chcesz udostępnić i definiująca odpowiednie metody lub składająca parę metod swojej bazowej w jedną. W językach które udostępniają poza dziedziczeniem kompozycję (np. scala poprzez mechanizm trait) można też wykorzystać ten mechanizm do składania klas z elementów specyficznych dla klienta (w ten sposób odwracając ten mechanizm - nie udostępniamy różnych interfejsów dla tej samej klasy, ale składamy tę klasę z interfejsów (i oczywiście implementacji) - trait'y w scali są do tego idealne).

Prawo Demeter:
Mówi ono, że metoda ma prawo wołać metody należące do:
- tej samej klasy
- obiektów będących polami własnej klasy
- własnych parametrów
- obiektów które tworzy
ale
- nie wołaj metod bezpośrednio na obiektach, które otrzymałeś w wyniku innego wywołania
- nie wołaj metod globalnych obiektów
Choć takie podejście ułatwia późniejszą refaktoryzację, czasem jego złamanie jest niezbędne. Poza tym przestrzeganie tej zasady powoduje utrzymywanie wysokiej spójności klas (i minimalizacji zależności).

Mów, ale nie pytaj (Tell, don't ask):
To dodatkowa zasada z tej samej grupy co Prawo Demeter. Mówi ona, że jest jak najbardziej ok żeby pobierać stan obiektu (wartość jego pól), ale nie wolno wykonywać żadnych funkcji związanych z tym obiektem (tym stanem) poza tym obiektem. Wszystkie decyzje dotyczące obiektu bazujące wyłącznie na jego stanie powinny być podejmowane wewnątrz niego samego.


Takich zasad można stworzyć pewnie więcej. Całe tony tych i podobnych (wraz ze szczegółowymi wyjaśnieniami i odniesieniami do literatury) spisane są na wiki Warda Cunninghamma (http://c2.com/cgi/wiki) które polecam do czytania.
Teraz już poprawianie jakości kodu powinno być dla Ciebie trywialne... :)

Komentarze (2):

Anonymous Anonimowy pisze...

Na brodę von Neumanna!

Zasada Podstawienia Liskov, nie Liskova (a już w ogóle nie Liskov'a)

Barbara Liskov jest kobietą (i niedawną laureatką nagrody Turinga)

5 czerwca 2009 00:51  
Blogger Paweł Lipiński pisze...

Dzięki, już zmieniam. A najlepsze jest to, że sam wszystkich z tym poprawiam :)

5 czerwca 2009 09:11  

Prześlij komentarz

<< Strona główna