Як писати код так, щоб він був зрозумілий іншим розробникам
Привіт! Мене звати Тетяна і я .Net Intermediate Software Engineer в ІТ-компанії SoftServe.
Я би сказала, що тему для статті я обрала досить філософську, але і конкретну водночас. Ми всі ніби маємо приблизно однаковий набір літератури на початку і по ходу розвитку своєї кар’єри. Кожен С++ розробник точно читав частково чи повністю Страуструпа, а для шарпістів біблією є Дж.Ріхтер та його «CLR via C#». Ми всі знаємо «Чистий код» Роберта Мартіна, а ще більшість з вас точно чули про його хорошого приятеля Роя Ошероува та його «Мистецтво автономного тестування».
Кожен програміст від джуніора до техліда має раціональні, але й суб’єктивні погляди на красу, чистоту та зрозумілість свого чи чужого коду. Але чи всі ми пишемо класний код? Чи для всіх нас це мистецтво і першочергова мета роботи?
Почнемо з базового: що таке «зрозумілий код»
Означення 1. Зрозумілий код — це естетично гарний, структурований і не перевантажений код, який має єдине форматування, оптимальну структуру та логіку.
З першого погляду здається, що і причепитися нема до чого, але припустимо, що для певної нової фічі, цей гарний естетичний код треба розширити чи написати до нього юніт-тести. Чи буде це так легко і прозоро для програміста, як в той момент, коли він вперше читав код — питання.
Одного дня, мені дали завдання покрити дуже великий клас юніт-тестами з нуля. А суть ще була в тому, що я не знала цей код і не знала навіть, як його подебажити, і яка взагалі функціональність у класу. Тобто почала просто з того, що прочитала цей клас. Написано було круто і доволі логічно, мій респект за назви змінних та методів.
Але згодом стало зрозуміло, що цей клас неможливо покрити тестами. Проблема була в тому, що кожен метод містив в собі дуплікований код (до речі, явне порушення дизайн-принципу DRY — don`t repeat yourself) для приватної змінної unitOfWork цього класу, яка залежить від статичного класу Bootstrapper. Потрібно це замокати через залежність від БД, але неможливо, якщо не відрефакторити код.
Рішення я знайшла таке: позбутись дублікації коду в кожному методі, винести цю логіку в protected virtual property, яка легко мокається і дає можливість покрити тестами весь клас. Результат — клас покрито тестами на 82% з нуля.
Так, це був взагалі не болісний рефакторинг, але попри красу коду з першого погляду, він спочатку був не досить «чистий».
Тут можна було б додати ще пару слів про те, що TDD — це круто, але з цим вам краще піти до Ошероува, бо він в тому однозначно майстер.
Єдине, що додам, хороша практика хоча б покривати юніт-тестами функціональність, яку ви заімплементили або той код, що змінили в рамках багу. Навіть якщо проєкт, на якому ви працюєте, не розроблюється за принципами TDD або не має суворої вимоги все покривати тестами, ви несете відповідальність за свій код і маєте бути впевнені у його коректності. Професіоналізм якраз і криється у таких принципових підходах до розробки. А ваші колеги в майбутньому зможуть легко розібратись у певній частині коду саме за його тестами, що також додає вам плюсик в карму.
Очевидно, що я не заспокоюсь, поки ми не виведемо гарне і чітке формулювання зрозумілого коду. Тому в означення 2 треба включити ще й базові характеристики чистого коду.
Означення 2. Зрозумілий код — це естетично гарний, структурований і не перевантажений код, який має єдине форматування, оптимальну структуру та логіку, тестабельний та легко розширюваний.
Самодокументований код — реальність чи нездійсненна мрія
Я часто чула тезу про те, що код, який потребує коментарів — поганий і має бути переписаним. Але коли ти трейні чи джун, то слухаєш дорослих і мачурних девів, видаляєш всі свої коментарі з коду, рефакториш і залишаєш остаточний, безсумнівно, найкращий варіант коду суб’єктивно зрозумілий для тебе та твоїх рев’юверів. А через два місяці прилітає баг, який асайнять на knowledge keeper’а цієї фічі.
Доводиться дебажити код, ніби бачиш його вперше, бо складність коду, його вкладеність або ж складність вимог даної функціональності були настільки високими, що, у підсумку, самодокументованого коду недостатньо для його розуміння.
Спасінням є додаткова документація у вигляді коментарів конкретних рядків коду чи
Також, я би хотіла відзначити високу необхідність додаткової документації і слідуванню процесам. Всі вимоги до функціональностей мають бути детально прописані на Confluence чи хоча б в Jira-айтемах, а саме з такими пунктами, як Current state, Acceptance Criteria та, в ідеалі, зі сценаріями Gherkin.
На жаль, команди без бізнес-аналітиків є зараз розповсюдженою практикою, тому розробники можуть бути відповідальними за опис фічей, і тут важливо не лінуватись документувати навіть найменші user story. Але те, що я реально оцінила з досвідом — це UML-діаграми складних фічей.
Наведу приклад з мого поточного проєкту. Для клієнта зробили класну складну фічу декілька років назад, а користуватись нею почали зараз. На мене заасайнили багу від супорт-команди клієнта, яка насправді не є багою.
Майже не втрачаючи час, ми з QC визначили, що все описане — 100% не баг, тому що знайомі з цією функціональністю. Але щоб довести це, недостатньо просто почитати код і описати, як воно працює. Архітектор з проєкту пошарив на нас сторінку конфлюєнс з UML-діаграмами цієї функціональності, що стало неперевершеним залізним аргументом в тих дискусіях з супорт-командою.
Я думаю, ви тепер розумієте, чому самодокументований код — це скоріше нездійсненна мрія, ніж реальність. Тому гіпотезу про зрозумілий код ми розширимо ще більше.
Означення 3. Зрозумілий код — це естетично гарний, структурований і не перевантажений код, який має єдине форматування, оптимальну структуру та логіку, тестабельний, легко розширюваний, має чіткі та компактні коментарі щодо логіки чи вимог конкретної функціональності.
Або KISS, або створюй власну мову програмування
Насправді це гучний заголовок до абсолютно простої тези — не ускладнюйте життя собі й іншим там, де це не потрібно, тобто KISS — keep it simple stupid — один з найпростіших і водночас найскладніших принципів розробки. І хай там як, але без SOLID тут не обійтись. Бо слідування цим принципам поліпшує якість, простоту коду і його логіку, а також по суті зводиться до дизайн-принципу KISS.
Наведемо деякі мої улюблені тези з книги «Чистий код» дядюшки Боба, але без опису всіх-всіх принципів:
- Кожен програмний модуль повинен мати одну й тільки одну причину для зміни — перший принцип з SOLID — Single Responsibility. Той же принцип лягає і на зв’язки між компонентами систем: до одного компонента повинні включатися класи, що змінюються за одними причинами і, одночасно, до різних компонентів повинні включатися класи, що змінюються у різний час та з різних причин — Common Closure Principle.
- Interface Segregation Principle — розробники програмного забезпечення повинні уникати залежностей від усього, що не використовується, в іншому випадку, такі залежності можуть стати причинами несподіваних проблем. По суті синонімом цього принципу, але для компонентів є Common Reuse Principle, згідно з яким класи, які не мають тісного зв’язку, не мають існувати в одному компоненті.
Не рідко програмісти-початківці, ознайомившись з першими патернами, намагаються всунути їх скрізь, де тільки можна, щоб, так би мовити, «набити руку». Це може призвести і до порушень принципів SOLID, і до зниження зрозумілості коду. Бо навішування патернів на ту логіку, де це не виправдано, ускладнить розуміння коду для програміста будь-якого рівня, чи то джуніор буде розбиратись, чи то сініор. Іншими словами, не порушуйте принцип YAGNI — You aren’t gonna need it — і не старайтесь додати якусь круту логіку, тому що просто захотіли потренувались у її реалізації. Ми імплементуємо суворо за вимогами і тільки ту функціональність, яка дійсно потрібна в даній ситуації.
Проте, якщо тобі не подобаються всі ці принципи і загальноприйняті правила та стандарти, вихід є — створи власну мову програмування. В такому випадку ти зможеш адаптувати свою мову програмування під конкретні задачі, зробити код більш зручним і зрозумілим для тебе та твоїх колег, визначити стандарти та стиль кодування і, можливо, навіть прискорити процес розробки та вирішення інноваційних задач. Бунтівники рухають цим світом, тому або KISS в універсальних мовах програмування, або власний шлях створення нової мови.
Означення 4. Зрозумілий код — це естетично гарний, структурований і не перевантажений код, який має єдине форматування, оптимальну структуру та логіку, тестабельний, легко розширюваний, має чіткі та компактні коментарі щодо логіки чи вимог конкретної функціональності, відповідає принципам SOLID.
Останнє означення виглядає достатньо чітким та коректним, але я припускаю, що воно все ще не ідеальне і, скоріше за все, я з позитивною упередженістю зарано зупинилась. Але хотілось би не втрачати інтерес читача і перейти до конкретніших прикладів, які я зустрічала на практиці.
Auto/var — це зручно і зрозуміло
Використовуйте var в шарпі чи auto в плюсах там, де це можливо при ініціалізації змінної, тобто майже скрізь. Це буде спонукати вас називати змінні так, щоб тип самої змінної був зрозумілим з контексту назви, що значною мірою покращить читабельність вашого коду, а також процес розробки. Тобто ви зможете орієнтуватись в коді тільки за назвами змінних без їх типізації, і в ідеалі розуміти, що це, наприклад, за Entity, не заглиблюючись у його праву частину. Приклад наведено нижче.
Explanatory variables
Це незалежні змінні або змінні predictor’и, які є пояснювальними для певної логіки.
Розглянемо на прикладі. На першому скріні ми бачимо доволі складний за структурою Dictionary і не менш складний IF. Якщо в майбутньому такий код доведеться дебажити, то я щиро не заздрю деву, який полізе в це розбиратись.
Але гарна звичка використовувати explanatory змінні, як наведено на скріні нижче. Такий код легко дебажити і легко читати, бо зрозуміло, чим є кожен предикат.
Код вище я написала в рамках імплементації однієї складної фічі з реального проєкту, але адаптувала його для вас так, щоб залишилась тільки форма для прикладу без конкретики і порушень NDA.
Класи хелпери — це зло
Якщо у назві класу є слово Helper, то це сигнал до того, що вже є проблеми з розумінням, для чого вам цей клас. Скоріше за всього під час рефакторингу у вас не виникло кращої ідеї, ніж винести сумнівно підходящий функціонал з основного класу, наприклад DbProcessor, і ви зробили ще один клас під назвою DbProcessorHelper.
Таким чином, ви даєте можливість будь-якому деву перетворити DbProcessorHelper на клас-монстр чи клас-смітник з різною функціональністю, бо це ж Helper. І в майбутньому такий клас мало того, що не дасть розуміння, навіщо він створений, та ще й майже напевно виникнуть складнощі у роботі через відсутність його Single Responsibility.
Виходить, що єдиною причиною для змін є будь-яка причина, адже всю нову функціональність можна віднести до категорії Helper.
Не залишайте в коді кортежі з великою кількістю елементів в них
Кортежі представляють нам короткий синтаксис для групування кількох елементів даних у спрощеній структурі даних. Але, навіть маючи контекст, далеко не завжди зрозуміло, які дані містить в собі Tuple.
Розглянемо приклад нижче. Ми маємо Dictionary для зберігання номера в черзі як ключ та для значення — Id/Db одруженої пари. В коменті описано, що міститься в ключі та значенні, але працюючи з таким Dictionary ми будемо постійно змушені прописувати довгий Tuple, тобто дублювати код, який без пояснювального коментаря над методом не буде зрозумілим жодному розробнику.
В такій ситуації швидке та очевидне рішення — обернути такий кортеж в using із лаконічною назвою MarriedCouple, щоб і надалі використовувати його в зрозумілішій формі.
Також є варіант винести цю логіку в структуру, наприклад, замість використання Tuple. Але це вже залишається на розсуд розробника використовувати той чи інший функціонал.
Синтаксичний цукор — то є сіль зрозумілого коду
Іншими словами, як в реальному житті, так і в програмуванні — зловживання цукру є шкідливим для вашого здоров’я.
Я знаю девів, які люблять навішати новомодного синтаксичного «цукру», аби всі бачили, що вони стежать за новими фічами і завжди в тренді. Але іноді я бачу в тому проблему з точки зору читабельності коду, а також його дебагінгу.
Боротьба з ворнінгами та перевірка complexity
Встановіть extension SonarLint для Visual Studio та включіть «Treat warnings as errors» — так ви позбудетесь різних code smells, помилок компілятора і таке інше. Безумовно, не менш хороша практика — аналізувати та поліпшувати код метрики для вашого solution за всіма проєктами та їх класами, наприклад, такі метрики, як Cyclomatic Complexity, Depth of Inheritance, Class Coupling.
Підсумок та порада
Більшість розробників не аналізує необхідність моніторингу Code Quality, написання юніт-тестів та і взагалі рефакторингу, як такого. Клієнти не бачать в тому сенс, щоб переплачувати, наприклад, за час на рефакторинг, а розробники через жорсткі дедлайни і не доестімейчені сторі, не завжди можуть заімплементувати якусь нову фічу зрозуміло і гарно, не переписавши при цьому старий код.
Виходить, що перші отримають не супер якісний код, а другі потім страждають від цього сніжного шару, заімплеменченого на костилях. Звідти й взаємозалежні баги — випадок, коли фіксаєш одне, а ламається інше, таким же чином виникають класи-монстри і методи-монстри, коли кожному наступному деву легше додати ще один мільйонний if, ніж взяти на себе відповідальність за складний рефакторинг коду, який ще й не покритий тестами.
Кожне, здавалося б, незначне рішення, може призвести до серйозних архітектурних проблем в майбутньому і до проблем розуміння коду в цілому, як для вас, так і для ваших колег. Перформанс роботи на таких проєктах буде суттєво падати навіть у найкрутіших команд, а взятися за рефакторинг буде здаватися вже нереальною задачею.
Але вихід є — пишіть професійно, зрозуміло і гарно кожен найменший кусочок коду. Якщо ви працюєте з класом-монстром, не додавайте в нього нічого нового, подумайте, як можна винести вашу функціональність в інший клас чи спростити impact.
Дякую за увагу і буду рада вашим коментарям з думками на цю тему.
86 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів