Код із запашком (код з душком, код, що погано пахне англ. code smell) — термін, що позначає код з ознаками (запахами) проблем в системі. Був введений Кентом Беком[1] і використаний Мартіном Фаулером в його книзі «Рефакторинг. Поліпшення існуючого коду»[2], з тих пір активно використовується розробниками ПЗ[3].

Запахи коду — це ключові ознаки необхідності рефакторингу[4]. Існують запахи, специфічні як для парадигм програмування, так і для конкретних мов. Основною проблемою, з якою стикаються розробники при боротьбі з запахами коду, є те, що критерії своєчасності рефакторингу неможливо чітко формалізувати без апеляції до естетики і умовного почуття прекрасного. Запахи коду — це не набір чітких правил, а опис місць, на які потрібно звертати увагу при рефакторингу[5]. Вони легко виявляються, але при цьому не у всіх випадках свідчать про проблеми[1].

Код із запашком веде до розпаду коду, розробники повинні прагнути до усунення запашку шляхом застосування одноразового або багаторазового рефакторинга[6]. У процесі рефакторингу відбувається позбавлення від запахів коду, що забезпечує можливість подальшого розвитку додатків з тією ж або більшою швидкістю. Відсутність регулярного рефакторинга з плином часу здатна повністю паралізувати проект, тому запахи коду необхідно усувати на ранніх стадіях[4]. Існують інструменти пошуку та виправлення запахів коду[7], проте досвід показує, що ніякі системи показників не можуть змагатися з людською інтуїцією, основаною на інформації[8].

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

Дослідження 2015 року з використанням автоматизованого аналізу півмільйона комітів і ручного обстеження 9,164 комітів встановило, що:

  • Існує емпіричне підтвердження наслідків «технічного боргу», але існує лише непідтверджена інформація про те, як, коли і чому це відбувається.
  • «Здоровий глузд підказує, що невідкладні заходи з технічного обслуговування і тиск, а також встановлення пріоритетів на ринку для створення більш якісного коду — є причинами таких запахів».

Запахи коду ред.

Загальні запахи об'єктно-орієнтованого коду ред.

Дублювання коду ред.

 
Приклад триразового дублювання коду та позбавлення від цього запашку за допомогою виділення методу і наступних викликів коду створеного методу
Докладніше: Дублювання коду

Дублювання коду — це використання однакових структур коду в декількох місцях. Об'єднання цих структур дозволить поліпшити програмний код[8].

Приклади дублювання і методи їх усунення:

  • Один і той самий вираз, що присутній в двох методах одного і того ж класу, то необхідно застосувати «Відокремлення методу» (Extract Method) і викликати код створеного методу з обох точок;
  • Один і той самий вираз є в двох підкласах, що знаходяться на одному рівні: необхідно застосувати «Виділення методу» (Extract Method) для обох класів з подальшим «Підйомом поля» (Pull Up Field) або «Формуванням шаблону методу» (Form Template Method), якщо код схожий, але не збігається повністю. Якщо обидва методи роблять одне і те ж за допомогою різних алгоритмів, можна вибрати більш чіткий з цих алгоритмів і застосувати «Заміщення алгоритму» (Substitute Algorithm);
  • Дублювання коду знаходиться в двох різних класах: необхідно застосувати «Виділення класу» (Extract Class) в одному класі, а потім використовувати новий компонент в іншому[9].

Довгий метод ред.

Серед об'єктних програм найдовше живуть програми з короткими методами. Чим довша процедура, тим важче її зрозуміти. Якщо у методу гарна назва, то не потрібно дивитися його тіло[5].

Слід дотримуватися евристичного правила: якщо відчувається необхідність щось прокоментувати, потрібно написати метод. Навіть один рядок має сенс виділити в метод, якщо він потребує роз'яснень[10].

  • Для скорочення методу досить застосувати «Виділення методу» (Extract Method);
  • Якщо локальні змінні і параметри перешкоджають виділенню методу, можна застосувати «Заміну тимчасової змінної викликом методу» (Replace Temp with Query), «Введення граничного об'єкту» (Introduce Parametr Object) і «Збереження всього об'єкта» (Preserve Whole Object)[5];
  • Умовні оператори і цикли свідчать про можливість виділення в окремий метод. Для роботи з умовними виразами підходить «Декомпозиція умовних операторів» (Decompose Conditional). Для роботи з циклом — «Виділення методу» (Extract Method)[10].

Великий клас ред.

Коли клас реалізує занадто велику функціональність, варто подумати про винесення деякої частини коду в підклас. Це позбавить розробників від надмірної кількості наявних у класу атрибутів і дублювання коду[10].

  • Для зменшення класу використовується «Виділення класу» (Extract Class) або «Виділення підкласу» (Extract Subclass). При цьому слід звертати увагу на спільність у назві атрибутів і на те, чи використовує клас їх всі одночасно[5];
  • Якщо великий клас є класом GUI, може знадобитися переміщення його даних і поведінку в окремий об'єкт предметної області. При цьому може виявитися необхідним зберігати копії деяких даних у двох місцях і забезпечити їх узгодженість. «Дублювання видимих ​​даних» (Duplicate Observed Data) пропонує шлях, яким можна це здійснити[11].

Довгий список параметрів ред.

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

  • «Заміна параметра викликом методу» (Replace Parameter with Method) застосовується, коли можна отримати дані шляхом виклику методу об'єкта. Цей об'єкт може бути полем або іншим параметром.
  • «Збереження всього об'єкта» (Preserve Whole Object) дозволяє взяти групу даних, отриманих від об'єкта, і замінити їх цим об'єктом.
  • «Введення граничного об'єкта» (Introduce Parameter Object) застосовується, якщо є кілька елементів даних без логічного об'єкта[11].

Розбіжні модифікації ред.

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

  • Якщо набір методів необхідно змінювати кожного разу при внесенні певних модифікацій в код, то застосовується «Виділення класу» (Extract Class) (Наприклад, три методи змінюються кожного разу коли підключається нова база даних, а чотири — при додаванні фінансового інструменту)[5].

Стрільба дробом ред.

При виконанні будь-яких модифікацій доводиться вносити безліч дрібних змін у велике число класів. «Стрільба дробом» схожа на «Розбіжну модифікацію», але є її протилежністю. «Розбіжна модифікація» має сенс, коли є один клас, в якому проводиться багато різних змін, а «Стрільба дробом» — це одна зміна, що зачіпає багато класів[12].

  • Винести всі зміни в один клас дозволять «Переміщення методу» (Move Method) і «Переміщення поля» (Move Field);
  • Якщо немає відповідного класу, то слід створити новий клас;
  • Якщо це необхідно, слід скористатися «Вбудовуванням класу» (Inline Class)[5].

Заздрісні функції ред.

Метод звертається до даних іншого об'єкта частіше, ніж до власних даних[5].

  • «Переміщення методу» (Move Method) застосовується, якщо метод явно слід перевести в інше місце;
  • «Виділення методу» (Extract Method) застосовується до частини методу, якщо тільки ця частина звертається до даних іншого об'єкта;
  • Метод використовує функції декількох класів: визначається, в якому класі знаходиться найбільше даних, і метод поміщається в клас разом з цими даними, або за допомогою «Виділення методу» (Extract Method) метод розбивається на кілька частин і вони поміщаються в різні місця.

Фундаментальне практичне правило говорить: «Те, що змінюється одночасно, треба зберігати в одному місці». Дані та функції, що використовують ці дані, які зазвичай змінюються разом, але бувають винятки[13].

Групи даних ред.

Групи даних, що зустрічаються спільно, потрібно перетворювати в самостійний клас[13].

  • «Виділення методу» (Extract Method) використовується для полів;
  • «Введення граничного об'єкту» (Introduce Parameter Object) або «Збереження всього об'єкта» (Preserve Whole Object) для параметрів методів[14].

Хороша перевірка: видалити одне із значень даних і перевірити, чи збереже сенс решта. Якщо ні, то це вірна ознака того, що дані напрошуються на об'єднання їх в об'єкт[13].

Одержимість елементарними типами ред.

Проблема пов'язана з використанням елементарних типів замість маленьких об'єктів для невеликих завдань, таких як валюта, діапазони, спеціальні рядки для телефонних номерів тощо

  • «Заміна значення даних об'єктом» (Replace Data Value with Object);
  • «Заміна масиву об'єктом» (Replace Array with Object);
  • Якщо це код типу, то використовуйте «Заміну коду типу класом» (Replace Type Code with Class), «Заміну коду типу підкласами» (Replace Type Code with Subclasses) або «Заміну коду типу станом / стратегією» (Replace Type Code with State / Strategy)[5].

Оператори типу switch ред.

Одним з очевидних ознак об'єктно-орієнтованого коду служить порівняно рідкісне використання операторів типу switch (або case). Часто один і той же блок switch виявляється розкиданим по різних місцях програми. При додаванні в перемикач нового варіанту доводиться шукати всі ці блоки switch і модифікувати їх. Як правило, помітивши блок switch, слід подумати про поліморфізм[15].

  • Якщо switch перемикається по коду типу, то слід використовувати «Заміну коду типу підкласами» (Replace Type Code with Subclasses) або «Заміну коду типу станом / стратегією» (Replace Type Code with State / Strategy);
  • Може знадобитися «Виділення методу» (Extract Method) і «Переміщення методу» (Move Method) щоб ізолювати switch і помістити його в потрібний клас;
  • Після налаштування структури спадкування слід використовувати «Заміну умовного оператора поліморфізмом» (Replace Conditional with Polymorphism)[5].

Паралельні ієрархії успадкування ред.

У коді з таким запашком щоразу при породженні підкласу, одного з класів, доводиться створювати підклас іншого класу[15].

  • Загальна стратегія усунення дублювання полягає в тому, щоб змусити екземпляри однієї ієрархії посилатися на екземпляри іншого ієрархії, а потім прибрати ієрархію в класі за допомогою «Переміщення методу» (Move Method) і «Переміщення поля» (Move Field)[15].

Лінивий клас ред.

Клас, витрати на існування якого не окупаються виконуваними ним функціями, повинен бути ліквідований[15].

  • При наявності підкласів з недостатніми функціями спробуйте «Згортання ієрархії» (Collapse Hierarchy);
  • Майже даремні компоненти повинні підлягати «Вбудовуванню класу» (Inline Class)[15].

Теоретична спільність ред.

Цей випадок виникає, коли на певному етапі існування програми забезпечується набір механізмів, який, можливо, буде потрібен для деякої майбутньої функціональності. У підсумку програму стає важче розуміти і супроводжувати[16].

  • Для незадіяних абстрактних класів використовуйте «Згортання ієрархії» (Collapse Hierarhy);
  • Непотрібна делегація може бути вилучена за допомогою «Вбудовування класу» (Inline Class);
  • Методи з невикористовуваними параметрами повинні підлягати «Видаленню параметрів» (Remove Parameter)[5].

Тимчасове поле ред.

Тимчасові поля — це поля, які потрібні об'єкту лише за певних обставин. Такий стан речей важкий для розуміння, так як очікується, що об'єкту потрібні всі його поля[17].

  • Тимчасові поля і весь код, який працює з ними, слід помістити в окремий клас за допомогою «Виділення класу» (Extract Class);
  • Видалити умовно код, що використовується, можна за допомогою «Введення об'єкта Null» (Introduce Null Object) для створення альтернативного компонента[16].

Ланцюжок викликів ред.

Ланцюжок викликів з'являється тоді, коли клієнт запитує у одного об'єкта інший об'єкт, інший об'єкт запитує ще один об'єкт і т. д. Такі послідовності викликів означають, що клієнт пов'язаний з навігацією за структурою класів. Будь-які зміни проміжних зв'язків означають необхідність модифікації клієнта[16].

  • Для видалення ланцюжка викликів застосовується прийом «Приховування делегування» (Hide Delegate)[16].

Посередник ред.

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

  • Якщо велику частину методів клас делегує іншого класу, потрібно скористатися «Видаленням посередника» (Remove Middle Man)[18].

Недоречна близькість ред.

«Недоречна близькість» виникає тоді, коли класи частіше, ніж варто було б, занурені в закриті частини один одного[18].

  • Позбутися «недоречної близькості» можна за допомогою «Переміщення методу» (Move Method) й «Переміщення поля» (Move Field);
  • За можливістю слід вдатися до «Заміни двобічного зв'язку односпрямованим» (Change Bidirectional Association to Unidirectional), «Виділення класу» (Extract Class) або скористатися «Приховуванням делегування» (Hide Delegate)[18].

Альтернативні класи з різними інтерфейсами ред.

Два класи, в яких частина функціональності загальна, але методи, що реалізують її, мають різні параметри[19].

  • Застосовуйте «Перейменування методу» (Rename Method) до всіх методів, які виконують однакові дії, але з різними сигнатурами[18].

Неповнота бібліотечного класу ред.

Бібліотеки через деякий час перестають задовольняти вимогам користувачів. Природне рішення — змінити дещо в бібліотеках, але бібліотечні класи не змінювати. Слід використовувати методи рефакторінга, спеціально призначені для цієї мети[19].

  • Якщо треба додати пару методів, використовується «Запровадження зовнішнього методу» (Introduce Foreign Method);
  • Якщо треба серйозно поміняти поведінку класу, використовується «Введення локального розширення» (Introduce Local Extension)[19].

Класи даних ред.

Класи даних — це класи, які містять тільки поля і методи для доступу до них, це просто контейнери для даних, що використовуються іншими класами[19].

  • Слід застосувати «Інкапсуляцію поля» (Encapsulate Field) й «Інкапсуляцію колекції» (Encapsulate Collection)[5].

Відмова від наслідування ред.

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

  • Необхідно створити новий клас на одному рівні з нащадком, і за допомогою «Спуска метода» (Push Down Method) і «Спуска поля» (Push Down Field) виштовхнути в нього всі недіючі методи. Завдяки цьому в батьківському класі буде міститися тільки те, що використовується спільно[20].

Коментарі ред.

Часто коментарі грають роль «дезодоранту» коду, який з'являється в ньому лише тому, що код поганий. Відчувши потребу написати коментар, спробуйте змінити структуру коду так, щоб будь-які коментарі стали зайвими[20].

  • Якщо для пояснення дій блоку все ж потрібен коментар, спробуйте застосувати «Виділення методу» (Extract Method);
  • Якщо метод вже виділений, але як і раніше потрібен коментар для пояснення його дії, скористайтеся «Перейменування методу» (Rename Method);
  • Якщо потрібно викласти деякі правила, що стосуються необхідного стану системи, застосуйте «Введення затвердження» (Introduce Assertion)[20].

Див. також ред.

Примітки ред.

Література ред.

Посилання ред.

  • Рефакторинг коду. refactoring.guru. Архів оригіналу за 10 вересня 2016. Процитовано 28 липня 2016. 
  • Запахи коду. refactoring.guru. Архів оригіналу за 11 вересня 2016. Процитовано 28 липня 2016. 
  • CodeSmell. Martinfowler.com. Архів оригіналу за 18 квітня 2015. Процитовано 13 жовтня 2013. 
  • Запахи поганого коду. Vihv.org. Архів оригіналу за 9 грудня 2013. Процитовано 13 жовтня 2013. 
  • Code Smell. Cunningham & Cunningham, Inc. (c2.com). Архів оригіналу за 2 грудня 2013. Процитовано 23 листопада 2013.