Запахи коду
Код із запашком (код з душком, код, що погано пахне англ. 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 (або 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].
- ↑ а б Martin Fowler_CodeSmell.
- ↑ Martin Fowler_Refactoring.
- ↑ Програміст про програмування.
- ↑ а б Vigorous Hive_CodeSmell.
- ↑ а б в г д е ж и к л м Зле пахне код.
- ↑ Counsell_Code Smells, 2010.
- ↑ devconf.
- ↑ а б Мартін Фаулер_Рефакторинг, 2003, с. 54.
- ↑ Мартін Фаулер_Рефакторінг, 2003, с. 54.
- ↑ а б в Мартін Фаулер_Рефакторінг, 2003, с. 55.
- ↑ а б в Мартін Фаулер_Рефакторінг, 2003, с. 56.
- ↑ Мартін Фаулер_Рефакторінг, 2003, с. 56—57.
- ↑ а б в Мартін Фаулер_Рефакторінг, 2003, с. 57.
- ↑ Зле пахне код, с. 57.
- ↑ а б в г д Мартін Фаулер_Рефакторінг, 2003, с. 58.
- ↑ а б в г д Мартін Фаулер_Рефакторінг, 2003, с. 59.
- ↑ Тимчасове поле.
- ↑ а б в г Мартін Фаулер_Рефакторінг, 2003, с. 60.
- ↑ а б в г Рефакторинг коду.
- ↑ а б в Мартін Фаулер_Рефакторінг, 2003, с. 61.
- Martin Fowler. Refactoring. Improving the Design of Existing Code // Addison-Wesley : журнал. — 1999.
- 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. — ISSN 1063-6773.
- S. Counsell, R. M. Hierons, H. Hamza, S. Black, and M. Durrand. Exploring the Eradication of Code Smells: An Empirical and Theoretical Perspective // Advances in Software Engineering Volume 2010 : журнал. — 2010.
- Рефакторинг коду. 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.