Nitrooos

Myśli programisty

JSDoc, czyli zacznij dokumentować swój kod!

nitrooos

Jak często zdarzało się Wam tracić czas na zrozumienie kodu napisanego tak niejasno, że jego jakość ociera się o sabotaż? Czy nie byłoby wspaniale mieć chociaż opis jego interfejsu, wiedzieć co pożera na wejściu oraz co oznaczają dane produkowane przez niego na wyjściu? To właśnie dla ułatwienia życia programistom w takich (i innych!) przypadkach powstały różne sposoby umieszczania w kodzie (najczęściej w formie komentarze) specjalnych adnotacji, służących opisowi działania danego kawałka kodu (funkcji, modułu, klasy itd). Jednym z takich narzędzi jest dostępny dla JavaScript program jsdoc.

Czym jest jsdoc?

jsdoc to program, który możemy zainstalować używając managera paczek npm za pomocą prostej komendy:

    npm install jsdoc --save-dev

Potrafi wykrywać on specjalne adnotacje w kodzie JavaScript, umieszczone jako część komentarza (pomiędzy znakami /** tutaj zawartość do zinterpretowania przez jsdoc */). Adnotacje te opisują np. znaczenie i typy poszczególnych parametrów wywołania funkcji, jej wartość i typ, opisują zastosowanie i zakres odpowiedzialności klasy itp. Wachlarz możliwości jest naprawdę szeroki (co, choć częściowo, postaram się pokazać w tym wpisie).

Adnotacje w tym kształcie już okazują się przydatne dla programistów, którzy będą pracowali nad naszym kodem w przyszłości, ponieważ lepiej i szybciej zrozumieją intencje autora, to jednak nie wszystko! Narzędzia tego możemy użyć do generowania dokumentacji kodu na podstawie samych adnotacji i treści w nich zawartych. Możemy robić to w sposób zautomatyzowany, w formacie gotowych do przeglądania stron HTML. Jakie to ma zastosowanie? Wyobraźmy sobie, że pracujemy nad projektem, z którego potencjalnie (od strony programistycznej) korzystać może wiele osób/organizacji. Interesować je będzie możliwość wykorzystania naszego kodu, jednak bez wgłębiania się w szczegóły implementacji. W takiej sytuacji udostępnienie im dokumentacji jest zbawienne, a dzięki jsdoc możemy wygenerować ją w każdej chwili.

Przejdźmy do konkretów!

Zaczniemy od stworzenia kodu składającego się z deklaracji klasy wraz z jej metodami oraz prostego modułu i typu. Poniższy kod jest wyłącznie przykładowy, nie należy wyciągać na jego podstawie wniosków dotyczących spojrzenia autora na świat ;) Zadeklarowana zostanie prosta klasa “Manager” z kilkoma metodami, umożliwiającymi obiektom tej klasy sprawne zarządzanie projektami w organizacji:

class Manager {
  _projects = [];
  _name;

  constructor(name, projects) { ... }
  reduceDeadline(project, fireMalcontents = false) { ... }
  cutBudgetOnClientRequest(project, reducedBudget) { ... }
  delayedProjects() { ... }
}

Oczywiście w przypadku tak prostej klasy możemy domyśleć się jak (mniej więcej) powinny zachowywać się zadeklarowane metody. Nie wiemy jednak dokładnie w jaki sposób zmieniają one stan obiektu oraz czy zwracają jakąś wartość. Dodatkowo same nazwy mogą okazać się mylące lub nieścisłe (słowo “Manager” oznaczać może przecież zarządców zupełnie w zupełnie różnych kontekstach). Dodatkowo, w prawdziwych projektach, kod potrafi być znacznie bardziej złożony.

Drugim kawałkiem do udokumentowania będzie prosty moduł, eksportujący jedną funkcję, służącą do usuwania duplikatów z tablicy obiektów:

const unique = (list) =>
  list.filter((element, index) => list.indexOf(element) === index);

export {
  unique
}

Tutaj sprawa jest jeszcze prostsza, ale nawet mając podaną nazwę funkcji trzeba się na chwilę wczytać w jej implementację, aby zrozumieć jej sposób działania, typ parametru i wartość.

Dodajmy pierwsze adnotacje jsdoc!

Zaczniemy od klasy Manager, poprzez dodanie nad jej definicją jednolinijkowej adnotacji:

/** Class representing a standard manager in your company */

Dzięki niej wiemy już jaki tym managera miał na myśli programista. Następnie opiszmy pola klasy: _projects i _name:

/**
  * List of all projects which manager is responsible for
  * @private
  * @type {Project[]}
  */
_projects = [];

/**
  * Name of the manager
  * @private
  */
_name;

Pierwsza adnotacja jaka się pojawia to @private i oznacza ona składową prywatną klasy. Jest to zarówno informacja dla programisty używającego tej klasy, jak i dla programu generującego dokumentację, o czym się wkrótce przekonamy. Drugą adnotacją jest @type. Dzięki niej wiemy, że typem składowej _projects jest tablica obiektów typu Project. Czym jednak jest Project? jsdoc oczywiście również nie ma pojęcia, dlatego jest on kolejną rzeczą jaką musimy zdefiniować.

Definiowanie własnego typu na potrzeby adnotacji jsdoc

/**
  * @typedef Project
  * @prop {string} name Name of the project
  * @prop {number} budget Current budget for the project
  * @prop {Date} deadline Current deadline of the project
  */

Adnotacja @typedef służy do definiowania typów użytkownika, jak widać przyjmuje ona jeden argument, którym jest nazwa typu. Z kolei za pomocą adnotacji @prop możemy definiować jakie pola (wraz z ich typami!) wchodzą w skład nowego typu. Po nazwie adnotacji następuje typ pola w nawiasach klamrowych, np . { string }, potem nazwa pola i jego krótki opis. Od tej pory możemy używać nowo zdefiniowanego typu w kolejnych adnotacjach, a jsdoc prawidłowo go rozpozna i wygeneruje w dokumentacji link do definicji tego typu. Nieźle! Prawdziwa siła tego narzędzia ujawnia się jednak dopiero przy opisywaniu funkcji i metod, dlatego też opiszmy konstruktor klasy Manager.

Opisujemy konstruktor oraz metody klasy Manager

/**
  * Creates a manager
  * @param {string} name Name of the manager
  * @param {Project[]} projects List of projects
  */
constructor(name, projects) { ... }

Pierwsza linia adnotacji pełni zawsze domyślnie rolę krótkiego opisu. Możemy wprowadzić go także w wielu liniach za pomocą adnotacji @description. Następnie wskazujemy parametry konstruktora, wraz z ich typami, nazwami i krótkimi opisami, analogicznie jak ma to miejsce przy definiowaniu typu. Tym razem jednak definiujemy parametry metody, a nie pola typu, dlatego używamy adnotacji @param. Kolejne metody mogą zostać opisane analogicznie:

/**
 * Reduce deadline on project when DEAR CLIENT requests it
 * @param {Project} project Project to reduce deadline
 * @param {boolean} fireMalcontents Flag indicating  whether to fire
 *  dissatisfied by this decision or not
 */
  reduceDeadline(project, fireMalcontents = false) { ... }

/**
 * Cut budget on project
 * @param {Project} project Project to cut budget
 * @param {number} reducedBudget New, reduced budget value
 */
  cutBudgetOnClientRequest(project, reducedBudget) { ... }

/**
 * Return all delayed projects under this  manager
 * @return {Project[]} List of delayed projects
 */
  delayedProjects() { ... }

Zwróć uwagę, że możemy korzystać zarówno z typów użytkownika, zdefiniowanych wcześniej, jak i z typów wbudowanych w JavaScript (string, boolean, Date itp). Jedyną nową adnotacją jest bardzo użyteczna @return, służąca do definiowania wartości funkcji/metody, wraz z jej typem i opisem.

To nie było takie trudne! Dlatego teraz zajmiemy się opisem modułu application/utils, eksportującego prostą funkcję.

Dokumentowanie modułu application/utils

/**
 * @module application/utils
 * @description
 * This is a module with some helper functions
 */

Taka adnotacja (@module) definiuje moduł w aplikacji, to znaczy jednostkę kodu eksportującą pewne wartości za pomocą instrukcji export. Jedyną funkcję w ramach tego modułu (dla przypomnienia, jest to funkcja unique, usuwająca duplikaty z listy) można zdefiniować taki opis:

/**
 * Removes duplicates from list
 * @function
 * @example
 * -> unique([1, 1, 2, 3, 2, 3, 4, 5, 5])
 * -> [1, 2, 3, 4, 5]
 * @param {any[]} list List of elements
 * @return {any[]} Unique  elements of original list
 */

Dodajemy tutaj prosty opis, wskazujemy, że jest to funkcja (@function), a następnie pokazujemy przykład użycia. Bywa to szczególnie przydatne w przypadku, gdy w samym opisie trudno zwięźle zawrzeć sposób działania funkcji. Adnotacja @example tworzy w dokumentacji fragment zapisany fontem o stałej szerokości. Właśnie dlatego idealnie nadaje się do podania przykładów wywołania funkcji z różnymi argumentami oraz ich wartości dla tych przypadków. Następnie mamy znane już adnotacje @param i @return. Tym razem jako typ podajemy tablicę obiektów dowolnego typu, ponieważ funkcja unique nie ogranicza się do filtrowania napisów czy liczb, stąd jako typ pojawia się any[].

Świetnie! Ale w jaki sposób wygenerować dokumentację?

Najprostszym sposobem na użycie jsdoc’a jest dodanie odpowiedniego wpisu w pliku package.json projektu:

"scripts": {
  ...
  "jsdoc": "jsdoc -d docs/frontend -r src/",
  ...
}

Od tej pory dokumentację można wygenerować wydając polecenie:

    npm run jsdoc

w konsoli. Poprzez opcję -d (bądź równoważną --destination) możemy określić katalog, w którym przechowywana będzie wygenerowana dokumentacja. Opcja -r (równoważny --recurse) oznacza, że tworząc dokumentację przeszukiwane będą wszystkie pliki wewnątrz katalogu źródłowego i jego podkatalogów. Szczegółowość generowanego dokumentu kontrolować można opcją -p (--private), kontrolującą czy w dokumentacji zamieszczać także symbole opatrzone adnotacją @private.

A tak wygląda przykładowa, końcowa dokumentacja dla naszych przykładów:

 Dokumentacja klasy Manager wygenerowana przez jsdoc Klasa “Manager”

 Dokumentacja modułu application/utils wygenerowana przez jsdoc Moduł “application/utils” z funkcją “unique”

Jak widać, wygenerowany dokument jest czytelny, i w prosty sposób możemy zobaczyć powiązanie pomiędzy informacjami zamieszczonymi w adnotacjach a tym, co widzimy w dokumentacji. Po prawej strony znajduje się nawigowane menu, dzięki któremu można przemieszczać się pomiędzy dokumentacją poszczególnych klas i modułów, sama strona dokumentacji po prawej zawiera podsumowanie zadeklarowanych funkcji/metod/typów, wraz z ich parametrami i opisem. Dzięki linkom zawsze można przeskoczyć do odpowiedniego kawałka kodu, który opisuje dokumentacja.

Źródła: