5 najlepszych praktyk dobrego programowania

Jak zostać świetnym programistą? Czym właściwie są dobre praktyki programowania? Z których warto korzystać? Uchylimy rąbka tajemnicy. Eksperci od kodu w Altkom Software & Consulting zdradzili nam, co jest ich zdaniem ważne, aby kod był czysty.

5 najlepszych praktyk v2 1 1024x538 1

Czym jest dla Ciebie czysty kod?

Robert: Martin Fowler napisał “Any fool can write code that a computer can understand. Good programmers write code that humans can understand.” Zdecydowanie zgadzam się z tym stwierdzeniem. Czysty kod charakteryzuje się przede wszystkim tym, że jest rozumiany przez wszystkie osoby w zespole, nie tylko przez autora. Podstawową i obowiązkową lekturą dla każdego programisty jest książka Clean Code. Wujek Bob opisuje w niej praktycznie wszystkie kluczowe zasady, których powinniśmy przestrzegać w codziennej pracy. Do pisania czystego kodu nie jest potrzebne używanie mikroserwisów, DDD, CQRS czy architektury heksagonalnej

Adam: Czysty kod to popularna fraza w świecie IT. Każdy z nas chciałby obcować z takim kodem. Robert C. Martin, autor Clean Code, zapytał kilku programistów, czym jest dla nich czysty kod. Dla każdego z respondentów pojęcie znaczyło coś innego, nie ma jedynej słusznej definicji. W moim odczuciu to kod, który działa, jest czytelny, bezpieczny, dobrze zaprojektowany.

I. Kod powinien być łatwy w zrozumieniu

Paweł: Zwykle powtarzam: „Kod, który jest trudny w zrozumieniu, jest napisany źle”. Co sprawia, że działanie kodu nie jest od razu jasne, kiedy się na niego spojrzy? Nazwy pól, metod, argumentów, pakietów, modułów powinny jednoznacznie mówić, co dany element robi i do czego służy. Czytając kod nie powinniśmy się zastanawiać, w jakim celu powstał i jakie jest jego znaczenie biznesowe, np. metoda oblicz (List<Integer> dane) – co ona może obliczać i czym są dane wejściowe? Nie powinniśmy zagłębiać się w implementację, żeby to wiedzieć. Wiąże się to z pojedynczą odpowiedzialnością (ang. Single Responsibility Principle) – jeśli faktycznie metoda odpowiada za jedną funkcję i nie łamie reguły SOLID, łatwo ją nazwać. Dodatkowo będzie krótka, a kod będzie łatwiej zrozumieć.

Piotr: Warto zwrócić uwagę na stosowanie spójnej konwencji. Rzecz prosta do osiągniecia poprzez zastosowanie wspólnego formatera i lintera do weryfikacji przyjętej konwencji, a dzięki niej nie musimy się później zastanawiać, dlaczego nazwa jednej z metod jest małymi literami, a innej DUŻYMI i czy ta DUŻYMI jest ważniejsza?

Robert: Każdy element (stała, zmienna, metoda, klasa, pakiet, moduł) naszego kodu powinien być odpowiednio nazwany. Nazwy powinny jednoznacznie wskazywać czego dotyczy lub z czym związany jest dany element, tzw. “magic numbers/strings” powinny zostać wydzielone do stałych mówiących czego dotyczą. Przykład:

Czy wiemy o co chodzi?

if(vt > 60) 
    throw new IllegalArgumentException();  

A teraz?

private static final int MAX_VISIT_TIME_IN_MINUTES = 60;
if(visitTimeInMinutes > MAX_VISIT_TIME_IN_MINUTES)
    throw new MaxVisitTimeExceededException(MAX_VISIT_TIME_IN_MINUTES); 

Bardzo często wymyślenie sensownych nazw to jedna z najtrudniejszych rzeczy, z jakimi musimy się zmierzyć podczas developmentu. Nie tylko ja tak uważam 🙂

Adam: Kod powinien być czytelny. Dobra nazwa powinna precyzyjnie odzwierciedlać, co robi dana klasa czy metoda i być zgodna z konwencją stosowaną w projekcie. Zarówno w kontekście formatowania (np. nie we wszystkich projektach stosowany jest capslock do nazywania dla stałych), jak i użytego słownictwa.

Warto wystrzegać się sufiksów, które najczęściej zaciemniają kod: klasy kończące się na Util, Helper, Extension itd. Każdy, kto nie zna kodu takiej klasy na pamięć, nie wie za co jest ona odpowiedzialna, będzie musiał poświęć czas na jej przeczytanie. Dobrze dobrana nazwa i logiczny podział kodu na metody – o tym powinieneś pamiętać.

II. Kod, który się powtarza jest trudny w utrzymaniu

Paweł: W moim odczuciu jedną z ważniejszych kwestii jest unikanie powtarzania kodu. Mówi o tym reguła DRY (ang. Don’t Repeat Yourself). Jeżeli zaczynając tworzyć rozwiązanie wiemy, że fragment kodu będzie używany w wielu miejscach, jest mechanizmem globalnym systemu/modułu lub częścią wspólną różnych funkcji – należy podejść do tematu w taki sposób, aby wspomniany fragment mógł być w tych miejscach łatwo podłączony. Jeśli takiej wiedzy nie mamy, a podczas procesu tworzenia okaże się, że potrzebujemy tego samego fragmentu w innym miejscu, to na tym etapie powinno nastąpić wydzielenie go np. do metody i użycie w obu miejscach. Czasem dopiero po zakończeniu tworzenia funkcji systemu i spojrzeniu na nią jako całość widać pewien schemat, w którym część kodu jest identyczna lub bardzo podobna do innej. Jest to moment, kiedy powinien zostać wykonany refaktor tak, aby zlikwidować duplikację.

Dlaczego powtarzanie kodu jest złe? Kod, który się powtarza jest trudny w utrzymaniu. Jeżeli zajdzie potrzeba zmiany w nim, to musiałaby zostać wykonana we wszystkich miejscach. W przypadku wielu powtórzeń jest to czasochłonne i kosztowne. Jest ryzyko, że np. o jednym z miejsc zapomnimy lub możemy o nim nie wiedzieć, bo zostało dodane przez inną osobę. Ostatecznie funkcja po zmianie będzie działała tylko czasami, o ile w ogóle.

Adam: Pisząc kod powinniśmy stosować się do paradygmatów języka. W przypadku języka obiektowego mowa o posługiwaniu się abstrakcją, dziedziczeniem, hermetyzacją, polimorfizmem. Za rozwinięcie tych paradygmatów uważam reguły z uniwersalnego SDK (ang. Software development kit) każdego programisty: SOLID, DRY, KISS. Może nie jest to najlepsze zastawienie, gdyż SOLID już jest zbiorem wzorców, ale jak najbardziej można go rozszerzyć o pozostałe dwie zasady. Pisząc kod zgodny z tymi regułami upewniamy się, że będzie on łatwy w przyswojeniu, rozbudowie i testowaniu. Wówczas będzie lepszy. Cechą dobrego kodu jest elastyczność, otwartość na zmiany. Wysoką elastyczność można zapewniać korzystając z niskiej zależności pomiędzy klasami. Dodatkowo wysokie pokrycie testami ułatwi dalsze rozwijanie i modyfikacje istniejącego kodu.

III. Kod powinien składać się tylko z tego, co jest niezbędne

Robert: Najważniejsze i często niedoceniane praktyki, to moim zdaniem KISS (ang. Keep it simple stupid) połączona z YAGNI (ang. You ain’t gonna need it). Powinny one blokować nasze programistyczne pokusy tworzenia samemu sobie wyzwań technicznych tam, gdzie biznes ich nie wymaga. Nie przygotowuj “na siłę” swoich rozwiązań do tego, żeby zawsze były w pełni konfigurowalne. Nie twórz generycznych struktur klas “bo może kiedyś się przydadzą”. Te “kiedyś” bardzo często zamienia się w “nigdy” lub gdy w końcu nadchodzi, to okazuje się, że w szczegółach koncepcja jest jednak inna niż ta przedstawiona na początku implementacji. Moim zdaniem dobry programista NIE powinien myśleć: “Żałuję, że nie wdrożyłem tutaj więcej wzorców projektowych”. Powinniśmy skupić się na rozwiązywaniu rzeczywistych problemów, używając prostych, pragmatycznych rozwiązań.

Paweł: Dobrą praktyką jest unikanie dodatkowych operacji. Kod powinien składać się tylko z tego, co jest niezbędne. Tworzenie nadmiernie rozbudowanych mechanizmów z myślą o tym, że może będą one przydatne mija się z celem. Jeśli nie wiemy, czy coś będzie przydatne, to całkiem możliwe, że nie będzie i niepotrzebnie poświęcimy na to czas. Jeśli jednak w toku pracy zauważymy potrzebę lub schemat, który sugeruje, że dodatkowy mechanizm lub funkcja by się przydała – wtedy należy ją dodać. Refaktoring i pielęgnacja kodu to naturalny proces wytwarzania oprogramowania – nie próbujmy zrobić wszystkiego na raz.

IV. Kod ma spełniać określone cele biznesowe

Paweł: Projekt powinien mieć logiczną strukturę – podział na moduły zgodne z biznesowym przeznaczeniem. To ułatwi z nim pracę. W tym miejscu warto wspomnieć o podejściu DDD (ang. Domain-driven design), które mówi o odzwierciedleniu rzeczywistości biznesowej w strukturze projektu.

