Komentarze w kodzie - okładka

Komentarze w kodzie

Tematowi komentarzy w kodzie poświęciłem kiedyś wpis, w czasach gdy blog nie istniał w obecnej formie i pod obecną nazwą. Niestety, nie posiadam treści zawartej w tamtym wpisie, ale pamiętam, że wywołał on dyskusję, co pozwoliło na wymianę poglądów i punktów widzenia. Temat wydaje mi się na tyle ciekawy, że warto do niego wrócić. Ponowne podejście do tego samego tematu wydaje mi się być ciekawym eksperymentem z punktu widzenia twórcy treści. Być może również i ten wpis wywoła ciekawą dyskusję i wymianę poglądów.

Ten artykuł będzie przydatny głównie dla tych, którzy stawiają swoje pierwsze kroki w świecie programowania. Jednak jeśli masz już nieco doświadczenia, to i tak zachęcam do przeczytania 🙂

Komentarze w kodzie – potrzebne czy nie?

Można tu odpowiedzieć stwierdzeniem, które jest bardzo popularne w branży IT – “to zależy”. Zależy oczywiście od konkretnego przypadku. Czasami komentarze są bezużytecznym szumem, a czasami mogą komuś oszczędzić godziny a nawet dni pracy.

Komentarze bezużyteczne

Komentowanie nieczytelnego kodu

Z kodem bardzo często jest jak z żartami – jeśli musisz go tłumaczyć to znaczy, że coś robisz źle. W wielu przypadkach kod może (a wręcz powinien) zostać zrefakotryzowany w celu jego uproszczenia i lepszego zrozumienia. Spójrz na poniższy przykład:


const employees = [
  {
    name: 'Kenny',
    salary: 2000
  },
  {
    name: 'Stan',
    salary: 3500
  },
  {
    name: 'Kyle',
    salary: 4250
  },
  {
    name: 'Jimmy',
    salary: 1800
  }
];

function a( arg ) {
  const x = arg.reduce((x,y) => ( x.salary || x ) + y.salary )/arg.length;
  
  return arg.map( y => ({
    name: y.name,
    s: Math.abs( y.salary - x )
  }) );
}

Czy bez dogłębnej analizy jesteś w stanie wytłumaczyć co ten kod robi? Funkcja a() przyjmuje jako argument listę pracowników i zwraca raport zawierający imię pracownika oraz odchylenie pensji pracownika od średniej. Teoretycznym rozwiązaniem problemu mogłoby być właśnie dodanie komentarzy. Efekt przedstawiam poniżej:


// Returns employees and salary deviation
function a( arg ) {
  // Get average salary
  const x = arg.reduce( (x,y) => ( x.salary || x ) + y.salary )/arg.length;
  
  return arg.map( y => ( {
    name: y.name,
    d: Math.abs( y.salary - x ) // Salary deviation
  } ) );
}

W teorii problem został rozwiązany. Jednak w praktyce nadal mamy paskudny kod, tylko z komentarzami. Utrzymanie i rozbudowa tego kodu w przyszłości będzie problematyczna. Zdecydowanie lepszym pomysłem jest dokonanie refaktoryzacji. Nazwy wykorzystanych zmiennych oraz samej funkcji całkowicie mogą zastąpić wykorzystane komentarze. Efekt refaktoryzacji przedstawiam poniżej:


function getSalaryDeviationReport( employees ) {
  const avg = employees.reduce(
    ( prev, curr ) => ( prev.salary || prev ) + curr.salary
  ) / employees.length;
  
  return employees.map( employee => ({
    name: employee.name,
    salaryDeviation: Math.abs( employee.salary - avg )
  }) ); 
}

Moim zdaniem, tak zrefaktoryzowany kod wygląda zdecydowanie lepiej i czytelniej niż kod z komentarzami.

Zakomentowane fragmenty kodu

Zakomentowane fragmenty kodu w zdecydowanej większości przypadków są po prostu zbędne. Argumenty pokroju “może kiedyś się przyda” uważam za całkowicie nietrafione. W obecnych czasach standardem jest korzystanie z systemów kontroli wersji (np. Git), nawet dla małych czy prywatnych projektów. Dzięki systemom kontroli wersji mamy wgląd do historii zmian w projekcie, co daje możliwość cofnięcia się do dowolnej zmiany i odzyskania usuniętego fragmentu kodu.

Tłumaczenie oczywistości

Komentarze tłumaczące oczywistości jestem w stanie zrozumieć jeśli pisze je osoba, która dopiero uczy się podstaw. Na samym początku przygody z programowaniem nawet tak podstawowe konstrukcje jak warunki czy pętle mogą wydawać się skomplikowane. Wtedy pozostawienie komentarza objaśniającego może być nawet wskazane. Niemniej jednak wraz z postępem i rozwojem umiejętności liczba takich komentarzy powinna dążyć do zera.

Komentarze użyteczne

Sytuacji, w których dodanie komentarza do kodu jest użyteczne okazuje się być całkiem sporo. W końcu, gdyby komentarze nie miały zastosowania, to taki mechanizm nigdy by nie powstał 🙂

JSDoc

JSDoc to API pozwalające na przygotowanie dokumentacji dla kodu JavaScript. W JSDoc komentarze zazwyczaj dodawane są bezpośrednio nad dokumentowanym kodem gdzie opisywany jest cel biznesowy dokumentowanenego kodu, zwracane wartości oraz parametry wejściowe wraz z ich typami w przypadku dokumentowania metod. JSDoc pozwala także na wygenerowanie przygotowanej dokumentacji w formie kodu HTML. Dokumentacja z użyciem JSDoc dla kodu z wcześniejszej części wpisu wygląda następująco:


/**
 * @typedef {Object} Employee
 * @property {string} name
 * @property {number} salary
 */

/**
 * @typedef {Object} EmployeeResult
 * @property {string} name
 * @property {number} salaryDeviation
 */

/**
 * @param {Array.<Employee>} employees - A list of employees
 * @returns {Array.<EmployeeResult>}
 */
function getSalaryDeviationReport( employees ) {}

Do udokumentowania kodu zostały wykorzystane trzy komentarze. Pierwszy z nich posłużył do zdefiniowania typu Employee, drugi definiuje strutkurę zwracanych obiektów, zaś trzeci z nich dokumentuje metodę getSalaryDeviationReport.

Na szczególną uwagę zasługuje tag @deprecated. Owy tag służy do oznaczania metod w kodzie jako nieaktualne. Zastępowanie jednego rozwiązania innym z jednoczesnym zachowaniem wstecznej kompatybilności pozwala użytkownikom na stopniowe dostosowywanie utrzymywanego kodu do nowego podejścia. Tag @deprecated jest interpretowany przez takie środowiska programistyczne jak VSCode czy WebStorm poprzez naniesienie stylów, np. w postaci skreślenia i zmiany koloru na elementy oznaczone tym tagiem. Jest to szczególnie przydatne w kontekście wykorzystywania innych podpowiedzi oferowanych przez IDE.

Komentowanie rzeczy nieoczywistych

Może zdarzyć się tak, że w Twoim kodzie znajdzie się jakaś konstrukcja, która pozornie wydaje się nie mieć sensu lub jej usunięcie bądź zmiana spowoduje duże szkody w systemie. Tyczy się to również nietypowych zabiegów mających na celu poprawę wydajności kodu.

Przykładem nieoczywistej sytuacji może być wykorzystanie zewnętrznych komponentów, które zawierają drobne niedociągnięcia. Załóżmy, że korzystasz z zewnętrznej biblioteki, która służy do generowania tokenów służących do uwierzytelniania się w aplikacji. Jednym z parametrów niezbędnych do wygenerowania tokenu jest czas życia tokena, który jest przekazywany w formie liczby. Niemniej jednak z powodu błędu programisty odpowiedzialnego za powstanie biblioteki do generowania tokenów, zamiast liczby możliwe jest również przekazanie wartości typu string, co powoduje wygenerowanie tokena z niepoprawnym terminem ważności. W wykorzystanej bibliotece czas życia tokena określany jest na podstawie wartości timestamp + lifetimeInSeconds. Jeśli w miejsce lifetimeInSeconds zostanie przekazany string to wartości zostaną złączone, a nie dodane do siebie (konkatenacja stringów), i w rezultacie powstanie token z o wiele za długim czasem życia.

W przypadku wykrycia takiego błędu słusznym jest upewnienie się, że w opisanym przypadku przekazana wartość zawsze będzie liczbą. Pozostawienie komentarza w kodzie z kilkoma słowami objaśnienia jest zatem jak najbardziej uzasadnione i wskazane. Ponadto, po zgłoszeniu problemu (np. w repozytorium z kodem biblioteki) warto w kodzie zostawic link np. do issue na GitHubie, w którym opisane zostały wszystkie szczegóły. Opisana przeze mnie sytuacja w kodzie mogłaby wyglądać następująco:


import { generateToken } from 'some-lib';

const expirationTime = getExpirationTime();

// In a case when the expirationTime is a string the expiration date is invalid.
// For more details see: https://github.com/some-dev/some-lib/issues/1512
const token = generateToken( parseInt( expirationTime ) );

Innym przypadkiem, w którym komentarz wręcz można wymusić jest stosowanie klauzuli @ts-ignore gdy kod jest napisany w TypeScript. Ta klauzula powinna być wykorzystywana tak rzadko jak to tylko możliwe, dlatego każe jej użycie powinno być opatrzone komentarzem. Co ciekawe, takie zachowanie można wymusić za pomocą ESLinta poprzez włączenie reguły ban-ts-comment:


'@typescript-eslint/ban-ts-comment': [
  'error',
  {
    'ts-ignore': 'allow-with-description'
    minimumDescriptionLength: 10
  }
]

Właściwość minimumDescriptionLength pozwala uniknąć krótkich objaśnień pokroju bug, wontfix, itd.

Opisywanie wyrażeń regularnych

O wyrażeniach regularnych na moim blogu już pisałem w artykule Wyrażenia regularne w JavaScript. O ile proste wyrażenia regularne są możliwe do zrozumienia w krótkim czasie, tak te bardziej złożone zdecydowanie warto opatrzeć komentarzem. Do komentarza objaśniającego warto również dołączyć przykłady. Oczywiście istnieją rozwiązania objaśniające wyrażenia regularne, jednak przeczytanie komentarza z przykładem jest często o wiele szybsze i wygodniejsze.

Warto mieć także na uwadze, że komentarz oddaje także intencje programisty tworzącego wyrażenie. Do wyrażeń regularnych mogą wkraść się błędy, a dzięki komentarzowi z intencją programisty proces debugowania prawdopodobnie będzie szybszy i bardziej efektywny.

Podsumowanie

Przykładów dobrych i złych komentarzy można znaleźć zdecydowanie więcej. Zachęcam do poczynienia samodzielnej refleksji nad tematem komentarzy w kodzie. Zachęcam także do zapoznania się ze źródłami i materiałami dodatkowymi oraz do pozostawienia oceny i… komentarza 🙂

Źródła i materiały dodatkowe