Część 0
Część 1
Część 2
Część 3
Część 4
Część 5

Therion – Secret of the Runes i szósta z zasad Jeff’a Bay’a

Pilnuj wszystkie encje by były małe

Na początek mały, obrazkowy przykład dlaczego warto pisać zwięzły kod.

Oto jeden prosty program. W zasadzie jego połowa. Całość ma około 1000 linii. Tu wersja czcionką 1ką. Wyobraźmy sobie sytuację, w której:

  • W trakcie kompilacji produkcyjnej usuwamy informacje dla debuggera.
  • Coś się spieprzyło na produkcji.
  • W stacktrace nie ma informacji o linii, bo zostały one wycięte na etapie kompilacji (piękny przykład tu).

Bingo! Szkoda tylko, że twoje serce nie wytrzymało stresu i po trzydziestej kawie odmówiło współpracy.

Czym jest encja?

W sumie nie encja, bo to określenie jednoznacznie kojarzy się z ORMem, POJO i generalnie klasami. Lepszym jest blok kodu, ale zostaniemy przy encji, bo tak. Inaczej mówiąc jakaś podstawowa jednostka kodu. Na nasze potrzeby przyjmijmy, że jednostką tą jest coś co powinno zdaniem czy to kompilatora czy też checkstyle zostać zamknięte pomiędzy { i }. W szczególności są to pętle, instrukcje warunkowe, metody, klasy. Dodatkowo Jeff Bay wspomina też o pakietach.

Jak duża powinna być encja?

Odpowiedź na to pytanie brzmi tradycyjnie „to zależy”. Najprościej rzecz ujmując zależy to od konkretnego przypadku, ale można określić pewne dobre praktyki.

Zacznijmy od klasy. Najprościej jest tu odnieść się do tego co dostarcza nam IDE. Klasa powinna mieścić się na ekranie. W całości. Oczywiście zanim klasa zostanie poddana temu testowi warto zwinąć javadoci. Można też trochę oszukiwać i zmniejszyć czcionkę, ale… kod musi być czytelny. Ustaw wielkość czcionki na taką jaka jest dla ciebie wygodna do pracy, uruchom edytor w trybie pełnoekranowym (Eclipse ctrl+m). Klasa ma się mieścić na ekranie. Dlaczego tak? Ponieważ tak mała klasa jest wygodna w czytaniu. Nie tylko jednak o wygodę tu chodzi. Za dużo kodu może oznaczać, że klasa ma za dużo odpowiedzialności. Względnie realizuje zbyt ogólną funkcjonalność za pomocą niskopoziomowych rozwiązań. Przykładowo klasa modem operująca bezpośrednio na ramkach. Jak temu zaradzić? Najprościej to delegować część czynności do osobnych klas, położonych „niżej” w architekturze. Wkraczamy tu na grunt grząski ponieważ większość programistów jest przyzwyczajona do sytuacji, w której SRP zwalnia z patrzenia na kod przez pryzmat abstrakcji. Klas z obrazka na górze jest świetnym przykładem. Jedna odpowiedzialność 1000 linii kodu. To, że w ramach tej odpowiedzialności można wydzielić pewne mniejsze elementy… nieważne.

Metody są trochę prostsze do ogarnięcia. Tu można przyjąć zasadę pięciu linii – pięciu znaków ;. Jeżeli jest za dużo średników to znaczy, że robimy za dużo rzeczy naraz. Szczególnie jest to widoczne tam gdzie korzystamy ze zmiennych lokalnych. Dużo zmiennych lokalnych – dużo problemów. Zmienne lokalne są fajne, ale pod warunkiem, że ich nie używamy. Powiedzmy sobie szczerze, zmiennej lokalnej nie zastąpisz mockiem. Tym samym nie można przetestować jak zachowa się kod pod wpływem różnych wartości. Mniejszy problem jeżeli zmienna lokalna została pozyskana z parametru bądź z pola. Wtedy można to jeszcze jakoś przetestować. Gorzej jeżeli jest to zmienna pozyskana z innej zmiennej bądź za pomocą operatora new.
Rozwiązaniem jest rozbicie metody na kilka mniejszych współpracujących ze sobą.

Pętle i instrukcje warunkowe. Bardzo podobne do metod. Jeżeli są dłuższe niż 5 linijek to można założyć, że robią za dużo na raz. Często też w przypadku instrukcji warunkowych przeniesienie części kodu do osobnej metody pozwala na odnalezienie pewnej grupy metod zależnych od jednego warunku, a to jest już świetnym punktem wyjścia do wprowadzenia nowej wyspecjalizowanej w obsłudze tego konkretnego warunku klasy. Dobrą praktyką jest przeniesienie całego bloku do osobnej metody „na dzień dobry”. W przypadku pętli można też zastosować zasadę nr 8 i zamiast gołej kolekcji użyć odpowiedniego obiektu opakowującego ze zdefiniowaną metodą robiącą „coś” na elementach kolekcji.

