Clean Code to nic innego jak zrozumiały i czysty kod. Kładąc nacisk na jego jakość, możemy wyeliminować wiele problemów w przyszłości. Pisanie i utrzymywanie wysokiej jakości kodu świadczy o profesjonalizmie programisty. Stosowanie Clean Code wiąże się oczywiście z poświęcaniem większej uwagi na strukturę kodu, jednak paradoksalnie pozwala oszczędzić czas.

Paradoks ten wynika ze zjawiska tak zwanego “długu technicznego”, który dotyka każdego projektu – nie tylko programistycznego. W warsztacie nieodkładanie narzędzi na swoje miejsca sprawi, że początkowo zaoszczędzimy nieco czasu, ale później stracimy go dużo więcej na uporządkowanie bałaganu. Podobnie pisanie chaotycznego kodu jest początkowo szybsze, ale ostatecznie – nawet w skali kilku dni – prace programistyczne zajmą więcej czasu.

Czysty kod w praktyce

Jako przykład czystego kodu poniższy fragment prezentuje scenariusz standardowego testu dla aplikacji internetowej:

Funkcja: Logowanie do aplikacji

Szablon Scenariusza: Jako użytkownik aplikacji, chcę mieć możliwość zalogowania na wcześniej utworzone konto.

Zakładając, że jestem na stronie logowania
Kiedy wpiszę "Jan Kowalski" w polu "Login"
I wpiszę "moje_tajne_hasło" w polu "Hasło"
I kliknę przycisk "Zaloguj"
Wtedy widzę stronę główną

Opiera się on na prostym szablonie i jest napisany językiem zrozumiałym dla każdego. Niemal każda osoba po krótkim szkoleniu byłaby w stanie napisać w ten sam sposób kolejne testy – na przykład taki, który sprawdza, czy użytkownik może się wylogować.

Co nam jednak daje taki prosty zapis? Do czego służy? Otóż za pomocą jednego kliknięcia możemy go uruchomić, aby automatycznie przetestował zadany scenariusz – krok po kroku – w ułamek sekundy. Wydaje się niemożliwe? A jednak!

Powyższy fragment to autentyczny kod napisany w języku Gherkin, który zostałby odpowiednio odczytany, zinterpretowany i wykonany. Język ten z założenia ma być jak najbliższy ludzkiej mowie. Oczywiście komputer nie jest na tyle inteligentny, aby domyślić się, co kryją dane hasła. Nie możemy też używać losowych słów. Każda fraza kryje pod sobą zaprogramowane funkcjonalności.

Na przykład “jestem na stronie logowania” zostanie zaprogramowane jako “Wykonaj zapytanie do strony https://test.globe-group.com/login”. Do przygotowania takich testów potrzebny jest więc co najmniej jeden programista, który przygotuje odpowiednie frazy. Same scenariusze – jak widać powyżej – nie wymagają jednak znajomości tajników programowania.

Dla porównania spójrzmy na pracę jednego z zawodników Turnieju Nieczytelnego Kodu w C (“Obfuscated C Code Contest”) z 2006 roku:

main(_){_^448&&main(-~_);putchar(--_%64?32|-~7[__TIME__-_/8%8][">'txiZ^(~z?"-48]>>";;;====~$::199"[_*2&8|_/64]/(_&2?1:8)%8&1:10);}

Co robi powyższy kod? Przecież to oczywiste – wyświetla obecną godzinę!

Dlaczego kod napisany w języku Gherkin jest zrozumiały dla każdego, a tego napisanego w języku C nie odszyfrowałby nawet najlepszy programista? Różnica nie tkwi w samym języku – kod napisany w C może wyglądać równie czytelnie. Polega ona na zastosowaniu praktyk czystego kodu opisujących zasady i techniki, dzięki którym pisany przez nas kod będzie nie tylko łatwy do zrozumienia, ale również do zmodyfikowania i rozbudowania o kolejne funkcjonalności.

Stosowanie praktyk Clean Code niesie za sobą szereg korzyści. Nie trudno wyobrazić sobie sytuację, gdzie – na przykład z powodu urlopu – projekt jednego programisty trafia do rąk innego. Jeśli autor kodu nie zadbał o to, aby był on czytelny, udokumentowany i dobrze przetestowany, to narazi projekt na obsunięcia czasowe. Poniżej przedstawię kilka technik, które pozytywnie wpływają na czytelność kodu, a więc i prędkość wytwarzania oprogramowania. Bardziej zaawansowane techniki wiążą się nierzadko nie bezpośrednio z samym kodem, ale z jego architekturą i relacjami, które występują pomiędzy poszczególnymi elementami aplikacji.

Zasady tworzenia czystego kodu

0. Nazewnictwo klas, metod, plików

Jako że jest to najbardziej podstawowa zasada, omówiona będzie pokrótce. Przykłady jej zastosowania widoczne są we fragmentach kodu prezentowanych w kolejnych punktach. Nazwy powinny być przede wszystkim opisowe – tak, aby na przykład po przeczytaniu nazwy funkcji nie było potrzeby analizy jej kodu. Nazwy powinny jasno przekazywać, co przechowuje dana zmienna, czy co robi dana funkcja. Nigdy nie należy stosować skrótów, ponieważ nigdy nie są jednoznaczne, przez co mogą być błędnie odszyfrowane.

1. Komentarze

Na pierwszy ogień pozwolę sobie przytoczyć dość kontrowersyjną zasadę. Komentarze w kodzie często odbierane są jako coś pozytywnego. Najczęściej opisują one, jak działa kilka linijek kodu większej funkcji lub określają jakąś zasadę logiki biznesowej, z której wynika zastosowanie takiego podejścia, a nie innego. Problemy pojawiają się jednak wtedy, kiedy założenia lub kod, do którego odnosi się komentarz, ulegną zmianie.

Najczęstszym zachowaniem, kiedy w kodzie napotkamy komentarz, jest “ja tego nie pisałem, więc zostawię tak, jak jest“. Jest to objaw szerszego zjawiska – często występującego w programowaniu – “nie ruszam, bo zepsuję“. Kod zostaje więc zmieniony, ale komentarz już tych zmian nie odzwierciedla. Teraz jest już nie tylko niezrozumiały, ale również nieaktualny i może nawet wprowadzać w błąd!

Wystarczy spojrzeć na samo zjawisko z większego dystansu. Dlaczego piszę komentarz? Bo coś jest niezrozumiałe! W takim razie zapiszmy kod w taki sposób, aby był zrozumiały sam z siebie, zamiast opatrywać go komentarzami.

        // ...
        // Sanityzacja tytułu raportu: usuwa nawiasy, kropkę i spacje.
        $report->setTitle(trim(rtrim(str_replace(["\{", "\}"], "", $title), '.')));
        // ...

Powyższy komentarz zawiera tylko szczątkowe informacje i szybko zacznie wprowadzać w błąd, jeśli kod zostanie zmodyfikowany. Lepiej jest przerobić ten fragment tak, aby kolejność i specyfika działań były jasne na pierwszy rzut oka:

    // ...
    $sanitizedTitle = $this->reportTitleSanitizer->sanitize($title);
    $report->setTitle($sanitizedTitle);
    // ...

public class ReportTitleSanitizer {
    // ...
    public function sanitize(string $title)
    {
        return $this->setTitle($title)
            ->removeCurlyBraces()
            ->trimDotFromEnd()
            ->trimWhitespaceFromBeginningAndEnd()
            ->getTitle()
        ;
    }
    // ...
    protected function removeCurlyBraces(): self
    {
        $this->title = str_replace(["\{", "\}"], "", $this->title);

        return $this;
    }
    // ...
}

Zamiast wywoływać ciągiem kolejne, niewiele mówiące funkcje lepiej przenieść tę odpowiedzialność do innej klasy, która zawierać będzie wiele małych funkcji o opisowych nazwach. Dzięki temu w razie konieczności dodania kolejnej reguły – na przykład zmiany kodowania, aby polskie znaki były poprawnie przetwarzane – nie musimy odszyfrowywać i modyfikować zawiłych linijek kodu. Wystarczy utworzyć kolejną drobną funkcję i wywołać ją na końcu łańcucha.

2. Wyrażenia logiczne

W kodzie często spotykamy się z wyrażeniami logicznymi, które reprezentowane są między innymi przez operacje “or” (lub “||”) oraz “and” (lub “&&”) i “not” (czyli negacja – “!”). Niestety nierzadko są one trudne do zrozumienia i łatwo jest popełnić błąd w ich interpretacji.

Na przykład wyrażenie:

!$number->isNegative()

oznacza liczbę, która nie jest ujemna. Czyli liczba dodatnia? Tak, ale należy pamiętać, że może to być również ZERO, co nie jest już tak oczywiste. Takie wyrażenie lepiej więc rozpisać w dłuższej formie jako

$number->isPositive() || ($number === 0)

czyli “liczba dodatnia lub równa zero”. Należy też zwrócić uwagę na okrągłe nawiasy, które wskazują kolejność działań. Kompilator czy interpreter może ich nie wymagać, ale zwiększają one czytelność.

Drugim przypadkiem są złożone wyrażenia logiczne, które składają się z więcej niż dwóch operacji:

($user->role->isAllowed(BlogPermissions::POST_CONTENT) || $user->isAdministrator()) || (($this->blog->allowedGroups(Groups::ALL)) && $this->blog->isNotModerated())

Trudność w tym przypadku polega na tym, że ciężko jest “pomieścić w głowie” dużą liczbę zależności. Sprawę utrudniają dodatkowo liczne nawiasy, które wskazują, w jakiej kolejności należy rozpatrywać poszczególne części całego wyrażenia.

Nawet rozpisując wyrażenie na bardziej przystępny język, jego zrozumienie jest nadal problematyczne: “Użytkownik może umieszczać treści na blogu LUB jest administratorem LUB blog pozwala wszystkim na umieszczanie treści I nie jest moderowany”. Rozwiązanie problemu jest bardzo proste w implementacji. Wystarczy kolejne pary występujących obok siebie wyrażeń zamknąć razem, w jednej zmiennej lub metodzie. Tak uzyskujemy dużo czytelniejszą formę: “Blog jest otwarty na wszystkie posty LUB użytkownik może umieścić post na blogu.”:

// ...
    $this->isBlogOpenForAllPosts($blog) || $this->isUserAllowedToPost($user))
// ...
    private function isBlogOpenForAllPosts(Blog $blog): bool
    {
        return $blog->allowedGroups(Groups::ALL) && $blog->isNotModerated();
    }

    private function isUserAllowedToPost(User $user): bool
    {
        return $user->role->isAllowed(BlogPermissions::POST_CONTENT) || $user->isAdministrator();
    }

3. Budowanie warstw abstrakcji

Sama zasada jest dość prosta w zrozumieniu, aczkolwiek jej implementacja może być nieco większym wyzwaniem. Składać się może na nią wiele większych i mniejszych technik, ale sprowadzają się one głównie do dwóch zasad: “ukrywajmy szczegóły implementacji” oraz “nie mieszajmy warstw abstrakcji”.

Zrealizowanie pierwszej zasady sprowadza się do tego, aby wszystkie drobne operacje – zwłaszcza te, które powtarzane są w wielu miejscach kodu – zamknięte były we własnych małych klasach i metodach. W samych klasach metody prywatne umieszczany na końcu i nazywamy opisowo. Dzięki temu kolejni programiści nie muszą za każdym razem odszyfrowywać, jak działa każda funkcja, analizując je linijka po linijce. Jeśli zobaczę wywołanie funkcji “ustawDatęPoczątkowąRaportu”, to wiem, co ona robi, bez konieczności wnikania w to, jak dokładnie została zakodowana.

Drugi krok może brzmieć dość ezoterycznie, jednak jest również dość prosty w osiągnięciu. Wystarczy, że w klasach, które ostatecznie używamy do wykonania danego zadania, będziemy wyłącznie wywoływać kolejno metody innych klas. W takich miejscach nie umieszczamy już bezpośrednich operacji na datach, kwotach, czy bazie danych tylko wywołujemy metody, które zrobią to za nas.

W skrócie: zamiast za każdym razem budować funkcjonalności z wielu małych klocków, które porozrzucane w kodzie tworzą chaos, zamykajmy wszystkie te drobne elementy we własnych, czytelnych klasach i metodach, które można ponownie używać w dowolnych miejscach kodu bez tworzenia bałaganu.

Poniżej prosty przykład obrazujący oba kroki. Najpierw, jak wygląda kod nieprzestrzegający tej zasady:

class XmlQuarterlyDepartmentReport
{
    public function __construct(\DateTime $monthStart, \DateTime $monthEnd, \App\Models\Department $department)
    {
        if ($monthStart->diff($monthEnd)->m !== 3) throw new LogicException('Kwartał powinien mieć 3 miesiące');
        $this->year = $monthStart->format('Y');
        $this->secondMonth = clone $monthStart;
        $this->secondMonth->add(new DateInterval('P1m'));
        $departmentDataProvider = new DepartmentDataProvider();
        foreach ([$monthStart, $this->secondMonth, $monthEnd] as $dateTime)
            $this->departmentExpenses[] = $departmentDataProvider->getExpensesForMonth($dateTime);
        // itp. itd. Pobieramy wszystko co potrzebujemy do tego konkretnego raportu.
    }
}

W powyższym przykładzie widzimy fragment kodu, który ma na celu zbudowanie raportu finansowego za podany kwartał. Każda wymagana operacja wykonywana jest jedna po drugiej. Kod związany z manipulacją dat wymieszany jest z tym, który pobiera dane z bazy, oraz walidacją danych wejściowych. Jeśli przyjdzie pora na przygotowanie podobnego raportu, ale np. z okresu całego roku, który dodatkowo zwracany będzie w innym formacie, konieczna będzie analiza wielu linijek kodu.

Korzystając ze wspomnianych już wcześniej rad, możemy ten proces znacznie ułatwić. Wystarczy wszystkie te drobne operacje przenieść do innych, małych klas i funkcji, które będziemy później tylko wywoływać w odpowiedniej kolejności i z odpowiednimi argumentami:

class FinancialReportDirector
{
    public function makeReport(DateTime $from, DateTime $to, Department $department, IReportFormatter $formatter)
    {
        return $this->departmentReportBuilder
            ->makeReport()
            ->setStartMonth($from)
            ->setEndMonth($to)
            ->setDepartment($department)
            ->setFormatter($formatter)
            ->build()
        ;
    }

    public function makeQuarterReport(Quarter $quarter, Department $department, IReportFormat $format)
    {
        return $this->makeReport($quarter->getFirstMonth(), $quarter->getLastMonth(), $department, $format);
    }
}

Operacje na datach, kwotach, walutach, walidacje poprawności danych i inne szczegóły ukryte są w konstruktorze i wewnątrz kolejno wykonywanych metod klasy budującej raport. Przy okazji utworzono też inne klasy, jak np. Quarter – która może walidować, czy aby na pewno podano trzy miesiące – oraz implementacje interfejsu IReportFormat, które ukrywają szczegóły formatowania.

Wspomniałem wcześniej, aby “nie mieszać warstw abstrakcji”. Jeśli pojawiłaby się potrzeba, aby do raportu dołączyć również jego autora, to nie zrobimy tego, dopisując kolejne linijki w powyższym kodzie. Ta funkcjonalność powinna być zaimplementowana w kolejnej małej metodzie, którą tylko wywołamy w powyższym łańcuchu, albo nawet ukryjemy w konstruktorze klasy budującej raport. Dzięki temu unikamy zaśmiecania kodu dodatkowymi, nieczytelnymi liniami, które za każdym razem musiałyby być przeczytane i zrozumiane.

Budując w ten sposób kolejne warstwy abstrakcji, oszczędzamy dużo czasu i kłopotów. Jeśli potrzebny będzie na przykład kolejny typ raportu, nie musimy odszyfrowywać stu linijek kodu, a – jak widać powyżej – mniej niż dziesięć. Jeśli natomiast okaże się, że raporty w danym formacie zawierają błąd, wystarczy poprawić go w jednym miejscu, gdzie również będzie niewiele linijek kodu, zamiast szukać w całym projekcie każde miejsce, gdzie tworzony jest jakiś raport.

Istnieje wiele technik i wzorców, które mają na celu sprawienie, aby kod był bardziej czytelny. Gdybym mógł wybrać jedną zasadę, którą od tej pory zaczęliby stosować każdy programista i programistka, brzmiałaby ona “piszmy kod dla ludzi“.

Dodaj komentarz