W tym artykule opiszę bardzo ważną kwestię, którą jest czytanie cudzego kodu, ale równocześnie będą, to również wskazówki jak pisać własny kod tak, aby ktoś inny mógł go zrozumieć. Pracując z narzędziem git, każdy fragment kodu posiada informację o autorze, zmianach w kodzie i czasie dokonania zmian. Tym samym “uchylanie się” od własnego kodu jest niemożliwe, mówienie, to nie mój kod nie zadziała.
Wstęp
Skupię się jedynie na czytelności kodu, która rzuca się w oczy już przy pierwszym code review. Dodatkowo opiszę tak ważną kwestię jak Definition of Done, czyli kwestia ustalenia kiedy kod, który został napisany uznajemy za skończony.
Nie będę zagłębiał się w tym artykule w takie elementy kodu jak:
– architektura aplikacji,
– udostępnianie funkcjonalności przez API,
– moduły/warstwy aplikacji.
Zajmę się takimi elementami kodu jak:
– konwencja nazewnicza,
– kolejność elementów w klasie,
– samoopisujący się kod,
– wzorce projektowe,
– dobre praktyki SOLID i DRY.
Kod źródłowy czytamy jak dobrze napisaną książkę
“Kod napisany w dowolnym języku programowania powinien czytać się jak dobra książka. Osoba czytająca książkę nie chciałby przecież być zanudzana dokładną kopią fragmentów książki umieszczaną w wielu miejscach tej samej książki (DRY – Don’t Repeat Yourself). Ciężko byłoby czytać książkę, która ma pomieszane rozdziały, akcja nie jest spójna i ma się wrażenie, że autor chyba nie wiedział co pisze (SOLID).” – artykuł “Dlaczego kod w języku Java powinien być SOLID’ny oraz suchy, DRY?“.
Prawdą jest, że sama znajomość podstaw języka Java nie oznacza, że będziemy pisać czytelny, poprawny i ładny kod. Tak samo jak znając język polski nie każdy potrafi pisać książki, do tego mamy różne gatunki literackie np.: powieść, dramat.
Definition of Done – kiedy mój kod jest ukończony?
Zacznijmy od Definition of Done – kiedy mój kod jest ukończony? Książka ma swój początek i koniec, składa się z rozdziałów i w pewnym momencie zostaje wydana, aby czytelnicy mogli zapoznać się z dziełem danego autora. W przypadku książek proces tworzenia i wydawania jest usystematyzowany. Tak samo jest z kodem źródłowym, ale nie każdy zdaje sobie z tego sprawę, że znajomość języka Java, to dopiero początek (więcej w artykule Czy sam język Java wystarczy?), a proces wytwarzania oprogramowania jest pracą zespołową.
Wracając do Definition of Done, pisząc kod źródłowy należy ustalić, co rozumiemy przez “skończyłem pisać mój kod, teraz ktoś może z niego korzystać”. Dlaczego w ogóle Definition of Done w artykule o tym jak pisać czytelny kod? Nie każdy tak samo rozumie kiedy kod jest skończony. Wyjaśnię na przykładzie wymagania biznesowego wykonującego przelewy zagraniczne – więcej o wymaganiach biznesowych w artykule Ja chcę pisać tylko kod, nie interesują mnie wymagania biznesowe!.
Kod odpowiedzialny za przelewy zagraniczne według młodszego programisty jest skończony w momencie, gdy uda mu się przelać środki z jednego konta testowego na inne konto testowe – oczywiście nie robimy testów na produkcji jak mBank. Według doświadczonego programisty jest on skończony w momencie, gdy będzie miał testy jednostkowe oraz będzie SOLID i DRY. Natomiast dla osoby zgłaszającej wymaganie biznesowe, skończony kod, to taki, który został umieszczony na serwerze testowym, przetestowany przez testerów manualnych i jest zgodny z wymaganiem biznesowym.
Czytelny kod źródłowy
Wracając do czytelnego kodu, który sami piszemy lub w momencie, kiedy czytamy kod kogoś innego. Jak zawsze kij ma dwa końce. Wiele razy widziałem jak moi koledzy “rwali sobie włosy z głowy” czytając kod kogoś innego, często zadawali na głos pytania “jak, to możliwe”, “kto, to w ogóle pisał”, “przecież, to nie ma szansy działać”, “gdzie jest Heniek, idę do niego!”. Bardzo łatwo jest sprawdzić kto jest autorem danego fragmentu kodu wystarczy polecenie git-blame. Mając na uwadze to, co napisałem, należy pisać własny kod tak, aby po przeczytaniu naszego kodu nie była wysyłana po nas ekipa Terminatorów.
Konwencja nazewnicza
Konwencja nazewnicza jest bardzo ważnym elementem, polecam zapoznanie się z oficjalną dokumentacją Oracle Code Conventions for the Java TM Programming Language opisującą konwencje nazewnicze dla języka Java – dokumentację można również pobrać w formie dokumentu PDF.
Kolejność elementów w klasie
Na pewnym etapie, mając już spore doświadczenie w kodowaniu w języku Java przeglądanie klas nie wygląda tak, że patrzymy od pierwszej linijki i przechodzimy do kolejnej i kolejnej. Przeglądając klasę sprawdzamy w jakich zmiennych klasa przechowuje swój stan, jak jest konstruowana, a następnie jakie udostępnia metody i wtedy zastanawiamy się do czego możemy ją wykorzystać. Zakładamy przy tym, że zmienne są na samym początku ciała klasy, pod nimi konstruktory, metody z logiką biznesową, a na samym końcu “metody pomocnicze” getter/setter i toString.
Wyobraźmy sobie teraz sytuację, w której programista przegląda pięćdziesiątą klasę danego dnia i za każdym razem napotyka inną kolejność elementów w klasie. Wypada z rytmu, nie może się skupić tym samym spada jego wydajność. To tak jak ze wspomnianymi na wstępie rozdziałami książki, która ma pomieszane rozdziały.
Samoopisujący się kod
Kod, który piszemy powinien być samoopisujący się, tzn. widząc nazwę metody i/lub zmiennej od razu wiemy do czego służy i nie musimy zastanawiać się, co autor miał na myśli. Jeżeli stosujemy komentarze dla metody i/lub zmiennej, to oznacza, że sami nie wiemy, co dany fragment kodu reprezentuje. Jedynym dopuszczalnym komentarzem dla kodu jest Javadoc, który służy do późniejszego wygenerowania dokumentacji dla naszego kodu. Każdy Framework posiada Javadoc, np. Springframework API Doc.
Jak wygląda kod samoopisujący się? Poniżej przykład dobrej i złej konwencji nazewniczej.
// nazwa klasy nie opisuje jej przeznaczenia i funkcji
public class Main {
// metoda 'calculate' nie wiemy, co dokładnie robi
// musimy zagłębiać się w jej kod
public double calculate(int a, int b) {
return a / b;
}
}
// nazwa klasy opisuje jej przeznaczenia i funkcji
public class Calculator {
// metoda 'division' od razu informuje o tym, co będzie robić
public double division(int a, int b) {
return a / b;
}
}
Wzorce projektowe
Na wstępie zaznaczę, że w codziennej pracy Junior Java Developer rzadko kiedy ma możliwość wprowadzenia jakiegoś wzorca projektowego do istniejącego systemu. Dlaczego piszę o tym na wstępie? Chciałbym zaznaczyć, że młodszy programista języka Java powinien skupić się raczej na dobrych praktykach SOLID i DRY, a następnie rozszerzać swój warsztat o wzorce projektowe. Dodam również, że nie ma narzędzia, które wykrywa wzorce projektowe w kodzie.
Dodatkowo wzorce projektowe, które są prezentowane w wielu tutorialach są oderwane od rzeczywistości. Co przez, to rozumiem? W rzeczywistym systemie wzorce projektowe nie są wyróżnione w jakiś szczególny sposób w kodzie, tak jak ma, to miejsce w tutorialach. W istniejącym systemie nie spotkamy raczej klas np.: MenuComposite (Composite Pattern), InProgressState (State Pattern). Spotkamy raczej klasy Menu, która jest właśnie kompozytem (Composite Pattern), klasa Job w połączeniu z klasą Step będzie reprezentować wzorzec stanu (State Pattern).
Dobre praktyki SOLID i DRY
Tak jak napisałem powyżej, młodszy programista powinien skupić się na dobrych praktykach podczas pisania kodu, zamiast na siłę używać wzorce projektowe. Więcej o SOLID i DRY napisałem o nich więcej w artykule Dlaczego kod w języku Java powinien być SOLID’ny oraz suchy, DRY?