programowanie

Jak pisać dobry i czytelny kod?

3 styczeń 2020

Jako programiści pracujemy z kodem praktycznie codziennie. Staramy się go zrozumieć, wprowadzamy w nim zmiany, sprawdzamy czy działa jak powinien. I tak w kółko, aż do znudzenia. Dla komputera jakość kodu jest nieistotna, podobnie jak dla użytkowników aplikacji. Najczęściej wystarczy, że program się nie zawiesza i robi to, co trzeba. Jednak dla nas, jakość kodu powinna mieć kluczowe znaczenie.

Czytelny i dobrze zorganizowany kod zwiększa naszą efektywność i komfort pracy. Prostszy kod łatwiej zrozumieć, co pozwala nam szybciej wprowadzać zmiany. Popełniamy przy tym mniej błędów, ponieważ mamy większą pewność, że nasz kod będzie działał prawidłowo. Jesteśmy bardziej spokojni i oszczędzamy swój czas.

Nie trudno zapomnieć, że języki programowania zaprojektowano dla nas, aby uczynić programowanie prostszym. Kod jest czytany i analizowany przez nas, a nie przez komputery. Dlatego powinien być napisany w taki sposób, dzięki któremu będzie dla nas łatwy do zrozumienia. Nie powinniśmy sobie tego jeszcze utrudniać.

Programy muszą być napisane tak, aby ludzie mogli je czytać, a maszyny wykonywać.

H. Abelson: Structure and Interpretation of Computer Programs

Zasady tworzenia dobrego kodu to temat przynajmniej na kilka grubych książek. W tym artykule pokażę Ci najważniejsze reguły, dzięki którym Twój kod będzie prosty, czytelny i elastyczny, co w efekcie pozwoli Ci zmniejszyć ilość błędów oraz przyśpieszyć cykl wydań aplikacji.

Formatuj kod

Podstawą dobrze sformatowanego kodu są wcięcia. Dzięki nim możemy w bardzo prosty sposób określić do jakiego bloku (funkcji, pętli, warunku itp.) należy dany fragment kodu. Wszystkie linie należące do tego samego bloku powinny mieć tą samą długość wcięcia, inaczej robi się bałagan.

Staraj się pisać kod w taki sposób, aby ilość poziomów wcięć, tzn. zagnieżdżonych bloków, była jak najmniejsza. Wtedy widać jak na dłoni co się dzieje, krok po kroku. Dzięki temu, że nie ma zbyt wielu zagnieżdżonych warunków czy pętli, kod staje się przez to prostszy w zrozumieniu.

Połącz logicznie powiązane linie kodu w bloki i rozdziel je pustą linią. To uprości nawigację, szczególnie w dłuższych funkcjach, ponieważ będziesz mógł z łatwością określić fragmenty odpowiedzialne np. za inicjalizację, obliczenia, przygotowanie, czy zwrócenie wyniku. Dzięki temu nie będziesz musiał czytać kodu linia po linii, będziesz mógł go skanować blokami.

Spróbuj ograniczyć ilość znaków w jednej linii. Unikniesz konieczności przesuwania kodu w poziomie, np. przy rozwiązywaniu konfliktów lub edycji kilku plików wyświetlonych jeden obok drugiego. Przyjmuje się, że długość linii nie powinna przekraczać od 80 do 120 znaków. Kod, który nie mieści się w jednej linii, zawsze można rozbić na kilka, co sprawia, że często staje się on jeszcze bardziej czytelny.

Korzystaj z automatycznego formatowania kodu. Oszczędzisz sobie sporo czasu. Wiele popularnych edytorów oferuje taką funkcję. Niestety, takie formatowanie nie zawsze sprawi, że kod będzie idealny. Czasami potrafi narobić niezłego bałaganu. Nie pozostaje wtedy nic innego, jak sformatować dany fragment kodu ręcznie.

Nazywaj rzeczy po imieniu

Każdy fragment kodu ma jakieś znaczenie. Dobrze dobrane nazwy pomagają nam zrozumieć co się w nim dzieje. Nazywaj zmienne, funkcje, czy klasy w taki sposób, aby jak najlepiej wyrazić cel ich istnienia w kodzie. Znalezienie takiej nazwy nie zawsze jest proste, ale nikt nie będzie miał wtedy wątpliwości co robi Twój kod.

Z właściwymi nazwami łatwiej zauważysz związki między pojęciami z domeny biznesowej, co pozwoli Ci lepiej podzielić kod na funkcje, klasy i moduły. Poza tym, kiedy za jakiś czas będziesz musiał coś zmienić, szybciej sobie przypomnisz co robi dany fragment kodu, gdy nazwy będą to dokładnie odzwierciedlać.

Często zdarza się, że polowanie na dobrą nazwę skutkuje zmianą struktury kodu na lepszą.

R.C. Martin: Czysty kod

Dobrze dobrane nazwy stałych, zmiennych, parametrów funkcji lub atrybuty klas powinny dokładnie określać co jest w nich przechowywane. Funkcje lub metody powinny być nazwane w taki sposób, który jasno pozwoli stwierdzić co robią. Natomiast nazwy klas lub interfejsów powinny zdradzać co reprezentują.

Im krótsza i prostsza nazwa, tym lepiej. Nikt nie lubi pisać długich nazw. Trudniej je zapamiętać, a głowa to nie śmietnik. Nie należy też przesadzać w drugą stronę. Zbyt krótka nazwa, która nie mówi zupełnie nic, też nie jest dobra. Ostatecznie, na końcu chodzi o to, aby kod był dla wszystkich czytelny i łatwy w zrozumieniu.

Unikaj skrótów. Kod to nie szyfrogram. Z czasem pewnie zapomnisz co oznaczał, a jego rozszyfrowanie będzie stanowić problem. Jeśli Tobie sprawi to trudność, to co dopiero innym?

Wybieraj nazwy, które będą unikalne i łatwe do odróżnienia. Nietrudno użyć niewłaściwej zmiennej czy funkcji, kiedy mają bardzo podobne nazwy. A błędy wynikające z takich pomyłek zawsze najtrudniej wychwycić.

Używaj tych samych słów do wyrażania tych samych koncepcji. Nie zamieniaj ich różnymi synonimami. Ciężko wtedy stwierdzić czy dalej mamy do czynienia z tą samą koncepcją, czy inną. To sprawia, że zrozumienie kodu staje się trudniejsze.

Na przykład: skąd będzie wiadomo, czy w zmiennej size znajduje się rozmiar pliku, skoro często w zmiennej o tej samej nazwie przechowywana jest ilość elementów w tablicy? Trudno wtedy udzielić pewnej i jednoznacznej odpowiedzi, jeśli nie spojrzy się w miejsce przypisania wartości do zmiennej. Nie byłoby tego problemu, gdyby programista nie mieszał pojęć i konsekwentnie używał dwóch różnych nazw, np. length do określania rozmiaru tablicy oraz size do określania rozmiaru pliku.

Jeśli w zmiennej przechowywana jest wielkość fizyczna, dopisz jednostkę. Nie będziesz musiał się potem zastanawiać, czy w zmiennej o nazwie distance odległość jest w metrach, kilometrach czy może latach świetlnych.

Podążaj za stosowanymi w danym projekcie praktykami nazewnictwa. Nie mieszaj nazw w stylu camelCase ze stylem snake_case. Jeśli widzisz, że wszystkie zmienne zawsze pisane są z podkreśleniem, a klasy z dużych liter – pisz tak samo. Inaczej wprowadzasz tylko chaos.

Pisz krótko i na temat

Mniej kodu, to mniej pracy i mniej potencjalnych błędów. Staraj się, aby klasy oraz funkcje były zwięzłe i spójne. Dzięki temu Twój kod stanie się elastyczny. Ze zbioru małych, wypróbowanych funkcji lub klas będziesz mógł szybko, niczym z klocków, złożyć dowolną funkcjonalność.

Poza tym, ktoś kiedyś będzie musiał przeczytać Twój kod, i dostosować go do nowych potrzeb. Im mniej kodu będzie musiał przeanalizować i zrozumieć, tym szybciej będzie mógł wykonać swoją pracę. To samo dotyczy również Ciebie.

Staraj się, aby kod funkcji mieścił się na jednym ekranie. Łatwiej przeanalizować funkcję, którą można w całości objąć wzrokiem, niż taką którą trzeba ciągle przewijać góra-dół, po kilka razy. Ponadto, mniejszą funkcję łatwiej przetestować, ponieważ często zawiera mniej potencjalnych dróg wykonania, tzn. warunków lub pętli.

Ogranicz kod klasy do maksymalnie kilkuset linii kodu. Klasy o większej objętości trudno się analizuje i ciężko coś tam znaleźć. Warto się wtedy zastanowić, czy nie została naruszona zasada pojedynczej odpowiedzialności i ewentualnie spróbować podzielić kod na kilka mniejszych klas.

Umieszczaj po jednej klasie w pliku. Nazwij go tak samo, jak nazwałeś klasę. Nie przejmuj się tym, że plików będzie więcej. Zawsze możesz je pogrupować w moduły. Nie przesadzaj też w drugą stronę. Nie warto umieszczać klas, które mają zaledwie po kilka linii kodu, w osobnych plikach, tylko i wyłącznie dla zasady. Wtedy zamiast uprościć poruszanie się po kodzie, możesz je tylko utrudnić.

Nie pisz kodu, który może kiedyś się przyda. Szkoda na to czasu. Nikt z nas nie ma szklanej kuli przepowiadającej przyszłość. Zamiast tego twórz kod, który można łatwo rozszerzyć o to, co będzie potrzebne w przyszłości. Możesz to osiągnąć np. poprzez umiejętne stosowanie wzorców projektowych i reguł SOLID.

Unikaj duplikowania kodu. Inaczej utrudniasz zachowanie spójności w kodzie. Łatwo można zapomnieć o wszystkich miejscach, w których znajduje się kopia danego kodu. Nie trudno potem pominąć jakieś z nich podczas wprowadzania zmian. Zamiast kopiować dany fragment kodu, wydziel go do osobnej funkcji i wywołaj ją wszędzie tam, gdzie tego potrzebujesz. Dzięki temu, gdy w przyszłości będziesz musiał np. poprawić jakiś błąd, wystarczy, że wprowadzisz zmiany tylko w jednym miejscu. Ponadto, oszczędzasz czas oraz ograniczasz ryzyko popełnienia przez siebie błędu.

Nie komplikuj. Zawiłe konstrukcje programistyczne, czy dodatkowe warstwy abstrakcji nie zawsze sprawią, że Twoje rozwiązanie będzie eleganckie i elastyczne. Może być wręcz odwrotnie. Tylko zaciemnisz swój kod, czyniąc go trudnym do zrozumienia. Upraszczaj wszystko do granic możliwości. Z im mniejszej liczby prostych elementów składa się kod, tym łatwiej go potem utrzymać.

Korzystaj z pracy innych programistów. Szybciej osiągniesz cel swojej pracy. Wyeliminujesz kod, o który potem będziesz musiał dbać oraz unikniesz tych samych błędów, które ktoś już kiedyś popełnił. Wystarczy znaleźć odpowiednią bibliotekę.

Refaktoryzuj kod na bieżąco

Kilka nowych linii kodu często wygląda bardzo niewinnie. Jednak gdy ilość takich małych zmian z czasem się nawarstwia, może się okazać, że struktura kodu, która kiedyś była poprawna, teraz już taka nie jest. To naturalna kolej rzeczy. Ważne, aby jak najszybciej to zauważyć i przywrócić ład w kodzie.

Zmiany przeprowadzane w złym kodzie zwykle prowadzą do tego, że staje się on jeszcze gorszy.

R.C. Martin: Czysty kod

Poprawiaj i porządkuj kod tak często, jak tylko zauważysz jakiś bałagan. Widzisz źle sformatowany kod? Popraw go. Nazwa funkcji nie odzwierciedla tego co robi? Zmień ją. Widzisz kod, który się powtarza? Wydziel funkcję pomocniczą albo nową klasę. Zauważyłeś kod, którego już nikt nie używa? Usuń go. Brakuje testu? Dopisz. Funkcja lub klasa jest za długa? Podziel ją na mniejsze.

Refaktoring to ewolucja, a nie rewolucja. Jedna duża poprawka wykonana raz na jakiś czas, zamiast pomóc, może być tylko źródłem wielu problemów. Dopiero suma regularnych, drobnych usprawnień, pozwoli kontrolować dług techniczny.

Jeśli musisz zrobić większy refaktoring, warto wcześniej określić które moduły są najczęściej modyfikowane. Pewnie tam będzie najwięcej bałaganu. Im szybciej zrobisz tam porządek, tym prędzej odczujesz korzyści płynące z dobrego kodu.

Podziel pracę na jak najmniejsze części. Istnieje wtedy mniejsze ryzyko, że coś zepsujesz. Staraj sie wdrażać efekty swojej pracy na bieżąco i obserwuj czy wszystko działa jak powinno. Gdy nagle coś przestanie funkcjonować, łatwiej określić skąd wziął się błąd, kiedy nowych zmian w kodzie jest mniej.

Zanim zaczniesz, zawsze upewnij się, że dobrze rozumiesz kod, który będziesz zmieniał i sprawdź czy jest solidnie pokryty testami. Jeśli ich nie ma, to warto je wcześniej dopisać. Inaczej bardzo łatwo możesz coś nieświadomie zepsuć.

Przedyskutuj z pozostałymi członkami zespołu większe poprawki, które chciałbyś wprowadzić. Unikniesz wtedy wielu problemów i nieporozumień. Wspólnie możecie dojść do lepszych rozwiązań, a Ty zyskasz pewność, że Twoja praca nie pójdzie na marne. To również dobra okazja do nauki.

Jeszcze jedna ważna uwaga: wypowiadając się krytycznie o pracy innych, staraj się to robić z wyczuciem. Nikt nie powinien poczuć się urażony Twoimi uwagami. W końcu nie chodzi o to, aby kogoś ochrzanić, ale żeby rozwiązać problem.

Dokumentuj tylko to, co trzeba

Opisywanie w komentarzach tego jak działa kod, szczególnie linia po linii, to strata czasu. To dodatkowa robota, która nie posuwa pracy do przodu. Taka dokumentacja, w miarę wprowadzania zmian w kodzie, często staje się nieaktualna. Czytelny kod nie wymaga drobiazgowej dokumentacji każdej linii. Stanowi dokumentację sam w sobie.

Umieszczaj w komentarzach te informacje, które nie wynikają wprost z kodu, np. założenia, ostrzeżenia, wyjaśnienia czy źródło pochodzenia użytych algorytmów. To pozwoli innym zrozumieć dlaczego kod działa w taki, a nie w inny sposób.

Jeśli masz problem ze zrozumieniem danego fragmentu kodu, i jedyne rozwiązanie jakie przychodzi Ci do głowy, to więcej wnikliwych komentarzy, to miej świadomość, że schodzisz na ciemną stronę mocy. Taki kod raczej wymaga refaktoryzacji. Lepsza dokumentacja nigdy nie zastąpi lepszego kodu.

Nie komentuj złego kodu - popraw go.

B.W. Kernighan i P.J. Plaugher: The Elements of Programming Style

Dokumentuj każdą klasę, funkcję czy interfejs. W przypadku klasy czy interfejsu opisz jaki jest jej cel i rola. W przypadku funkcji wyjaśnij krótko co robi, jakie przyjmuje argumenty, jakie wyjątki może wyrzucić, oraz czy zwraca jakiś wynik.

Popularne IDE potrafią analizować komentarze w określonym formacie oraz wyświetlać na ich podstawie podpowiedzi. Dzięki temu nie będziesz musiał zaglądać do kodu za każdym razem, gdy będziesz potrzebował jakiś informacji. Pozwoli to też nowym programistom szybciej zapoznać się z projektem.

Zawsze wypowiadaj się krótko, treściwie i na temat. Komentarze to nie miejsce do pisania bajek, esejów filozoficznych lub wylewania swoich żali. Nikt nie lubi czytać nudnych i przydługich komentarzy w kodzie. To opóźnia zabranie się do roboty.

Pisz testy

Dzięki testom wiesz, czy program działa zgodnie z założeniami. Możesz pracować nad kodem bez obaw, że coś zepsujesz i dowiesz się o tym dopiero, gdy wypuścisz aktualizację. Ponadto, testy stanowią świetny przykład użycia danego kodu, co jest pewnym rodzajem jego dokumentacji.

Jeśli sam nie przetestujesz swojego oprogramowania, zrobią to za Ciebie użytkownicy.

B.W. Kernighan i P.J. Plaugher: The Elements of Programming Style

Testy wymagają dobrego przemyślenia struktury kodu oraz jego podziału na funkcje i klasy. Dzięki temu kod automatycznie staje się lepszy i czytelniejszy. Kiedy kod jest skomplikowany albo zagmatwany, pisanie testów może być trudne i zniechęcające. Dość łatwo wtedy pominąć jakiś specjalny przypadek, a niewykryte luki czy błędy, z braku kompletnych testów, mogą potem trafić na produkcję.

Weryfikuj po jednej sytuacji w teście. Inaczej trudno stwierdzić jaki jest cel testu, co konkretnie sprawdza. Każdej sytuacji, która może się wydarzyć, powinna odpowiadać jedna funkcja testowa. Idealnie, powinna składać się z trzech etapów: przygotowania danych do testów, wykonania testowanej akcji oraz weryfikacji wyniku z wartością oczekiwaną. W ten sposób dokładnie wiadomo w jakich okolicznościach dana sytuacja może wystąpić, i jakiej reakcji oczekujemy od aplikacji.

Skup się na testowaniu przypadków brzegowych. W ten sposób możesz upewnić się, że dany fragment kodu zachowa się prawidłowo w każdym możliwym przypadku. Sprawdzenie tylko najprostszej, oczekiwanej sytuacji nie wystarczy, aby z czystym sumieniem móc powiedzieć, że dany kod został rzetelnie przetestowany.

Nie wystarczy sprawdzić, czy np. funkcja wykonująca dzielenie dwóch liczb zwraca poprawny wynik. Trzeba się jeszcze upewnić, że w przypadku dzielenia przez zero funkcja zwróci informację, że nie może wykonać takiego obliczenia. Na tym właśnie polega testowanie przypadków brzegowych.

Przewiduj możliwe błędy. Nie ograniczaj się wyłącznie do weryfikacji przypadków brzegowych wynikających z kodu. Zawsze spróbuj się zastanowić co jeszcze może się wydarzyć nieprzewidzianego. To dość trudne, ponieważ często na pierwszy rzut oka tych błędów nie widać. Zazwyczaj wydaje się, że wszystko jest zrobione dobrze. Niestety, w większości sytuacji to tylko złudzenie.

Wystarczy przeoczyć choćby możliwość podania do funkcji błędnego parametru, wystąpienia wyjątku w bibliotece, albo utratę połączenia z serwerem bazy danych. Taka sytuacja podczas dodawania rekordów, może skutkować brakiem spójności w danych. Część z nich może zostać zapisana, a część nie. Trzeba się przed takimi sytuacjami umieć zabezpieczyć. To, że coś ma małe szanse się zdarzyć, nie oznacza, że nigdy się nie wydarzy. Może być wręcz przeciwnie, i to prędzej niż myślisz.

Śledź pokrycie kodu testami. To łatwa metoda weryfikacji, która pozwala ustalić jakie fragmenty kodu zostały przetestowane, a jakie nie. Niestety, wysokie pokrycie kodu, nie zawsze świadczy o tym, że wszystko przetestowaliśmy. Istotne jest jeszcze to, czy uwzględniliśmy wszystkie możliwe błędy, które nie wynikają bezpośrednio z naszego kodu, o czym wspominałem wyżej. A to niekoniecznie może wpływać na wzrost współczynnika pokrycia testami.

Staraj się przygotować dane testowe tak, aby jak najmniejsza ich ilość wystarczała do przetestowania całej aplikacji. Dzięki temu zamiast marnować czas na ciągłe przygotowywanie nowych danych testowych, możesz od razu przystąpić do pisania testów, korzystając z danych, które już dobrze znasz.

Porównując wartości liczb z przecinkiem, zawsze określ margines błędu. Nie warto przejmować się niezgodnością wyniku na siódmym miejscu po przecinku, gdy istotne są tylko pierwsze trzy. Obliczenia na liczbach z przecinkiem, tj. float, czy double z natury nie są dokładne. Jeśli zależy Ci na większej precyzji, zmień typ danych.

Gdy wykryjesz błąd, najpierw napisz test, który go potwierdzi, a dopiero potem popraw kod. To najprostszy sposób, aby zacząć korzystać z TDD. Ponadto, na bieżąco uzupełniasz repozytorium o brakujące testy. Kiedy następnym razem zdarzy Ci się popełnić ten sam błąd, Twój test go wykryje, i będziesz miał okazję poprawić usterkę, zanim narobi poważnych szkód.

Każdy błąd należy znajdować tylko raz.

A. Hunt, D.Thomas: Pragmatyczny programista

Uruchamiaj testy automatycznie przy każdej aktualizacji kodu w repozytorium. W ten sposób bez zbędnego wysiłku otrzymasz informacje o tym, które testy zakończyły się pomyślnie, a w których wystąpił niespodziewany błąd. Zadbaj o to, aby środowisko testowe było jak najbardziej zbliżone do środowiska produkcyjnego. Zyskasz wtedy większą pewność, że kod na produkcji będzie działał równie dobrze.

Każdy test powinno dać się uruchomić osobno. To jaki z nich został wykonany wcześniej nie powinno mieć żadnego wpływu na wynik testów wykonywanych w dalszej kolejności. Dzięki temu zamiast puszczać za każdym razem cały pakiet testów, możesz uruchomić tylko te, które weryfikują nowe zmiany w kodzie.

Testy muszą być powtarzalne. Gdy niektóre z nich raz kończą się sukcesem, a innym razem niepowodzeniem, trudno wtedy stwierdzić, czy mamy do czynienia z błędem, czy nie. Zagadka jest tym większa, kiedy w kodzie, który rzekomo nie działa, nikt nic nie zmieniał. Postaraj się usunąć wszystkie czynniki, np. w danych testowych, które mogą sprawiać, że wynik testu będzie losowy. Inaczej taki test staje się bezużyteczny.

Testy powinny być szybkie. Czekanie na wykonanie testu w nieskończoność to marnotrawstwo czasu. Im szybciej otrzymasz informację zwrotną, mówiącą o tym, czy test przeszedł, czy nie, tym szybciej możesz wprowadzać w kodzie zmiany.

Warto pamiętać, że testy są przeprowadzane w ściśle kontrolowanych warunkach. Nawet jeśli wszystkie z nich zakończyły się sukcesem, to nadal nie mamy pewności, że aplikacja jest wolna od wszelkich błędów, czy usterek. To oznacza tylko tyle, że w danych sytuacjach aplikacja powinna działać prawidłowo. Z tego powodu warto monitorować pracę aplikacji także po udostępnieniu jej użytkownikom i zbierać informacje o nieoczekiwanych błędach, aby można je było potem poprawić.

Czy to już wszystko?

Prostota, czytelność i testowalność to chyba najważniejsze rzeczy wpływające na jakość kodu, choć z pewnością nie wszystkie. Czynników, które decydują o jakości kodu, a tym samym również aplikacji, jest znacznie więcej. Na koniec chciałbym się z Tobą podzielić jeszcze kilkoma wskazówkami.

Zawsze pisz kod tak, jakby gość, który miałby go po Tobie utrzymywać był brutalnym psychopatą, który wie gdzie mieszkasz.

J. Woods

Nigdy nie zadowalaj się pierwszym lepszym rozwiązaniem. Zastanów się, czy można zrobić to samo lepiej i prościej. Prawie zawsze znajdziesz coś, co da się poprawić. Postępując w ten sposób wychodzisz poza swoją strefę komfortu i zmuszasz się do myślenia ponad to, co już znasz. To jedyny sposób, aby doskonalić się samodzielnie.

Przeglądaj kod innych. Oceń czy jest dla Ciebie przejrzysty, czy rozumiesz co się w nim dzieje. Zwróć uwagę w jaki sposób autor podszedł do rozwiązania danego problemu oraz jak zaprojektował strukturę kodu. Spójrz na to krytycznie. Pomyśl co można by zrobić lepiej. To świetna okazja do nauki.

Podrzuć swój kod do oceny bardziej doświadczonemu programiście. Taka osoba szybko wychwyci błędy, które Tobie z jakiegoś powodu mogły umknąć. Poza tym to także doskonały test czytelności Twojego kodu. Jeśli jest on tak samo zrozumiały dla innych, jak dla Ciebie, to może oznaczać, że jest napisany dobrze.

Jeśli chcesz dowiedzieć się więcej na temat dobrych praktyk wytwarzania kodu i oprogramowania, warto zerknąć do poniższych książek. Na pewno znajdziesz w nich wiele interesujących informacji.