Запахи коду

Матеріал з Вікіпедії — вільної енциклопедії.
Перейти до: навігація, пошук

Код із запашком (код з душком, код, що погано пахне англ. 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].

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

Примітки[ред.ред. код]

Література[ред.ред. код]

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