Wpis

Zasady pisania dobrego kodu

O potrzebie pisania dobrego kodu oraz najważniejszych, ogólnych zasadach, które pomagają tworzyć kod czytelny i łatwy w utrzymaniu.

Zasady pisania dobrego kodu

Potrzeba pisania dobrego kodu

Jeśli skupiać się wyłącznie na szybkim napisaniu kodu „na już”, pod dług techniczny może urosnąć do poziomu nie do udźwignięcia, a później pojawią się problemy z utrzymaniem. Dlatego podczas prowadzenia projektu developerskiego pisanie od początku czytelnego i łatwego w utrzymaniu dobrego kodu jest bezsprzecznie ważne.

W przypadku rozwiązywania zadań algorytmicznych (PS, Problem Solving) lub zawodów programistycznych (CP, Competitive Programming) często po zakończeniu zadania czy konkursu nie ma potrzeby ponownego użycia kodu, a zwłaszcza w CP, gdzie obowiązują limity czasu, można usłyszeć opinię, że ważniejsza jest szybka implementacja niż pisanie dobrego kodu. Aby odpowiedzieć na to pytanie, warto zastanowić się, po co w ogóle uprawiasz PS/CP i jaki kierunek chcesz obrać.

Moim zdaniem, jeśli poza ogólnym kształceniem umiejętności rozwiązywania problemów spojrzeć wyłącznie od strony programistycznej, to rzeczy, których można nauczyć się dzięki PS/CP, są następujące:

  • W trakcie rozwiązywania problemów w zadanych ograniczeniach czasu i pamięci można wypróbować i opanować różne algorytmy oraz struktury danych; dzięki temu w realnych projektach łatwiej wyczuć, jakich algorytmów i struktur danych użyć w konkretnej sytuacji
  • Po napisaniu i wysłaniu kodu natychmiast dostaje się obiektywny feedback: poprawne/niepoprawne, czas wykonania oraz zużycie pamięci — można więc ćwiczyć szybkie i sprawne pisanie dokładnego kodu bez pomijania szczegółów
  • Można oglądać kod napisany przez innych zaawansowanych uczestników, porównywać z własnym i znajdować obszary do poprawy
  • Ponieważ wielokrotnie pisze się relatywnie małe fragmenty kodu o podobnej funkcjonalności (w porównaniu z realnym projektem), da się ćwiczyć pisanie zwięzłego i dobrego kodu, zwracając uwagę na detale i (zwłaszcza gdy ćwiczysz PS samodzielnie) nie będąc przywiązanym do terminów

Oczywiście PS/CP można traktować po prostu jako hobby, ale jeśli robisz PS/CP po to, by podnieść umiejętności programistyczne, to ostatnia korzyść — „ćwiczenie pisania dobrego kodu” — jest równie duża jak poprzednie trzy. Pisanie dobrego kodu nie przychodzi naturalnie od razu; trzeba je systematycznie doskonalać przez powtarzalną praktykę. Co więcej, kod złożony i trudny w czytaniu ciężko się debuguje, a nawet Tobie samemu trudniej go napisać poprawnie za pierwszym razem — przez co łatwo tracić czas na nieefektywne debugowanie i finalnie wcale nie implementować tak szybko. PS/CP oczywiście różni się od realnej pracy, ale mimo to całkowite ignorowanie jakości kodu i skupianie się wyłącznie na doraźnej implementacji jest (z powodów powyżej) odwróceniem priorytetów. Dlatego osobiście uważam, że także w PS/CP warto pisać kod zwięzły i efektywny.

Komentarz dodany 12024.12:
Patrząc na obecne trendy, budowanie zaplecza wiedzy do pisania wydajnych programów (algorytmy, struktury danych itp.) oraz rozwijanie umiejętności rozwiązywania problemów wciąż będzie mieć sens, ale na etapie przekuwania tego w działający kod nie warto już koniecznie upierać się, by wszystko pisać samodzielnie. Lepiej aktywnie korzystać z AI, takiej jak GitHub Copilot, Cursor czy Windsurf, oszczędzić czas i przeznaczyć go na inne zadania lub naukę. Oczywiście jeśli robisz PS/CP dla ogólnej umiejętności rozwiązywania problemów, do nauki algorytmów/struktur danych albo po prostu jako hobby — nie ma powodu, by Cię od tego odciągać. Natomiast wkładanie czasu i wysiłku w PS/CP wyłącznie po to, by ćwiczyć samo pisanie kodu, wydaje się dziś mieć znacznie niższy zwrot z inwestycji. Co więcej, nawet w zawodach stricte developerskich znaczenie testów programistycznych jako etapu rekrutacji prawdopodobnie zauważalnie spadnie w porównaniu z dotychczasowym.

Zasady pisania dobrego kodu

Niezależnie od tego, czy kod jest pisany na konkurs, czy w pracy, warunki, które pozwalają nazwać go „dobrym”, nie różnią się znacząco. W tym wpisie omawiam główne zasady, które ogólnie pomagają pisać dobry kod. Trzeba jednak pamiętać, że w PS/CP, dla szybkiej implementacji, czasem idzie się na większe kompromisy niż w praktyce zawodowej — takie przypadki zaznaczę osobno w tekście.

Pisanie zwięzłego kodu

