Будь як кіт, вилижи свій код: 8 хороших практик по підвищенню якості коду


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

Програмісти-початківці зазвичай проводять рік або два, не звертаючи уваги на правила “хорошого коду”. Звичайно, вони можуть чути такі вирази, як “елегантний” або “чистий” код, але не завжди здатні дати їм визначення. Це цілком нормально. Для програміста без досвіду існує тільки один важливий параметр – робочий код.

 

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

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

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

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

Терміни

Спершу дамо декілька визначень:

  • стан (state) – це дані, що зберігаються в пам’яті додатка. Кожна призначена змінна є частиною стану додатку;
  • рефакторинг (refactor) – зміна коду програми без зміни її поведінки (принаймні, видимої для користувача). Мета рефакторинга – спростити код і зробити його простіше для читання і розуміння.

1. Розділення проблем

Давайте порівняємо створення коду з процесом готування. Простий рецепт припускає, що кожен подальший крок буде здійснений після завершення попереднього, як тільки усі кроки будуть виконані – страва буде готове. Але якщо узяти, наприклад, складний рецепт, коли на плиті одночасно стоять дві киплячі каструлі, в мікрохвильовій печі обертається тарілка, у вас є три види овочів, млини з різними спеціями (і ви не пам’ятаєте, що вже додали), це викличе справжній стрес.

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

Якщо знати, що на кухні буде декілька кухарів, тоді варто розділити рецепт на декілька субрецептів. Тоді кожен з кухарів працюватиме тільки над своєю частиною рецепту з мінімальною взаємодією. Один – готує киплячу воду для макаронів, другий – нарізує та готує овочі, третій – подрібнює сир. Четвертий – робить соус. Завдяки чітко визначеним завданням кожен з них робить свою роботу.

Гірший варіант написання коду дорівнює найпростішому рецепту, коли кожен рядок коду визначений в одному і тому ж просторі і нанизаний зверху вниз. Щоб зрозуміти або змінити такий код, вам треба буде переглянути його багато разів. Адже змінна з другого рядка може впливати на операцію в рядку 832, і єдиний спосіб це зрозуміти – переглянути увесь код.

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

Кращий з варіантів – це розділення рецепту на субрецепти. У коді їх називають “модулями” або “класами”. Кожен модуль пов’язаний з певною операцією або частиною даних. Таким чином, людина, яка займається овочами, не турбуватиметься про інгредієнти для соусу, а людина, що готує макарони, не хвилюватиметься про терку для сиру. Їх проблеми розділені.

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

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

2. Глобальні змінні

Візьмемо, наприклад, змінну username. При створенні форми авторизації в додатку ви раптом вирішили, що ім’я користувача використовуватиметься в декількох місцях додатка, наприклад, на сторінці авторизації і в налаштуваннях. Тому зробили цю змінну глобальною. У Python вона позначається як global, а в JavaScript ця властивість об’єкту window. На перший погляд, це хороше рішення. Тепер у будь-якому місці, де вам треба використати змінну username, ви легко можете це зробити. Але чому не зробити глобальними усі змінні?

Уявимо, що щось пішло не так. Знайшовся баг в коді, який пов’язаний зі змінною username. Навіть з використанням інструментів пошуку у вашій IDE знайти цю змінну буде складно. При пошуку змінної username ви отримаєте сотні, а то і тисячі результатів. Деякі результати пошуку будуть глобальною змінною, яку ви встановили на початку проекту, деякі – іншими змінними з ім’ям username, деякі будуть просто словом username, яке використовувалося в коментарі, імені класу, імені методу і т. д. Можна буде встановити додаткові фільтри для пошуку, але все таки відладка займе багато часу.

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

Таке використання коду полегшить ваше життя. Потрібні дані завжди знаходитимуться тільки в одному місці. І якщо вам знадобиться відстежити, коли частина коду або даних була змінена, ви завжди зможете використати getter чи setter.

3. Не повторюйте себе

Давайте поговоримо про стосунки.

Бути в стосунках – це добре, адже ви завжди знаходите підтримку у своєму партнерові. Часто буває, що доводиться розповідати історію вашого знайомства. І рідко така історія буває такою простою, як: “Ми зав’язали розмову в продуктовому магазині та одружилися наступного дня”. Таким чином, вам доводиться розповідати одну і ту ж історію кожного разу, кілька разів в тиждень, що досить стомливо.

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

Знаходячись в повному подиві, ви створюєте веб-сторінку “Як ми зустрілися”, а потім відвідуєте офіс FedEx, щоб роздрукувати тисячу візитних карток з адресою цього сайту. Після цього відправляєте лист з посиланням усім, хто чув стару версію. І тепер, коли хтось запитує, як ви зустріли свого партнера, ви просто даєте йому візитну картку. Якщо історія коли-небудь зміниться, ви можете оновити веб-сторінку, тоді усі дізнаються нові подробиці вашого знайомства.

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

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

4. Приховання складності

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

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

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

Коли ви створюєте клас або метод, перше, що вам треба створити, – це інтерфейс, тобто частина кода, яку повинен знати інший шматок коду (що викликає) для використання цього класу або методу. Для методу це також називається сигнатурою. Кожного разу під час перегляду функції або класу в API документації (наприклад, на MDN або jquery.com) ви бачите інтерфейс – все, що вам треба знати для його використання без коду, що міститься в нім.

Інтерфейс має бути простим, але виразним. Все повинно бути зрозуміло без слів, зухвала функція не повинна знати порядок виконання подій або даних, за які вона не відповідає.

Поганий інтерфейс:

function addTwoNumbersTogether (number1, number2, memoizedResults, globalContext, sumElement, addFn)  // returns an array

Хороший інтерфейс:

function addTwoNumbersTogether (number1, number2)  // returns a number

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

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

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

У JavaScript змінні, оголошені з допомогою var, let чи const, автоматично стають приватними у функції, в якій вони оголошені, до тих пір, поки ви не повертаєте або не призначаєте їх об’єкту. У багатьох інших мовах використовується ключове слово private. Воно повинне стати вашим кращим другом. Змінні варто робити публічними тільки у випадках крайньої необхідності.

5. Близькість

Треба оголошувати змінні як можна ближче до місця їх використання.

Інстинкт програміста до організації коду може працювати навіть проти самого програміста. Адже можна подумати, що організований код виглядає так:

function (){ var a = getA (), b = getB (), c = getC (), d = getD (); doSomething (b); doAnotherThing (a); doOtherStuff (c); finishUp (d);}

getA () і інші подібні функції не визначені в цьому сегменті коду, але уявіть, що вони повертають корисні значення.

Дивлячись на цей невеликий метод, можна подумати, що код добре організований і легко читається. Але це не так. d, з якоїсь причини, оголошується в рядку 4, хоча вона не використовується до рядка 9, а це означає, що треба прочитати увесь метод, щоб переконатися, що змінна більше ніде не використовується.

Добре організований метод виглядатиме так:

function (){ var b = getB (); doSomething (b); var a = getA (); doAnotherThing (a); var c = getC ();
doOtherStuff (c); var d = getD (); finishUp (d);}

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

Звичайно, у більшості випадків ситуація не настільки проста. Що, якщо b треба передати два методи: doSomething () і doOtherStuff ()? В цьому випадку вашим завданням буде зважити параметри і переконатися, що метод все ще простий для читання (в першу чергу, залишаючи його невеликим). У будь-якому випадку треба переконатися в тому, що b не оголошується раніше моменту її використання і використовується в найближчому сегменті коду.

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

6. Багаторівневе вкладення коду (Deep nesting)

JavaScript відомий своєю складною ситуацією, відомою як “callback hell” :

Бачите )};,що повторюється починаючи з середини коду? Це і є горезвісне пекло зворотного виклику (callback hell). Цього можна уникнути, але це історія для ще однієї статті.

Але давайте розглянемо щось, що називається if hell.

callApi ().then (function (result){ try {
if (result.status === 0) { model.apiCall.success = true; if (result.data.items.length > 0) { model.apiCall.numOfItems = result.data.items.length; if (isValid (result.data) { model.apiCall.result = result.data; } } } } catch (e) { // suppress errors }});

Підрахуйте пари фігурних дужок { }. Шість з яких вкладені. Це надто багато. Цей блок коду важко читати частково через те, що код ось-ось закриє праву сторону екрану, а програмісти ненавидять горизонтальну прокрутку, адже доведеться прочитати усі умови if, щоб з’ясувати, як ви потрапили на рядок 10.

Тепер подивимося на це:

callApi ().then (function (result){ if (result.status !== 0){ return; } model.apiCall.success = true;
if (result.data.items.length <= 0) { return; } model.apiCall.numOfItems = result.data.items.length; if (!isValid (result.data)) { return; } model.apiCall.result = result.data;});

Так набагато краще. Тепер це нормальний шлях коду, і тільки в деяких ситуаціях код відхиляється у блок if. Процес відладки спрощується в рази. І якщо ми хочемо додати додатковий код для обробки умов помилки, можна легко написати пару рядків усередині цих блоків if (а уявіть, що блоки if у початковому коді також мали б блоки else, жах).

Окрім цього, були видалені блоки try - catch, тому що не треба пригнічувати помилки. Помилки – ваш друг, і без них додаток стане сміттям.

7. Чисті функції

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

Це чиста функція:

function getSumOfSquares (number1, number2) { return (number1 * number1)  +  (number2 * number2);}

А це нечиста функція:

function getSumOfExponents (number1, number2) { scope.sum = Math.pow (number1, scope.exp)  + Math.pow (number2, scope.exp);}

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

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

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

8. Модульне тестування

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

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

Висновок

Гарний код приносить задоволення від роботи з ним, його підтримка не викликає у вас особливих проблем. Поганий код – тортури для душі. Намагайтеся писати добрий код.

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

Переклад статті “Steps to better code”

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


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

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