Robert: W kodzie powinniśmy używać nazw i pojęć, które słyszymy od biznesu. Jeśli słyszymy, że “oferta konwertuje się w polisę” to w naszym kodzie powinniśmy znaleźć klasę Offer, która będzie miała w sobie metodę convertTo. Z powyższymi zasadami związane jest również jedno z podstawowych założeń projektowania domain-driven (DDD), czyli język wszechobecny (ang. uniquitous language).

Piotr: Kod, który tworzymy ma spełniać określone cele biznesowe na środowisku produkcyjnym. Musimy mieć możliwość monitorowania tego, jak nasza aplikacja działa – jakie informacje logujemy o pojawiających się w niej operacjach lub przepływach? Czy w ramach tego typu logów nie pojawiają się dane wrażliwe? Czy w aplikacji nie pojawiają się nieoczekiwane błędy, a jeśli tak, to czy są poprawnie obsługiwane? Powinniśmy pamiętać o wystawieniu metryk, które możemy monitorować. Dzięki temu szybciej dowiemy się, że zaczyna dziać się coś niedobrego. Mamy szansę zareagować zanim będzie zupełnie źle.

Istotna jest odpowiedzialność na poziomie obszaru biznesowego, za który dany kod odpowiada. Przykład – jeżeli kod odpowiada za zamówienia, to niech realizuje zamówienia. Odpowiedzialność za płatności i promocje pozostawmy innym modułom. Obszary biznesowe i zależności pomiędzy nimi często nie są tak proste, jak mogłyby się wydawać. Mamy również odpowiedzialność na poziomie mniejszych struktur – klasy, metody itd. Jeżeli będziemy starali się stosować Single Responsibility Principle, to nasz kod będzie łatwiejszy w testowaniu, czytaniu i utrzymaniu. Istnieje odpowiedzialność, którą możemy delegować – jeżeli kod, który wytwarzamy jest zgodny z Open Closed Principle, to pozwala na łatwiejsze testowanie czy zmiany implementacyjne. Nie spawa naszego rozwiązania na sztywno, daje możliwość zamiany jednego rozwiązania na inne bez ryzyka, że popsujemy coś co zostało wytworzone wcześniej.

V. Bycie „skautem” nie zawsze jest opłacalne

Robert: Każda z praktyk, wymienianych w książkach/opracowaniach, stosowana niewłaściwe, może wprowadzić więcej złego niż dobrego. Chciałbym zwrócić uwagę na kilka, które przyniosły więcej problemów niż korzyści w projektach, w których uczestniczyłem.

Pierwsza z nich to, cytując wujka Boba, “Bądź jak skaut. Zostaw kemping czystszy niż go zastałeś”. Patrząc z lotu ptaka jest to świetna praktyka, którą każdy z nas powinien stosować. Jednak trzeba robić to z głową i trzymając się kilku dodatkowych zasad. Jedną z nich jest rozdzielenie zmian wynikłych z tzw. “turbo refactoringu” (czasami nazywanego też “sprzątaniem”) od zmian w funkcjonalnościach biznesowych. Druga zasada: jeśli robisz większy refactoring, a dla danego kawałka kodu nie ma testów (tak, w rzeczywistości nie jest tak różowo i mało który system ma 95% pokrycia testami), to zacznij od ich napisania. Kolejna zasada, to “jedna asercja per test”. Pamiętajmy jednak, że jest to podejście, które jest niepraktyczne i nielogiczne przy testowaniu dużych systemów klasy enterprise. W miejsce tej zasady możemy spróbować obudować “standardowe” asercje podejściem bardziej “fluent” połączonym z wiedzą biznesową i tworzyć własne klasy asercji używane w metodach testujących.

W jaki sposób zaimplementować dobre praktyki we własnym kodzie?

Robert: Programuj, programuj i jeszcze raz programuj Oglądanie stream’ów, filmików na YouTube, jak ktoś inny to robi, czy nawet czytanie najmądrzejszych książek i artykułów nie nauczy Cię pisać czystego kodu, czy też stosować różnego typu wzorców tam, gdzie są faktycznie potrzebne.

Paweł: Nie wymyślajmy koła na nowo. Istnieje szereg gotowych rozwiązań w postaci bibliotek, które często zawierają potrzebne elementy układanki. Po co pisać w kolejnym projekcie StringUtils do obsługi ciągu znaków?

Piotr: Potraktujmy ogólnie znane praktyki jedynie jako wskazówki, które warto wziąć pod uwagę, ale przede wszystkim przedyskutujmy w zespole i wspólnie zdecydujmy, które są dla nas istotne, a które świadomie zignorujemy.

Co Wy dodalibyście do naszej listy? Jakie zasady stosujecie? Napisz do nas o swoich doświadczeniach – chętnie poznamy Twoje podejście: hr@altkomsoftware.com

Rozmawiała: Kamila Binkowska, HR Team