Дублювання коду «здорової людини»: коли це виправдано
Привіт, я Ігор Левченко, працюю в IT з 2015 року, зараз Senior .NET/Sitecore Developer у DataArt. У межах менторської програми DataArt працюю з менш досвідченими колегами як ментор, що допомагає систематизувати професійні знання та готує до відповідей на спірні запитання.
Сьогодні я піду проти системи та спробую пояснити, коли написання поганого коду може бути меншим злом та чому дублювання коду іноді є прийнятнішим для деяких бізнес-випадків, ніж бездоганний і чистий код, який ми всі обожнюємо.
❗️ ВАЖЛИВО: маю попередити вас, що бездумне дотримання подальших рекомендацій з цієї статті може зашкодити так само, як і бездумне дотримання будь-якого іншого принципу чи шаблону. Пам’ятайте приказку Парацельса: «Отрута є в усьому, і жодна річ не буває без отрути. Дозування робить її або отрутою, або ліками». Тож, нумо занурюватись.
Табу
Дублювання коду є незвичним предметом обговорення. Більшість експертів зневажають цю тему, оскільки вважають, що воно не приносить нічого, крім проблем і болю. Але водночас це одна з найпоширеніших речей, які ви можете зустріти у проєктах.
Від самого початку кар’єри програміста нас вчать писати чіткий і правильний код, і я в жодному разі не ставлю під сумнів ідею уникання дублювання. Ми маємо вивчати принципи, методи і прийоми запобігання поганому коду.
Проте я не прихильник методу, коли ми вважаємо їх непорушними законами та змушуємо менш досвідчених колег повторювати як мантру і слідувати ним без жодного сумніву. Ми маємо пам’ятати про виключні випадки, переважно бізнесові, які змушують розробників йти проти цих правил.
І ця стаття має на меті спонукнути вас поглянути на проблеми дублювання під іншим кутом.
Що таке дублювання
Давайте розглянемо, про що йтиметься у статті.
Типи дублювання, які я розглядаю (цілком або частково):
- Буквальне дублювання: назва говорить сама за себе — буквальне копіювання частин коду, структур та/або вмісту. Це може стосуватися одного рядка або цілих методів чи класів.
- Семантичне дублювання: реалізація різна, але суть ідентична.
- Структурне дублювання: копіювання загальної структури або організації класів/методів/модулів.
- Ненавмисне дублювання: моє улюблене. Відбувається випадково через недостатню обізнаність або неуважність, коли різні розробники незалежно один від одного пишуть подібний функціонал, не усвідомлюючи цього.
Дублювання, яке я не розглядаю:
- Дублювання конфігурації: дублювання налаштувань чи будь-яких інших змінних у конфігурації, у різних частинах кодової бази, як-от «connection strings», спеціальні значення для конфігурації застосунку тощо.
- Дублювання від генераторів коду: ця тема занадто широка, щоб охопити її в цій статті. Кількість таких генераторів настільки значна й вони можуть працювати настільки по-різному, що я змушений залишити це вам як домашку.
- Будь-який інший тип, який ви можете запропонувати: крім того, я не розповідаю про проєкти з відкритим вихідним кодом або утилітарні. Тут лише про бізнес і ентерпрайз.
Насамперед
Давайте нагадаємо собі, чому ми маємо уникати проблеми, що обговорюється.
Основна причина — підтримка. Бажаю успіхів у пошуках і виправленні багів. Зміна вимог у такому випадку також є гарним викликом. Як ви можете здогадатися, знаходити той самий код у різних місцях — це цікава пригода.
Здатність до тестування. Це не буде проблемою, лише якщо у вас немає тестів, навіть юнітових. В іншому випадку вам доведеться покривати один код декілька разів. Дуже продуктивно та захоплююче, чи не так?
Масштабованість і розширюваність. Коли додається новий функціонал чи змінюється наявний, ми вимушені вносити зміни в ту саму логіку в декількох місцях. Це означає вищу ймовірність помилок і зниження гнучкості.
Читабельність. Ви можете написати найкрасивіший, найчистіший код, але якщо він повторюється від місця до місця, ви точно не будете щасливими. Вам доведеться постійно думати: «Я вже десь бачив цей метод чи він трохи відрізняється?». Я вже мовчу про думку ваших колег.
Розмір кодової бази. Якщо ситуація заходить занадто далеко, це може вплинути на час компіляції чи навіть на час виконання.
Послідовність і стандартизація. Цей пункт скоріше опосередкований, проте незважаючи на будь-які конвенції щодо мови чи конкретної команди, ми всі знаємо, що кожен розробник має унікальний почерк.
У реальному житті різні файли або підпроєкти можуть також бути написані у трохи власному стилі. Це природно й може спричинити проблеми лише у випадку, якщо ви почнете копіювати і вставляти вже наявний код.
Таким чином, замість інтуїтивного успадкування існуючого стилю, це може привнести трохи додаткового безладу, копіюючи не лише сам код, а й різні стилі написання.
Бізнес є бізнес
«Життя як корова, воно буває різним,» — українське прислів’я
Робочий процес звичайного програміста виглядає так: отримання вимог -> написання коду -> тестування (ви повинні протестувати, агов!) -> ... -> ПРОФІТ (отримання грошей).
Проте кожен розробник має розуміти, що його головна мета не обмежується написанням коду. Адже справа не в цьому, а в реалізації вимог ваших клієнтів, про що багацько людей забуває. Іноді (або, чесно кажучи, більшу частину часу) ми перебуваємо під тиском і мусимо поспішати, а це, зі свого боку, іноді змушує нас робити зло заради загального блага.
Ще одна важлива річ. Припустімо, що нам знадобиться продублювати код або використати будь-яке інше сумнівне розв’язання проблеми. У такому випадку я наполягаю на повідомленні бізнесу про цю ситуацію з чітко вказаними причинами. Клієнт має розглянути ризики, що можуть з’явитися в середньо-/довгостроковій перспективі, та те, скільки ресурсів може піти на написання коду «належним» чином. Зрештою, саме бізнес володіє кодовою базою.
Коли ви можете перетнути межу
Тут починається найбільш інтригуюча частина — зараз ми обговоримо потреби бізнесу, які можуть переважити бажання мати чистий код.
Термінова необхідність показати перші результати
В першу чергу це стосується PoC/нових проєктів, де зацікавленим сторонам потрібні результати на вчора, щоб ухвалити рішення щодо наступних кроків.
На цьому етапі ви можете не знати, як виглядатиме програма, коли бізнес вирішить випустити проєкт у люди. Невчасне зайве ускладнення архітектури може зіграти злий жарт, спричинивши нескінченний ланцюжок змін.
Я був у ситуації, коли власник продукту хотів простий застосунок на
2–3 сценарії з3–4 кроками в кожному. Але після першого демо керівництво вирішило його ускладнити. На той час ми вже мали архітектуру для простого плаского вебзастосунку, тому нам довелося витрачати час на її зміну для набуття більшої придатності для складніших сценаріїв, водночас розробляючи новий функціонал.Це не трагедія, але відкладення рішень щодо архітектури могло б заощадити нам час і зменшити кількість і складність змін. На щастя, бізнес поставився до цього з розумінням. Будучи повністю обізнаними про ситуацію, вони не скаржились.
Термінові виправлення
Уявіть, що потрібно виправити критичну помилку на проді, раптово почати кампанію з розсилання імейлів чи виконати інше подібне завдання. «Вправлятися швидше, рефакторити пізніше» може бути прийнятною стратегією, якщо цей процес знаходиться під вашим повним контролем і ви впевнені, що не втратите його.
Зазвичай я стикався з цим у вигляді хотфіксів, коли ми спочатку мали справу з проблемою, а потім відразу готували краще рішення для наступного релізу. Нічого особливого.
Зміна вимог або процесу
Це пов’язано з пунктами, згаданими раніше. Ви можете іноді використовувати дублювання під час масштабної зміни вимог і обмеження в часі. Знову ж таки, швидша імплементація і тестування вимог, а рефакторинг пізніше можуть бути не найгіршим варіантом. Особливо якщо ваша команда велика і вам для початку ще треба зрозуміти, що саме потрібно рефакторити.
Іноді це призводить до декількох класів з однаковими «покращеннями» від різних людей, що призводить до «дублювання узагальнення». У цьому випадку ви маєте розраховувати на чудового ліда і внутрішню координацію, щоб упоратися без проблем.
Особисто зі мною таке траплялося рідко. В таких випадках замовники забували внести до беклогу важливий функціонал, а потім про це згадували ближче до релізу. Ми повідомляли бізнес про ризики та попереджали, що якість може бути не найкращою.
Керівництво замовника обирало мати нову функціональність «як є», не відкладаючи до наступних релізів. Звісно, ми виправляли такі технічні борги до наступного релізу.
Завеликий вплив на легасі
Тут особливо немає чого додати: якщо вам потрібна невелика зміна, але ви не хочете чи не можете мати справу із застарілим кодом, інколи краще скопіювати, перш ніж починати думати про рефакторинг.
Пам’ятаю кілька випадків, коли моїй команді не дозволяли змінювати застарілий код через потребу у зворотній сумісності. Водночас необхідні нові функції вимагали архітектурних змін. Застарілий код був реалізований і протестований давно (та ще й, мабуть, у далекій-далекій галактиці).
Для бізнесу був прийнятнішим і дешевшим варіант не мати такого великого імпакту, змінюючи стару частину кодової бази. Класичний випадок — «якщо код працює, не чіпай».
Нашій команді довелося брейнштормити, як звести до мінімуму використання антипатернів і структурного дублювання. Я пам’ятаю, що наше рішення було не найкрасивішим, але ми досить близько підійшли до межі найкращої реалізації.
Незнання будь-якої хорошої практики
Може здатися смішним, але що бачите, те й отримуєте. Це все, що я можу сказати у разі недостатньо кваліфікованої команди.
Особисто я брав участь лише в одному проєкті з такою ситуацією (з метою мінімізації витрат). Вони мали підтримувати існуючий продукт. Їхній менеджер попросив мене приєднатися до команди, щоб зменшити технічний борг проєкту.
Чудовий час, коли я міг організовувати роботу, як захочу!
Проєкти, що закінчуються або є короткотривалими
«Après moi, le déluge» — «Після нас хоч потоп». Якщо ваш проєкт наближається до завершення, а час є обмеженим, можливо, варто зосередитися на стабільності та продуктивності, а не на чистоті та ідеальному стилі.
Особливо, коли це простий сайт без складної логіки та великих планів на майбутнє. У такому випадку ніхто не оцінить ваше мистецтво написання коду — час і гроші у пріоритеті.
Чесно кажучи, в мене таких випадків не було, тому цей пункт додаю зі слів колег. Я мав досвід роботи з проєктами, що закінчуються (стадія підтримки, а потім списання з балансу), але ми не були під тиском і зробили все можливе, щоб залишити кодову базу кращою, ніж будь-коли.
Варто зазначити, що це була не лише вимога бізнесу — моя команда також мала дух уникання поганих рішень. Я не можу нікому рекомендувати на власний розсуд робити дурниці — власник продукту має вирішити, як проєкт зустріне свій кінець.
Додам, що коли у вас одночасно 9 проєктів на підтримці та 2 в активній стадії розробки, ви завжди будете молитися за людей, які зробили їх зрозумілими та не покинули на останній фазі життєвого циклу.
Індульгенція від розробника
З технічного погляду дублювання виглядає набагато менш розумним. Мені б хотілося, щоб для нього взагалі не було причин, але...
У будь-якому разі пам’ятайте: ця частина не про те, що можна писати дурню, а про ситуації, коли вас можуть «зрозуміти і пробачити». Звісно, це не що інше, як моя особиста думка.
Керування версіями
Уявіть, що вам потрібно підтримувати кілька версій Web API. Усі вони можуть мати аналогічні функції, але різні механізми авторизації, обмеження, валідації, формати відповідей тощо.
Дублювання коду дозволяє налаштовувати кожен API з повторним використанням спільної бізнес-логіки.
Можу згадати версіонування API в одному з моїх проєктів, коли ми мали одну актуальну версію та одну попередню (застарілу) водночас. Не було причин створювати ще один рівень абстракції для трохи іншої логіки на контролерах, особливо знаючи, що застарілу версію буде видалено через кілька спринтів.
Інтеграція
Візьмемо для прикладу програму, яка інтегрується з кількома зовнішніми службами чи системами. Вона може мати варіації у протоколах, форматах (як-от дата), механізмах автентифікації тощо.
Дублювання коду для кожного адаптера інтеграції дозволяє обробляти певні вимоги для кожної з них, повторно використовуючи загальні шаблони самої інтеграції.
Я працював з багатьма сторонніми сервісами, але більшість мали численні відмінності. На щастя, мені не довелося застосовувати цей пункт — все було згідно SOLID.
Локалізація ПЗ
Може містити дублювання коду — кожна мова чи регіон можуть мати різні переклади, формати дати й часу, чи інші аспекти «культури». Іноді простіше мати правила для кожної, навіть якщо є спільні риси з іншими, ніж вигадувати складну інфраструктуру.
У моєму досвіді були багатомовні проєкти, але нам не довелося кидати виклик загальним принципам, щоб керувати цим функціоналом, тому дуже багато про це не скажу.
Можливо, ви можете зіткнутися з цією проблемою, якщо у вас є щось на кшталт «один модуль/ процесор на культуру». З іншого боку, такий підхід може в разі необхідності надати вам чудові можливості кастомізації.
Специфічний код для платформи
Мультиплатформовий UI може вимагати дублювання, щоб задовольнити специфічний дизайн кожної платформи та моделі взаємодії.
Ви можете генералізувати основну логіку, але може знадобитися й дублювати деякий код для UI для відповідності унікальним вимогам кожної.
Я майже не працював зі специфічним для різних платформ кодом. Втім, колись у мене було кілька дружніх розмов з людьми, які використовували Xamarin Forms, — вони скаржилися на різні колекції графічних контролів для певних платформ.
Їм довелося реалізовувати дуже схожу поведінку для кожного трішки іншого компонента. На мій погляд, це нагадує структурне та семантичне дублювання.
Конкретні оптимізації
- Розгортання циклу та однорядкові розширення (Loop unrolling & inline expansion): передбачає заміну викликів функцій фактичним кодом функції, може покращити чутливу до продуктивності частину програми.
- Спеціальна оптимізація архітектури може знадобитися при оптимізації низького рівня, специфічної для певної архітектури чи апаратної платформи.
- Зменшення витрат на виклики функцій. Виклики функцій зазвичай призводять до певних додаткових витрат через влаштування стеку, передачу параметрів і керування адресами повернення. У сценаріях, чутливих до продуктивності, дублювання коду, замість узагальнення/ інкапсуляції його у функції, може змінити час виконання.
DTO
Я вважаю DTO останнім місцем, де потрібно впроваджувати якісь складнощі. Якщо у вас є поле «Ціна» в моделях «Послуга» та «Продукт», немає потреби виносити це поле у спільний інтерфейс. Я вже не кажу про успадкування в DTO між різними шарами застосунку.
Проте я не маю на увазі утилітарні поля, спільні для джерела даних, як-от мітки часу, теги тощо.
Я дуже рідко використовую наслідування в DTO, але бачив, як люди грали з цим підходом та страждали, коли структура даних джерела змінювалась.
У свою чергу, їм доводилося змінювати наслідування, що впливало на бізнес-логіку і змушувало команду навіть змінювати абстракції, бо все, по-старому працювати не могло... Це був єдиний раз, коли я сердився на людей, які не дублювали, лол.
Зазвичай я дублюю щось типу «createdDate», заголовки тощо, але лише тоді, коли джерелом даних є один і той самий сервіс і команда на 100% впевнена, що ці поля не буде змінено чи видалено. І то не завжди.
Ефект доміно
Частково пов’язано з першим пунктом цього переліку: якщо логіка в різних модулях з часом стає майже або цілкою ідентичною, подумайте про її узагальнення. І не страшно, якщо ви залишите все як є, якщо знаєте, що через місяць методи знов можуть змінитись окремо один від одного.
На моїй пам’яті було лише кілька разів, коли моя команда вирішила не зменшувати семантичне дублювання, під час процесу значного редизайну і змін вимог. Ми створили перелік ненавмисних, структурних і семантичних дублювань та залишили рефакторинг на останній етап.
Я досі вважаю цей підхід розумним, бо багато методів були оптимізовані для конкретних випадків, решту видалили чи легко відрефакторили та перемістили на логічні місця. Цей рефакторинг був одним з найадекватніших на моїй пам’яті.
«Переінженерування»
Насправді хороший код схожий на вдалий жарт — вам не потрібно його пояснювати. Залишаючи осторонь регулярний біль, варто двічі подумати, чи не призведуть рефакторинг і генералізація до ще менш інтуїтивно зрозумілого коду.
Немає честі в самозадоволенні від рефакторингу, який нічого не покращує.
Я був свідком прикладу надмірного інжинірингу лише раз. Неофіт отримав завдання переписати взаємодію між контролерами і модулем авторизації, про яке ж сам і попросив. Він хотів, а потім і реалізував все як у книжках, що він нещодавно прочитав.
На жаль, результат був занадто ускладненим, і нам довелося його ревертнути. Ці зміни прибирали деяке дублювання, проте не мали жодних інших практичних переваг. А зрозуміти, що й як працює, стало складніше.
N.B.
Метою цієї статті було наголосити на тому, що за особливої потреби правила доводиться порушувати — і це нормально. Я не вписуюсь за погані практики кодування, але прагну пролити світло на широко поширене захоплення «красою заради краси».
Зосередження на другорядних аспектах замість боротьби зі складними й заплутаними архітектурними рішеннями може створювати відчуття експертизи та вагомого внеску.
Моя головна порада: ретельно визначайте пріоритети. Не бійтеся взаємодіяти з бізнесом. Переспрямовуйте час та енергію на виконання нагальніших і важливіших завдань. Але водночас не впадайте у крайнощі. Прагніть до якості коду, але пам’ятайте, що у цього визначення є багато аспектів і зовнішній вигляд — лише один з них.
Ніхто, крім вас і вашої команди, не може вирішувати, які методи, практики, стандарти чи шаблони застосовувати до вашої кодової бази — інструменти мають служити людям, а не навпаки.
Сподіваюся, моя стаття стала вам у пригоді. Чи, принаймні, розважила. Будьте розумними, пишіть хороший код, вітру в спину та сонця в обличчя!
21 коментар
Додати коментар Підписатись на коментаріВідписатись від коментарів