“KISS(Keep It Simple, Stupid)”

  • Im krótszy i bardziej zwięzły kod, tym mniejsze ryzyko literówek i prostych bugów oraz łatwiejsze debugowanie
  • W miarę możliwości pisać tak, by kod dało się łatwo zrozumieć nawet bez osobnych komentarzy; komentarze dodawać tylko wtedy, gdy naprawdę są potrzebne, aby uzupełnić szczegółowe wyjaśnienia. Lepiej niż polegać na komentarzach, jest utrzymać prostą strukturę samego kodu.
  • Jeśli piszesz komentarze, rób to jasno i zwięźle
  • Liczbę argumentów przekazywanych do jednej funkcji ograniczać do 3; jeśli trzeba przekazać więcej, zgrupować je w jeden obiekt
  • Gdy zagnieżdżenie warunków (depth) robi się podwójne lub potrójne, spada czytelność — dlatego należy unikać niepotrzebnego pogłębiania zagnieżdżenia.
    ex) W porównaniu z kodem powyżej, kod poniżej wykorzystujący idiom Guard Clause jest korzystniejszy pod względem czytelności

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    async def verify_token(email: str, token: str, purpose: str):
        user = await user_service.get_user_by_email(email)
      
        if user:
            token = await user_service.get_token(user)
      
            if token :
                if token.purpose == 'reset':
                    return True
        return False
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    async def verify_token(email: str, token: str, purpose: str):
        user = await user_service.get_user_by_email(email)
      
        if not user:
            return False
        
        token = await user_service.get_token(user)
      
        if not token or token.purpose != 'reset':
            return False
        
      return True
    
  • Jednak w PS/CP czasem, idąc krok dalej, stosuje się „sztuczki” w postaci makr C/C++, żeby skrócić kod i pisać szybciej. Może to być przydatne w konkursach o dużej presji czasu, ale jest to metoda, która sprawdza się głównie w PS/CP; generalnie używania makr w C++ należy unikać.
    ex)

    1
    
    #define FOR(i,n) for(int i=0; i<n; i++)
    

Modularyzacja kodu

“DRY(Don’t Repeat Yourself)”

  • Gdy ten sam kod jest używany wielokrotnie, wydzielić go do funkcji lub klasy i używać ponownie
  • Aktywne ponowne wykorzystywanie kodu dzięki modularyzacji poprawia czytelność, a gdy później trzeba coś zmienić, wystarczy zmodyfikować daną funkcję lub klasę tylko raz — co ułatwia utrzymanie
  • Zasadniczo idealnie jest, gdy jedna funkcja nie robi dwóch lub więcej rzeczy, tylko realizuje jedną odpowiedzialność. Jednak kod pisany w PS/CP to zwykle małe programy o prostych funkcjach, przez co możliwości ponownego użycia są ograniczone, a czas jest limitowany — więc może być trudno przestrzegać tych zasad tak rygorystycznie jak w pracy.

Wykorzystywanie bibliotek standardowych

“Don’t reinvent the wheel”

  • Na etapie nauki algorytmów i struktur danych przydatne jest samodzielne implementowanie struktur typu kolejka czy stos oraz algorytmów sortowania, by zrozumieć zasadę działania; poza tym lepiej aktywnie korzystać z bibliotek standardowych
  • Biblioteki standardowe były używane i weryfikowane niezliczoną liczbę razy oraz są dobrze zoptymalizowane, więc zwykle są wydajniejsze niż własna implementacja od zera
  • Skoro można użyć istniejącej biblioteki, nie ma sensu marnować czasu na ręczne pisanie kodu o tej samej funkcjonalności, a w pracy zespołowej innym członkom zespołu łatwiej jest zrozumieć taki kod

Stosowanie spójnego i jednoznacznego nazewnictwa

“Follow standard conventions”

  • Używać niemających dwuznaczności nazw zmiennych i funkcji
  • Dla większości języków programowania istnieją właściwe im konwencje nazewnictwa (naming convention) — warto poznać te stosowane w bibliotece standardowej danego języka i konsekwentnie je stosować przy deklarowaniu klas, funkcji, zmiennych itd.
  • Nazywać tak, aby było jasne, co robi każda zmienna/funkcja/klasa, a w przypadku typu boolean — przy jakim warunku zwracane jest True

Wszystkie dane przechowuj w postaci znormalizowanej

  • Wszystkie dane należy przetwarzać poprzez normalizację do jednego, spójnego formatu
  • Jeśli te same dane mogą występować w co najmniej dwóch formatach, mogą pojawić się trudne do wyłapania subtelne bugi (np. minimalnie różniące się reprezentacje tekstowe albo inne wartości hashy)
  • Przy zapisie i przetwarzaniu danych takich jak strefy czasowe czy napisy, należy je konwertować do jednego standardowego formatu (np. UTC, kodowanie UTF-8) natychmiast po wczytaniu lub po wykonaniu obliczeń. Dobrą praktyką jest wykonywać normalizację od razu w konstruktorze klasy reprezentującej te dane albo bezpośrednio w funkcji, która je wczytuje.

Rozdziel logikę kodu od danych

  • Danych niezwiązanych z logiką kodu nie należy umieszczać bezpośrednio wewnątrz instrukcji warunkowych; należy je wydzielić do osobnej tabeli
    ex) W porównaniu z kodem powyżej, zaleca się pisać jak w kodzie poniżej.

    1
    2
    3
    4
    5
    6
    
    string getMonthName(int month){
      if(month == 1) return "January";
      if(month == 2) return "February";
      ...
      if(month == 12) return "December";
    }
    
    1
    2
    3
    4
    5
    
    const string monthName[] = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"};
    
    string getMonthName(int month){
      return monthName[month-1];
    }
    
Ten wpis jest objęty licencją CC BY-NC 4.0 przez autora.