Na koniec pakiety. Jeff Bay określił maksymalną ilość klas w pakiecie na 10. Rzecz kontrowersyjna, ale do pewnego stopnia sensowna. Tak samo jak w przypadku klas zbyt duża ilość linii może świadczyć o wykonywaniu wielu rzeczy tak w przypadku pakietów zbyt wiele klas może być wskazówką, że pakiet jest przeładowany. Co to tak naprawdę oznacza? Przywoływana przeze mnie klasa z obrazka siedzi w pakiecie wraz z około 150 innymi klasami. Każda z tych klas to osobny bean (nie do końca, ale sens jest ten sam) zatem zdawać by się mogło, że wszystko jest OK. Tyle tylko, że czasami trzeba coś zmienić. W takim przypadku rekompilacja trwa. Testy trwają. Integracja trwa. W sumie nieźle masz czas zaparzyć trzydziestą kawę. Niewielkie pakiety promują silną modularyzację w aplikacji. O ile na co dzień nie dostrzegamy tego to mając już dwie podobne aplikacje możliwość napisania kodu raz i przenoszenia go pomiędzy aplikacjami to duży plus.
Przykład z życia ELIXIR i EuroELIXIR to w praktyce identyczne aplikacje. Różnią się trochę jeśli chodzi o komunikaty przesyłane pomiędzy uczestnikami, bo wynika to z wymagań biznesowych np. Euro musi obsługiwać komunikaty w formacie XML, ale cała reszta taka jak zarządzanie oddziałami, użytkownikami, przeglądanie komunikatów, zarządzanie przebiegiem jest identyczne. Niestety COBOL i LINC nie miały możliwości pakietowania kodu. Zatem kod został zduplikowany. Po migracji duplikacja pozostała (i jest zwalczana w miarę potrzeb i wolnego czasu).
Mając kod w niewielkich pakietach potencjalna ekstrakcja interesującego nas fragmentu do osobnego projektu będzie stosunkowo prosta.

Inne metody skracania kodu

Zanim jednak zabierzemy się za skracanie kody warto przyjrzeć się kilku technikom pozwalającym na pisanie mniejszych ilości kodu od ręki.

Biblioteki

Na początek warto zastanowić się czy jakiś fragment kodu nie został już napisany. Świetnym przykładem jest wprowadzona w Javie 7 klasa Objects wywodząca się z Commons Lang i dostarczająca builderów dla metod equals czy hashCode. Podobnie ma się sprawa z np. serializacją obiektów do XML (tu polecam Simple – łatwe proste i nawet nie ma bugów). Osobnym zagadnieniem są oczywiście frameworki oraz Lombok.

Mechanizmy języka

W tym standardy. Zamiast po raz setny pisać warunek na nie nullowy parametr można użyć Bean Validation i adnotacji @NotNull (od wersji 1.1 także dla parametrów metod) albo jak nie mamy ochoty na kod beta machnięcie kilku aspektów. Szczególnie przydatnymi narzędziami są tu rozwiązania Dependency Injection w wersji czy to Springa, czy to CDI czy to Java Inject (Guice).

Własne standardy

Mój faworyt:

Listing 1. Po co ten if

public void doSth(Param param){
    if(param == null){
        throw new NullPointerException("Param is null");
    }
    param.someMethod();
}

NPE i tak by poleciał, a rzucanie wyjątku tylko by go wyrzucić mija się z celem. Szczególnie, że komunikat nic nie wnosi. W zamian warto jasno określić czy metoda akceptuje np. null. Jak nie akceptuje to zanim wstawimy warunek warto sprawdzić czy wywołanie metody już domyślnie nie wywali błędu. Zawsze jeden if mniej. Podobnie w przypadku weryfikacji. Jeżeli już rzucamy wyjątkiem to niech komunikat coś mówi. Zresztą obsługa wyjątków to osobny temat.

Podsumowanie

Jest to, nie ukrywam, bardzo trudna do wdrożenia zasada. Często można spotkać się z oporem przed rozbijaniem kodu na drobne klasy. Warto jednak pamiętać, że szczególnie większe projekty, warto rozdrabniać. Ma to na celu maksymalne uproszczenie testowania kodu i jego późniejszego utrzymania. Rozdrobnienie pozwala też na szybkie wyłapanie zduplikowanego kodu oraz błędów w projekcie. Szczególnie ta ostatnia cecha jest pożądana ponieważ pozwala na wczesne wykrywanie miejsc podatnych na błędy czy nieprzewidzianych lecz możliwych stanów aplikacji, przypadków użycia czy też innych sytuacji.