Код із запашком (код з душком, код, що погано пахнеангл.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 і помістити його в потрібний клас;
У коді з таким запашком щоразу при породженні підкласу, одного з класів, доводиться створювати підклас іншого класу[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].
Позбутися «недоречної близькості» можна за допомогою «Переміщення методу» (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].
Mantyla M. V., Vanhanen J., Lassenius C.Bad smells-humans as code critics// Software Maintenance, 2004. Proceedings. 20th IEEE International Conference on: журнал.—2004.— P. 399—408.— ISSN1063-6773.