Brzydkie zapachy w kodzie

14 kwietnia 2022
Jakub Rojek Jakub Rojek
Zdjęcie autorstwa Mikhail Nilov z Pexels (https://www.pexels.com/pl-pl/zdjecie/okulary-sloneczne-kwiaty-kobiety-patrzacy-9476320/)
Kategorie: Programowanie, Podstawy IT

Węch jest jednym z podstawowych zmysłów człowieka, który służy między innymi do czerpania przyjemności z odczuwania miłych zapachów. Niestety, każdy kij ma dwa końce, więc pozwala nam również mieć wątpliwej jakości radość z wąchania brzydkich, śmierdzących rzeczy. Ma to swoje uzasadnienie - jeśli coś brzydko pachnie, to prawdopodobnie jest zepsute i należy to wyrzucić. Tym skojarzeniem kierował się jeden z guru inżynierii oprogramowania, Kent Beck, który zaproponował wprowadzenie tego terminu jako określenie na fragmenty kodu, które mają "potencjał" do spowodowania później problemów z architekturą lub po prostu prowadzą do błędów. Tym samym takie code smells lub bad smells są szukane podczas przeglądów kodu lub zauważane podczas modyfikacji oprogramowania i zazwyczaj poddawane refaktoryzacji.

Istotny jest fakt, że brzydkie zapachy w kodzie zwiastują potencjalne późniejsze problemy - same w sobie najczęściej są elementem części, który działa i przeszłaby testy. Jednak każdy, kto kiedykolwiek programował, dobrze wie, że rozwój oprogramowania w dużej mierze opiera się o istniejący już kod i powielanie złych nawyków oraz korzystanie z brzydkich rozwiązań (zostawionych przez kogoś innego lub nas samych) poskutkuje błędami w przyszłości, których naprawianie zabierze wiele czasu i nerwów. Dlatego tak ważne jest, aby wyrobić sobie odruchy unikania pewnych rzeczy i po prostu z "automatu" starać się pisać dobry kod. Należy jednak też mieć świadomość, że nie zawsze brzydkie zapachy wynikają z braku umiejętności - równie często są one rezultatem pośpiechu i goniącego harmonogramu, przez co zdarzają się nawet najlepszym. Dlatego nie należy ich traktować jako "zło ostateczne" - co więcej, nie zawsze potrzebne jest ich perfumowanie. O tym krótko wspomnimy pod koniec artykułu.

Przez lata wyróżniono wiele rodzajów brzydkich zapachów - można je znaleźć w wielu zestawieniach, takich jak na przykład to. W niniejszym tekście nie będziemy tak dokładnie wszystkich analizować i zamiast tego skupimy się na najczęściej występujących błędach.

Duplikacja kodu

To zdecydowanie najpopularniejszy brzydki zapach spotykany na wielu poziomach i chyba częściej wynikający z pośpiechu niż braku wiedzy. A jego skutki są dość oczywiste - jeśli kod, który został powielony, musi zostać poprawiony, to należy to zrobić w dwóch lub więcej miejscach. Jest to niepotrzebne marnotrawstwo czasu, energii i też obciąża nasze nerwy, żeby o czymś nie zapomnieć. Oto skromny przykład, w którym widać powtarzające się fragmenty kodu (zazwyczaj te duplikaty są zdecydowanie bardziej rozległe):

...
$node1 = $document->createNode('node1');
$node1->setAttribute('attr1', 'val1');
$node1->setValue('element1');

$node2 = $document->createNode('node2');
$node2->setAttribute('attr2', 'val2');
$node2->setValue('element2');

$nodeParent->setChild($node1);
$nodeParent->setChild($node2);
...

Na szczęście, rozwiązanie w tym przypadku jest proste - wystarczy taki kod wyekstrahować do osobnej metody (lub nawet klasy) i z niej korzystać w tych wielu miejscach. Dzięki temu odpowiednią funkcję mamy nie tylko wydzieloną, ale i można ją wyszukać poprzez mechanizmy zintegrowanych środowisk programistycznych (IDE). Trudności mogą być dwie. Pierwsza to odpowiednie sparametryzowanie takiego kawałka kodu, jeśli pomiędzy każdym z powielonych miejsc były jakieś różnice. Druga to ustalenie odpowiedniego miejsca, do którego należy przenieść fragment - jeśli był wykorzystywany tylko w jednym pliku, to sprawa jest prosta (dodatkowa metoda w pobliżu), ale jeśli to listing używany w przeróżnych lokalizacjach, to zazwyczaj tworzy się odpowiednię klasę w jakimś wspólnym module: rdzeniu (ang. core), pomocniczym (ang. helper) lub użyteczności (ang. utility).

Ciąg instrukcji warunkowych

Nazywana także kolokwialnie "drabinką ifów" konstrukcja, która polega na użyciu w jednym miejscu bardzo długiej listy instrukcji if, else if lub switch, czasami zajmującej całą wysokość ekranu. Oczywiście, ma to na celu wykonanie odpowiedniej operacji lub zwrócenia konkretnej wartości w danym przypadku. Natomiast problemy zaczynają się w momencie, kiedy tych linijek jest naprawdę bardzo dużo po sobie i przeglądanie takiego kodu zaczyna być problematyczne. Także zawartość tych ifów ma znaczenie, choć w obu przypadkach wynik jest negatywny - jeśli każdy przypadek skutkuje wykonaniem tylko jednej linijki (np. zwrócenia czegoś), to sama obecność (często długich) instrukcji warunkowych przytłacza nasz wzrok, a jeśli tych operacji jest więcej, to zazwyczaj dzieje się to w bardzo długim pliku, co jest jeszcze innym typem brzydkiego zapachu. Dodatkowo, jeśli takich miejsc w kodzie jest dużo, to przy dodawaniu nowego przypadku trzeba pamiętać o umieszczeniu go we wszystkich lokalizacjach. Ponownie mały przykład:

...
if($img->format == 'png') {
	// przetwarzaj grafikę w formacie PNG
}
elseif($img->format == 'bmp') {
	// przetwarzaj grafikę w formacie BMP
}
elseif($img->format == 'gif') {
	// przetwarzaj grafikę w formacie GIF
}
else {
	// przetwarzaj grafikę w innym formacie (np. JPG)
}
...

Takim drabinkom można przeciwdziałać poprzez używanie wzorców projektowych odpowiednich do danej sytuacji - przykładem jest State, Strategy lub Abstract Factory. Jeśli jednak nie chcemy od razu sięgać po dość potężną refaktoryzację, to warto się zastanowić, czy pomiędzy poszczególnymi odnogami nie ma jakichś podobieństw, które umożliwią połączenie pewnych instrukcji w jedną, być może z dodatkowymi parametrami. Także wyciągnięcie kodu do osobnej klasy, która będzie zwracać wartość odpowiednią w danej sytuacji, może być pomocne - wówczas niejako przenosimy problem w inne miejsce, ale przynajmniej nie będzie to elementem innych klas o "poważniejszej" odpowiedzialności.

Bardzo długa klasa lub metoda

O tym zapachu zdążyliśmy już sobie wspomnieć i nic dziwnego, gdyż również występuje bardzo często i także wiąże się z pozorną wygodą. Pozorną, gdyż umieszczanie kodu "obok siebie", w jednym pliku może wydawać się szybkim i komfortowym rozwiązaniem, jednak zwróćmy uwagę, że program zazwyczaj się rozszerza, a nie kurczy, a to owocuje coraz dłuższymi plikami, klasami, metodami itd. Z tego powodu późniejsze przeglądanie kodu może być bardzo uciążliwe, a zmiany trudne do przetworzenia w różnych sytuacjach choćby przez mechanizmy w systemach kontroli wersji (np. Git). Dodatkowo, przeszukiwanie takich zbiorów dyskowych jest uciążliwe, gdyż musimy filtrować jeden lub kilka plików, a nie możemy dostać się do odpowiedniego fragmentu już po konkretnej nazwie pliku, na co pozwala wiele IDE.

Rozwiązanie jest oczywiste - jeśli dana metoda lub klasa jest zbyt długa, należy ją podzielić na dodatkowe podmetody lub podklasy. Ma to dodatkową zaletę - jeśli będziemy trzymać się odpowiedniej konwencji nazewniczej (w myśl zasady samodokumentującego się kodu), to zyskamy dodatkowy czynnik opisujący poczynania programu w danym miejscu. Zbyt długie klasy zazwyczaj wiążą się z przyjęciem przez nie zbyt dużej odpowiedzialności - zasada mówi o tym, że powinna być ona spójna i np. klasa do przetwarzania żądań odnoszących się do użytkownika, nie powinna zawierać kodu dotyczącego statusu delegacji. Dzięki podziałowi na więcej klas zyskamy dodatkowe możliwości filtrowania w IDE, gdyż łatwiej będzie nam odnaleźć plik odpowiedzialny za daną rzecz (jeśli go dobrze nazwiemy). A żeby tego było mało, mniejsze pliki oznaczają łatwiejsze i szybsze przeglądanie zawartości. W przypadku metod warto przyjąć ogólną zasadę, że jeśli nie mieści się (na wysokość) na jednym ekranie, to jest to sygnał, że warto się nią zainteresować pod kątem refaktoryzacji.

Z tym brzydkim zapachem wiąże się jeszcze inny, bardziej specyficzny, który nazywamy czasem "boską klasą" (ang. god class). Niektóre architektury oprogramowania bazują na konwencji, w której jest jedna (lub kilka) bardzo dużych klas, zawierających odwołania do wszystkich innych i de facto stanowią krytyczną część systemu. To najczęściej powoduje, że są długie, ich edycja oraz przeglądanie są niewygodne, a co najgorsze, jakiekolwiek zmiany mogą powodować problemy w innych miejscach programu. To jasny sygnał, że warto rozważyć podzielenie jej na kilka klas.

Czy zawsze opłaca się poprawiać?

W tym miejscu dochodzimy do pytania, czy zawsze opłaca się od razu usuwać dany brzydki zapach. Tutaj zależy, kogo spytacie - przykładowo, inaczej odpowie badacz inżynierii oprogramowania, a inaczej osoba zajmująca się aplikacjami komercyjnie. Wszystko zależy od sytuacji, warunków i stanu projektu.

Zawsze należy wziąć pod uwagę dostępne zasoby, zarówno ludzkie, jak i czasowe. Oczywiście, warto poprawiać kod w momencie, kiedy harmonogram nie jest jeszcze napięty, a sama poprawka też niezbyt czasochłonna. Sytuacja staje się bardziej problematyczna, kiedy zbliża się termin oddania projektu lub refaktoryzacja pociągnie za sobą duże zmiany i konieczność przetestowania wielu miejsc pod kątem potencjalnych problemów. Dlatego warto przeanalizować możliwe konsekwencje danego zapachu - jeśli jest to kod, który z dużym prawdopodobieństwem nie będzie wkrótce rozszerzany, to może warto poczekać z wyczyszczeniem danego fragmentu do czasu, gdy potrzeba tego rozwoju rzeczywiście zaistnieje. Natomiast gdy mamy do czynienia z kluczowym kawałkiem systemu lub kodem, który zawiera mechanikę często powielaną, szczególnie przez młodszych programistów, to warto od razu wziąć się do pracy.

Pamiętajmy bowiem o tym, że brzydki zapach znaleziony w danym miejscu, którego naprawa zostanie odłożona w czasie, może zostać zapomniany lub po wielu tygodniach nie będziemy pamiętać, co z nim było nie tak. Istnieje też pojęcie długu technologicznego, który powstaje, gdy takie niedobre miejsca w kodzie zostają zbyt długo i zaczynają wpływać na coraz bardziej rozszerzający się system, powodując różne problemy. Wówczas wyciągnięcie projektu z takiego dołka będzie ekstremalnie trudne, a pamiętajcie, że klient nie wyda na to dodatkowych środków.

Pewnym rozwiązaniem jest częściowa refaktoryzacja i działanie zgodnie z zasadą skautów, która nakazuje sprzatać kod "przy okazji" realizacji czegoś w jego okolicach. Wspominaliśmy o tej regule w naszym artykule o utrzymywaniu oprogramowania. Z drugiej strony, nie zawsze warto od razu skupiać się na napisaniu idealnego kodu, kiedy będzie to czasochłonne, a czas goni. Zazwyczaj i tak nie przewidzimy wszystkich przypadków i zamiast pracować nad funkcjonalnością, utkniemy w pogoni za idealnymi konstrukcjami. Wszystko zależy od umiejętności i doświadczenia programisty, ale warto wiedzieć, kiedy należy faktycznie zadbać o perfekcję, a kiedy nieco odpuścić, gdyż nie przyniesie nam to znaczących korzyści.

Podsumowanie

Brzydkie zapachy to ponownie jeden z "klasyków" inżynierii oprogramowania, bardzo często mający odniesienie do praktyki. Wiedza o tym, jaki kod źródłowy jest "ładny", a jaki może nieść ze sobą problemy, jest fundamentalna i warto ją wzbogacać w trakcie całej kariery programisty. W pewnym momencie rozwiązania same się nasuwają i stają się coraz prostsze do zastosowania. Naturalnie, w dzisiejszym tekście poruszyliśmy zaledwie skrawek z możliwych typów brzydkich zapachów - zachęcamy do dalszego doczytywania i dokształcania się.

Pozdrawiam i dziękuję - Jakub Rojek.

Piszemy nie tylko artykuły na blogu, ale też aplikacje i dokumentację dla naszych klientów. Zobacz, z kim do tej pory współpracowaliśmy.

O autorze

Jakub Rojek

Główny programista i współwłaściciel Wilda Software, z wieloletnim doświadczeniem w tworzeniu i rozwoju oprogramowania, ale także w pisaniu tekstów na różnorakich blogach. Zaprawiony w boju analityk i architekt systemów IT. Jednocześnie absolwent Politechniki Poznańskiej i okazjonalny prowadzący zajęcia na tej uczelni. W wolnych chwilach oddaje się graniu w gry wideo (głównie w karcianki), czytaniu książek, oglądaniu futbolu amerykańskiego i e-sportu, odkrywaniu cięższej muzyki oraz wytykaniu innym błędów językowych.

Jakub Rojek