Każdy kiedyś uczył się pisać. Najpierw rysując szlaczki aby opanować kontrolę nad dłonią, potem literka za literką cały alfabet. Gdy już się nauczyliśmy – charakter pisma wyrabiał się wraz z ilością zapisanego tekstu. Jednak niektórzy starali stawiać równe, okrągłe literki, starali się robić równe odstępy między wyrazami. Tak długo, aż mieli ładny, czytelny charakter pisma. Ale nie wszyscy się starają – większość pisze tak jak leci.
Tak samo jest z programistami. Gdy nauczą się podstaw programowania – nabierają doświadczenia poprzez pisanie programów i…. to wszystko. Pisanie programów w stylu: „ważne, że działa” i doświadczenie jedynie poprzez własnoręcznie napisany kod. Ale nie każdy taki jest. Niektórzy chcą pisać dobry kod – starają się, uczą nowych rzeczy, analizują inne programy, aby zobaczyć jak piszą inni. Dążą do pisania kodu w jak najlepszym stylu.
Jak jednak wygląda dobry kod? Jak się go pisze? Czterowersem z rymem naprzemiennym?
Jak można ocenić, że kod jest dobry?
Przez tyle lat gdy na świecie są komputery narodziło się mnóstwo autorytetów z dziedziny programowania. Powstały też kanony, standardy i przede wszystkim dobre style programowania. Po tych wszystkich latach można także ocenić który kod jest dobrze napisany.
Przede wszystkim program powinien być bardziej przemyślany niż napisany. Czym więcej czasu poświęcimy na projektowanie i analizę, tym więcej czasu zaoszczędzimy na programowaniu.
Przed przystąpieniem do pracy na programem powinniśmy wiedzieć co, gdzie i jak powinno w programie funkcjonować. Które klasy powinny znaleźć się w programie, co powinny wykonywać i jak się nazywać, a także z którymi innymi klasami powinny być połączone.
Na samym początku powinniśmy zadać sobie pytanie CO ten program (lub pojedynczy moduł programu) powinien robić lub CZYM powinien być. Musimy się zastanowić jakie warunki program musi spełniać aby dobrze działał – czyli wykonywał to co powinien. Pamiętaj: niezależnie gdzie pracujesz, jakie oprogramowanie tworzysz oraz jakiego języka programowania używasz – jednego możesz być pewien: wymagania się zmieniają. Dobrze napisany program pozwoli nam na szybkie zmodyfikowanie i dostosowanie programu do nowych wymagań.
Powinniśmy napisać przypadki użycia. Są to napisane krok po kroku działania programu. Może to bardzo pomóc przy projektowaniu programu. Analizując później tak sporządzony przypadek użycia – możemy rzeczowniki w nim użyte uznać jako klasy a czasowniki jako metody.
Nazwy klas powinny jednoznacznie wskazywać czym klasa jest, a nazwy metod – jakie czynności wykonują. Każdy obiekt reprezentuje jedno pojęcie (przynajmniej powinien). Nie używa się obiektów realizujących dwa lub więcej obowiązków. Jeżeli przeglądając właściwości obiektu znajdujemy właściwości o wartości null lub w ogóle nie wykorzystywane, to może oznaczać, że obiekt pełni kilka ról. Jeżeli takie właściwości istnieją, a jesteśmy pewni, że obiekt pełni tylko jedną rolę w systemie to nieużywane właściwości powinniśmy wywalić do oddzielnej klasy. Cały kod który może ulec zmianie oddzielamy od tego który nie ulegnie zmianie. Takie działanie nazywa się hermetyzacją.
Jeżeli wydzielimy to, co może się zmienić, a potem faktycznie się zmieni – a zmiany które to spowoduje będziemy musieli dokonać w klasach innych niż wyodrębniona – to oznacza, że hermetyzacja jest źle wykonana.
Obiekt powinien mieć tylko jedno zadanie a wszystkie usługi powinny się koncentrować na jego realizacji. Bardzo łatwo sprawdzić czy ta zasada jest zachowana. Wystarczy dla każdej metody utworzyć zdanie gdzie nazwa metody jest czasownikiem, nazwa klasy podmiotem a obiekty podane jako parametry metody mogą służyć jako dodatkowe rzeczowniki (chyba że parametr jest nieistotny bo np. tylko zmienia ilość lub szyk). Gdy metoda jest bez parametrów dodajemy „się”(może być w domyśle, chodzi o to, że metoda wykonuje czynność na samym sobie). Jeżeli utworzone w ten sposób zdanie łamie sens jego rzeczywistego odpowiednika to zasada jednej odpowiedzialności jest złamana. Np. klasa Obiad posiadająca metody: liczbaKalorii(), podajSkład(), ugotuj(). Porównując klasę do rzeczywistego obiektu obiad wiemy, że obiad jest w stanie podać SWOJĄ liczbę kalorii albo SWÓJ skład, ale SAM SIĘ NIE ugotuje. Więc metoda ugotuj() łamie tę zasadę. Obiad gotuje kucharz, czyli Kucharz.ugotuj(Obiad).
Analiza ta jednak w dużej mierze zależy od znajomości systemu. Np. w powyższym przykładzie metody Kucharz.ugotuj(Obiad) jest zgodna z zasadą jednej odpowiedzialności. A metoda Kucharz.ugotuj()? Czy jest zgodna? Na pierwszy rzut oka nie jest. Nie ma logicznego sensu aby kucharz SAM SIEBIE gotował. Jednak system może być zaprojektowany tak, że każdy obiad ma swojego kucharza i obiekt Obiad jest przekazywana obiektowi Kucharz jako parametr konstruktora. Zatem metoda ugotuj() wie, którego Obiadu użyć i nie potrzebuje go jako parametru. Mając tą wiedzę – czy dalej twierdzimy, że metoda Kucharz.ugotuj() łamie zasadę jednej odpowiedzialności? Pamiętaj, że ocena o odpowiednim stosowaniu zasad ( lub ich łamaniu) jest wysoce zależna od znajomości systemu.
W naszym programie powinniśmy także odpowiednio zadbać o relacje między klasami. Rodzajów tychże nie ma dużo: dziedziczenie, implementacja lub asocjacja.
Z dziedziczeniem jest pewien problem. Dziedziczenie (czy też zgodnie z UML – generalizacja) powinno być wykonane w ten sposób aby była możliwość podstawienia typów pochodnych w miejsce ich typów bazowych (Zasada paradygmatu Liskov’a). Dziedziczenie w celu rozszerzenia funkcjonalności niesie za sobą ryzyko, że klasy pochodne nie będą potrzebne do ich działania i nie będą potrafiły tych metod zaadoptować. W takich sytuacjach należy poszukać innych rozwiązań, np. delegacja(jedna z grupy asoocjacji) lub kompozycja.
Kompozycja to jest używanie interfejsów. Z tym, że nie chodzi tylko o słowo kluczowe interface ale także klasy abstrakcyjne. Kompozycja pozwala stosować zachowanie udostępnione przez rodzinę innych klas i zmieniać to zachowanie w trakcie działania. Określamy w obiekcie, którym interfejsem jesteśmy zainteresowani (który realizuje niezbędne operacje) a w jego miejsce podsyłać klasy, które ten interfejs implementują. Stosują kompozycję nie jesteśmy uzależnieni od konkretnej implementacji. Kod jest elastyczny na rozszerzanie bo można utworzyć nową klasę o tym interfejsie i ją podstawić. Używając interfejsów a nie klas które je implementują pozwoli na napisanie kodu dla klas, które nawet jeszcze nie istnieją. Kompozycja jest dobrym sposobem aby program był otwarty na rozszerzanie i podatny na zmiany, co jest bardzo ważne: powinniśmy starać się powodować aby raz napisany kod nie był modyfikowany. Jednocześnie napisać go tak aby szło go rozszerzyć, jeżeli nie przez kompozycję to np. przez dziedziczenie.
Nie ma lepszego sposobu na sprawdzenie czy pogram jest odporny na zmiany niż sama zmiana.
Innym typem relacji jest asocjacja, która oznacza zależność jednej klasy od drugiej. Utworzenie obiektu klasy A w obiekcie klasy B jest asocjacją. Są dwa główne rodzaje asocjacji: delegacja i agregacja. W delegacji obiekt który ma wykonać pewną czynność nie wykonuje jej, ale przekazuje jej wykonanie do innej klasy. W agregacji jeden obiekt jest częścią drugiego, np. obiekt Adres jest częścią obiektu Pracownik.
Częstym problemem jest nie odróżnienie kiedy używać agregacji a kiedy kompozycji. Agregację wykorzystujemy wtedy, gdy obiekt agregowany istnieje poza obiektem agregującym, jako oddzielny byt. Jeżeli obiekt agregowany nie musi mieć własnego bytu – używamy kompozycji.
Kolejną ważną zasadą jest unikanie powtarzania kodu. Najłatwiej tego dokonać poprzez wyodrębnienie wspólnych fragmentów i umieszczenie ich w jednym miejscu. Czyni to kod łatwiejszym w utrzymaniu i przy ewentualnych zmianach. Chodzi tu także o to, aby nie powtarzać funkcjonalności w kilku miejscach. W takim przypadku trzeba przenieść kod dotyczący tej funkcjonalności w jedno miejsce. Jednym słowem: należy się starać, aby każda informacja i operacja była tylko w jednym miejscu.
Pracując z dużym programem koncentruj się tylko na jednej możliwości naraz. Nie pozwól się zdekoncentrować i nie zajmuj się innymi możliwościami. Najpierw należy się skupiać na głównych funkcjonalnościach programu. Potem – wchodząc w głąb – programujemy kolejne funkcjonalności jedną po drugiej tak długo, aż będą napisane wszystkie z tego poziomu. Potem piszemy bardziej szczegółowe i tak dalej.
Drugą szkołą jest skupianie się na przypadkach użycia. Tworzymy scenariusz przejścia przez aplikację i piszemy kod aby ten scenariusz obsłużyć. Potem kolejny (inny) scenariusz i kod. Tak długo aż scenariuszami opiszemy wszystkie funkcjonalności programu.
Opanowanie tych zasad pomoże zaznajomienie się z wzorcami projektowymi – co i moim zdaniem jest wielce zalecane.
Mam nadzieję, że teraz pisany kod będzie prześliczny a jeszcze bardziej elastyczny :D – po prostu dobry kod.