15 порад з написання самодокументованого коду (на прикладі JavaScript)


Дізнайтесь більше про нові кар'єрні можливості в EchoUA. Цікаві проекти, ринкова оплата, гарний колектив. Надсилайте резюме та приєднуйтеся до нас.

Нікому не подобається знаходити в коді недоречні й даремні коментарі. Таку помилку легко допустити, не оновивши (чи не видаливши) коментар після зміни коду. Поганий коментар не поламає Вашого коду, але уявіть, що буде при відладці. Ви читаєте коментар, в ньому говориться одне, а код робить абсолютно інше. У результаті Ви просто втрачаєте час, з’ясовуючи, як це працює; у гіршому разі, це навіть може ввести Вас в оману.

Проте написання коду без коментарів також не варіант. У моєму більше як п’ятнадцятирічному досвіді програмування я жодного разу не зустрічав кодової бази, яка абсолютно не потребувала коментарів.

Проте у нас є способи зменшити потребу в коментарях. Ми можемо використати певну техніку програмування, щоб зробити свій код ясніше просто за рахунок можливостей мови. І це не лише спрощує розуміння нашого коду, але й допомагає поліпшити архітектуру всієї програми.

Такий код називається самодокументованим. Дозвольте мені показати Вам, як Ви можете застосувати цей підхід до написання коду прямо зараз. Хоча всі приклади в статті написані на JavaScript, більшість використовуваної техніки застосовна і в інших мовах.

Огляд техніки

Деякі програмісти розглядають коментарі як частину самодокументованого коду. У цій статті ми сфокусуємося тільки на коді. Коментарі важливі, але це велика тема, яку треба розглядати окремо.

Ми можемо розділити всю техніку написання самодокументованого коду на три категорії:

  • структурні, в якій структура коду або каталогів використовується для уточнення його призначення;
  • пов’язані з іменуванням, такі як іменування функцій і змінних;
  • пов’язані із синтаксисом, в яких для цього використовуються (чи не використовуються) окремі можливості мови.

Деякі з цієї техніки прості на папері, але складні у використанні, тому я покажу практичні приклади при описі кожної з них.

Структурні

Спочатку розглянемо структурну техніку, вони реалізуються шляхом спеціального розміщення коду для підвищення його ясності.

Перенесення коду у функцію

Це те саме, що і рефакторинг “виділення функції” – він означає, що ми беремо наявний код і переносимо його в нову функцію: ми “виділяємо” код в нову функцію.

Наприклад, спробуйте вгадати, що робить наступний рядок коду:

var width =  (value - 0.5)  * 16;

Не зовсім зрозуміло, і коментар тут явно б не завадив. Чи потрібне виділення функції, щоб зробити код самодокументованим?

var width = emToPixels (value);function emToPixels (ems) { return (ems - 0.5)  * 16;}

Єдина зміна полягає в тому, що обчислення виведені в окрему функцію. Назва функції описує те, що вона робить, і цей код не потребує подальших уточнень. Як додатковий бонус ми отримуємо корисну функцію-хелер, яку Ви можете використати де завгодно, тобто цей метод також дає змогу уникнути дублювання.

Заміна умовного виразу функцією

Умови з декількома операндами складно зрозуміти без коментарів. Ми можемо застосувати до них схожий метод для того, щоб вони були зрозумілішими:

if (!el.offsetWidth ||!el.offsetHeight) {}

Для чого використовується умова з цього прикладу?

function isVisible (el) { return el.offsetWidth && el.offsetHeight;}if (!isVisible (el)) {
}

Ми перенесли код у функцію, і код відразу став набагато очевиднішим.

Заміна виразу змінною

Заміна чого-небудь за допомогою змінної схожа на перенесення коду у функцію, тільки замість функції ми просто використовуємо змінну.

Розглянемо ще раз попередній приклад з умовою:

if (!el.offsetWidth ||!el.offsetHeight) {}

Замість виділення функції ми можемо зробити його ясніше за рахунок додавання змінної:

var isVisible = el.offsetWidth && el.offsetHeight;if (!isVisible) {}

Це може бути кращим варіантом, ніж виділення функції, наприклад, коли логіка, яку Ви хочете уточнити, дуже специфічна для конкретного алгоритму, використовуваного тільки в одному місці.

Найчастіше цей метод застосовується з математичними виразами:

return a * b +  (c / d);

Ми можемо зробити ясніше цей приклад за допомогою розділення обчислень:

var multiplier = a * b;var divisor = c / d;return multiplier + divisor;

У мене погано з математикою, тому уявіть на місці цього прикладу який-небудь корисніший алгоритм. У будь-якому разі, Ви можете переміщати комплексні вирази в змінні, щоб додати сенсу в складний для розуміння код.

Інтерфейси класів і модулів

Інтерфейси, тобто публічні методи і властивості класу або модуля, можуть працювати як документація для його використання.

Розглянемо приклад:

class Box { setState (state) { this.state = state; } getState (){ return this.state; }}

Цей клас може мати й інший код, зрозуміло. Я спеціально роблю приклад простим, щоб проілюструвати, як публічний інтерфейс виконуватиме роль документації.

Чи можете Ви сказати, як використовується цей клас? Можливо, і зможете, згаявши час, але відразу це не очевидно.

В обох функцій осмислені імена, що дозволяють зрозуміти, що вони роблять. Однак це не дає точного розуміння того, як Вам потрібно їх використати. Швидше за все, Вам потрібно буде вивчити більше коду або документацію, щоб розібратися.

А що коли поміняти цей код у такий спосіб:

class Box { open (){ this.state = 'open'; }
close (){ this.state = 'closed'; } isOpen (){ return this.state === 'open'; }}

Тепер застосування цього коду стало зрозуміліше, хіба не так? Зауважте, що ми тільки змінили публічний інтерфейс, внутрішня реалізація залишилася тією самою, що і в прикладі з використанням властивості this.state.

Тепер Ви з першого погляду можете сказати, як використовується клас Box. Це показує, що незважаючи на правильне найменування функцій в першій версії коду, весь клас у цілому примушував замислитися, а в результаті простих дій ми відразу отримуємо значний ефект. Завжди думайте про картину в цілому.

Групування коду

Групування різних частин коду може працювати як форма документації.

Наприклад, завжди треба прагнути оголошувати змінні якомога ближче до того місця, де вони використовуються, і групувати змінні, використовувані спільно.

Це можна застосовувати для індикації відносин між різними частинами коду, тому при внесенні в цей код змін, буде простіше зрозуміти, які ділянки коду також варто змінити.

Розглянемо приклад:

var foo = 1;blah () xyz ();bar (foo);baz (1337);quux (foo);

Ви відразу бачите, як часто використовується foo. Порівняйте з наступним варіантом:

var foo = 1;bar (foo);quux (foo);blah () xyz ();baz (1337);

Згрупувавши всі випадки використання foo, ми можемо відразу побачити, які частини коду, залежать від цієї змінної.

Використайте чисті функції

Чисті функції набагато простіше для розуміння, ніж функції, залежні від стану.

Що таке “чиста функція”? При виклику такої функції з однаковими параметрами, вона завжди робить однаковий результат, тому і називається “чиста” функція. Це означає, що у функції немає жодних побічних ефектів або залежності від стану (часу, властивостей об’єкта, Ajax).

Такий тип функцій простіше для розуміння, оскільки будь-які значення, що впливають на результат, передаються явно як аргументи. Вам не потрібно довго шукати, звідки щось з’явилося або що вплинуло на результат, оскільки все на видному місці.

Інша причина, через яку такі функції роблять код самодокументованим, це те, що Ви можете довіряти їх результату. Не дивлячись ні на що, функція завжди повертатиме результат, що ґрунтується винятково на переданих їй параметрах. На них також не впливають жодні зовнішні чинники, тому Ви можете бути впевненими, що вони не спричинять несподіваний побічний ефект.

Прикладом побічних ефектів є document.write (). Досвідчені розробники JavaScript знають, що цей метод не потрібно використовувати, але багато новачків спотикаються на ньому. Іноді цей метод працює добре, але за певних обставин він може повністю очистити сторінку. Ось такий  побічний ефект!

Для кращого розуміння поняття чистих функцій рекомендую прочитати статтю Functional Programming: Pure Functions.

Структура файлів і каталогів

При іменуванні файлів і каталогів потрібно наслідувати ту саму систему іменування в усьому проекті. Якщо явної системи іменування в проекті немає, то наслідуйте стандартну для використовуваної мови.

Наприклад, якщо Ви додаєте новий код до користувацького інтерфейсу, знайдіть схожу функціональність у Вашому проекті. Якщо такий код поміщається в src/ui/, то поміщайте туди і новий код.

Це спрощує знаходження коду і показує його призначення на основі того, що Ви вже знаєте про інші фрагменти коду в проекті. Розміщення всього коду користувацького інтерфейсу в одному місці ясно показує, які задачі він виконує.

Іменування

Є популярна цитата про дві найскладніші речі в програмуванні:

Є тільки дві дійсно складні речі: інвалідація кеша та іменування сутностей, – Філ Карлтон.

Подивимося, як ми можемо використати іменування, щоб зробити код самодокументованим.

Перейменування функції

Іменування функцій зазвичай не є надто складним, але все ж ознайомтесь із правилами, яким Ви можете слідувати:

  • Уникайте використання розмитих слів типу “обробляти” або “управляти”: handleLinks (), manageObjects ().
  • Використовуйте активні дієслова: cutGrass (), sendFile () – очевидно, що такі функції працюють.
  • Позначайте повернене значення: getMagicBullet (), readFile (). Це не обов’язково робити завжди, але допомагає там, де має сенс.
  • У мовах із сильною типізацією можна використати опис типу, що також допомагає позначати повернені значення.

Перейменування змінної

Для змінних є два основних правила:

  • Вказуйте одиниці виміру: якщо у Вас є числові параметри, то можете включити їх в назву як очікувані одиниці виміру. Наприклад, widthPx замість width показує, що використовується значення в пікселях.
  • Не використовуйте скорочення: назви типу a чи b непридатні ні для чого, крім лічильників у циклах.

Наслідуйте прийняту систему іменування

Намагайтеся наслідувати ту саму систему іменування у Вашому коді. Наприклад, якщо у Вас є об’єкт певного типу, використайте відповідне іменування:

var element = getElement ();

Не потрібно спонтанно використовувати інші терміни:

var node = getElement ();

Якщо Ви наслідуєте одну систему іменування по усій кодовій базі, то при подальшому ознайомленні з нею можна зробити припущення про значення речей виходячи з їх значення в інших місцях.

Використайте осмислені повідомлення про помилки

Undefined – це не об’єкт!

Давайте не наслідуватимемо приклад JavaScript і зробимо так, щоб помилки, повернені нашим кодом, містили осмислені повідомлення.

Що робить повідомлення про помилку осмисленим?

  • воно повинне описувати проблему, що сталася;
  • за змогою повинно включати значення змінних або інші дані, що спричинили помилку;
  • головне: повідомлення про помилку повинне допомагати нам виявити, що ж пішло не так, діючи як документація про належну роботу функції.

Синтаксис

Пов’язані із синтаксисом методики написання самодокументованого коду можуть бути специфічними для кожної конкретної мови. Наприклад, Ruby і Perl дозволяють Вам використати весь спектр дивних синтаксичних трюків, яких загалом потрібно уникати.

Розглянемо техніку, застосовну в JavaScript.

Не використовуйте синтаксичні трюки

Не використовуйте дивні трюки. Ось один зі способів заплутати людей:

imTricky && doMagic ();

Це еквівалентно наступному коду, що виглядає адекватніше:

if (imTricky) { doMagic ();}

Завжди віддавайте перевагу останньому варіанту. Синтаксичні трюки не дають жодних переваг.

Використайте іменовані константи, уникайте магічних чисел

Якщо є спеціальні значення у Вашому коді, такі як числа або рядки, використайте замість них константи. Навіть якщо це здається очевидним зараз, через місяць або два ніхто не згадає, чому було використано конкретно це число:

const MEANING_OF_LIFE = 42;

Якщо Ви не використовуєте ES6, можете для тих самих цілей застосувати var, працювати все буде так само.

Уникайте булевих прапорів

Булеві прапори можуть зробити код складним для розуміння. Розглянемо приклад:

myThing.setData ({ x: 1 }, true);

Що означає true? Це абсолютно не зрозуміло, доки Ви не пошукаєте в первинниках setData ().

Замість цього можете додати іншу функцію або перейменувати наявну:

myThing.mergeData ({ x: 1 });

Тепер Ви відразу визначите, що відбувається.

Використайте переваги можливостей мови

Ми навіть можемо використати окремі можливості вибраної мови, щоб краще передавати призначення певного коду.

Прикладом цього в JavaScript є методи ітерації по масиву:

var ids =[];for (var i = 0; i < things.length; i++) { ids.push (things[i].id);}

Цей код збирає список ID в новий масив. Проте щоб дізнатися про це, нам потрібно прочитати весь код циклу. Порівняємо це з методом map ():

var ids = things.map (function (thing) { return thing.id;});

У цьому випадку ми відразу дізнаємося, що створюється новий масив із чимось, оскільки саме це виходить після роботи map (). Це може бути вигідним, особливо, якщо використовується складніша логіка. Ось список функцій для ітерації на сайті MDN.

Інший приклад в JavaScript – це використання ключового слова const.

Часто Ви оголошуєте змінні, значення яких не збираєтеся змінювати. Поширеним прикладом цього є підключення модулів з CommonJS:

var async = require ('async');

Ми можемо зробити цю незмінність очевиднішою:

const async = require ('async');

Маєте додатковий бонус: якщо хто-небудь випадково спробує змінити це, ми отримаємо помилку.

Антипатерни

Маючи всі перераховані можливості, Ви можете зробити багато корисного. Проте є речі, про які Вам варто потурбуватися.

Витягання заради декількох коротких функцій

Деякі люди виступають за використання дуже маленьких функцій, Вам до снаги добитися цього, витягаючи все, що можна. Проте це може згубно вплинути на простий для розуміння код.

Уявіть, що Ви відлагоджуєте код, дивитеся на функцію a (), бачите, що вона використовує функцію b (), функцію, що використовує у свою чергу c () і т. д.

Хоча короткі функції прості для розуміння, якщо Ви використовуєте функцію тільки в одному місці, то Вам краще використати заміну виразу за допомогою змінної.

Не форсуйте

Зазвичай немає абсолютно правильного способу написання самодокументованого коду, тому якщо застосування якої-небудь методики не здається Вам вдалою ідеєю, не варто її застосовувати.

Висновок

Самодокументованість коду багато дає для поліпшення його підтримуваності. Кожен коментар – це додаткове навантаження, що потребує підтримки, тому зменшення кількості коментарів, там де це можливо, є корисною практикою.

Проте самодокументованість коду не замінить ні документації, ні коментарів. Наприклад, код обмежений у виразі намірів, тому докладні коментарі потрібні. Документація API також дуже важлива для бібліотек, оскільки читання коду не уявляється можливим, крім невеликих бібліотек.

Джерело: Прогрессор

Київ, Харків, Одеса, Дніпро, Запоріжжя, Кривий Ріг, Вінниця, Херсон, Черкаси, Житомир, Хмельницький, Чернівці, Рівне, Івано-Франківськ, Кременчук, Тернопіль, Луцьк, Ужгород, Кам'янець-Подільський, Стрий - за статистикою саме з цих міст програмісти найбільше переїжджають працювати до Львова. А Ви розглядаєте relocate?


Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *