🤸

3.1. Гибкость – важнейшее свойство кода

Если выделить 1 самое важное свойство, которым должен обладать ваш код, то это будет “гибкость” – насколько ваш код и архитектура способны без проблем изменяться при появлении требования не вписывающихся в первоначальную структуру.
 
В статье с примерами рассказываю, как добиться гибкости кода и архитектуры:
 

Цикл проекта

 
У любого проекта существует 4 фазы: создание, развитие, стазис и смерть.
 
Откинем пункт со смертью проекта, потому что при разработке мы не должны задумываться об этой фазе.
 
Стазис (когда мы разработали проект и больше его не трогаем) тоже не такая частая ситуация и если вы точно знаете, что произойдет стазис, вы можете написать этот проект как угодно.
 
А вот по-настоящему важные фазы, на которые нам нужно ориентироваться это создание и развитие проекта.

Почему я описываю их отдельно создание и развитие?

При создании проекта, вы берете весь скоп знаний, можете спроектировать систему, написать ее и она будет работать.
 
А вот при развитии продукта, в 70% случаев будут приходить такие требования, которые не были заложены в первоначальную архитектуру при создании и вам придется их каким-то образом внедрять.
 
Так вот, гибкость – это то, насколько ваш код и архитектура способны быстро изменяться, чтобы добавлять в нее не заложенные первоначально требования.
 
И именно гибкость является САМЫМ главным критерием качества кода.
 
Если вы быстро создали систему, которая потом требует дней рефакторинга, чтобы добавить новую фичу, НЕ отвечающую первоначальным требованиям, значит ваша система некачественная (негибкая).

Но как мы можем достичь гибкости?

Для начала, нужно понять: а что именно создает трудности при разработке проекта?
 
Существует такие понятия как Natural Complexity (Натуральная Сложность) и Artificial Complexity (Искусственная Сложность).
 
Natural Complexity (Натуральная Сложность) – сложность решения задачи программированием, продиктованная внешними условиями: особенностями языка программирования, возможностями базы данных / протокола передачи, скоростью работы сетей, архитектурой ОС, мощностью процессоров, скоростью движения электронов через провода и транзисторы, etc.
 
Короче, все то, что продиктовано внешними условиями и нами сложно не контролируется.
 
Artificial Complexity (Искусственная Сложность) – сложность, которую мы сами добавили в свою систему: разделение на микросервисы или модели бизнес-логики, которые по итогу на 1 запрос вызывают все друг друга в зацикленом графе; куча слоев абстракций, через которые нужно пробраться, чтобы дописать 1 фичу; side-effects возникающие в коде из-за магии библиотек, использующих name convention или самоопределенные хуки, etc.
 
Если копнуть в глубь “Искусственная Сложности”, мы придем к понимаю, что вся она завязана на создании излишних абстракций и границ.
 
Соответственно, от Натуральной сложности очень тяжело / невозможно избавиться и они будет добавлять проблем на проекта, но:
 
От “Искусственной сложности” мы можем избавиться, если откажемся от абстракций, группировок и неочевидного поведения.

Как на практике “отказаться от абстракций, группировок и неочевидного поведения”

 
Попробую привести вам примеры того, что стоит или не стоит делать:
 
  • Не пытайтесь абстрагироваться от инфраструктуры, если на то нет железобетонных причин. Если вы используете конкретную библиотеку / БД / MQ / etc. не пытайтесь создать поверх них дополнительные интерфейсы, используйте их в тех местах кода так, как вам нужно.
// # Вместо создания абстрактных репозиториев const changeUserEmail = async (userRepostoty: UserRepository, req: { email: string }) => { const user = await userRepository.getByEmail(req.email) // ... } // # Используйте напрямую в коде вызов адаптера // или сам язык запросов (например, SQL) const registerUser = async (pg: PgConnection, req: { email: string }) => { const user = await pg.table(UserTableName).where({ email: req.email }) // ... }
Абстракции в виде репозиториев могут быть нужны только если вам 100% нужно подменять реализацию инфры (например, вы разрабатываете self-hosted софтину, которая может работать поверх разных БД / MQ / etc.), в остальных случаях не усложняйте себе жизнь и не создавайте их.
 
  • Модели должны выглядеть прям так, как лежат в БД. Если в модели не лежит другая модель (например, как jsonb поле PostgreSQL, или вложенный документ MongoDB), то не нужно в описании структуры их вкладывать (просто добавь именно те поля, которые есть в БД), так как это принято во многих ORM. Описывайте все сущности прямо так, как они лежат в БД.
// # Если у вас, например PostgreSQL, то вместо: type Profile = { id: string user: User } // # пишите прям как в БД type Profile = { id: string user_id: string // Потому что там лежит не целый пользователь в виде jsonb, а лишь ссылка на таблицу }
 
  • Не используйте дополнительных механик работы с зависимостями. Чтобы передавать зависимости в коде, у вас есть аргументы функций / конструктор класса, используйте его, без дополнительной магии типа IoC.
 
  • Минимизируйте side-effects и неочевидное поведение. Например, существуют такие фишки, как preSave, postInsert , preUpdate хуки, так вот, все, что будет происходить внутри них абсолютно неочевидно для разработчика, который не прочитал их содержимое. Это на 100% рано или поздно приведет к очень тяжело отслеживаемым багам и проблемам в сетапе новых разрабов. Достаточно было просто создать функции, которые занимаются созданием / изменением модели и вызывать их, что позволило бы явно видеть, что происходит при вызове этой функции.
 
  • Разделяйте все и полностью. Это очень сложная мысль, но подумайте: если вы разделите свой код на UserController и ProfileController, а потом появится метод, который связан с ними обоими, куда вы его засунете? От этого решения будет очень сильно зависеть вся дальнейшая логика. Хуже, когда такое происходит сразу с тремя сущностями. Поэтому, вместо того, чтобы пытаться делить методы на какие-то группировки, пишите каждый метод отдельно друг от друга, условно: UpdateUserData , SetEmailOnProfile, DeleteUserAndProfile и так далее.
// # Вместо const ProfileController = { setEmail: (req: Request) => {}, deleteUserAndProfile: (req: Request) => {}, } // # Разделяем на отдельные ручки const SetProfileEmail = (req: Request) => {} const DeleteUserAndProfile = (req: Request) => {}
Этот паттерн в ООП еще называется Service Object
 
Таких примеров может быть очень много, формула для самостоятельного обнаружения возможности стать гибче:
 
“Где я своим кодом создаю абстракции, группирую логику или запускаю код без явного его объявления?” – найти эти места и избавиться от них.
 
Таким образом, в вашем проекте останется минимальное кол-во Искусственной Сложности и вы будете сталкиваться только с проблемами Натуральной Сложности.
 

Что дальше?

А дальше мы обсудим с чего именно начинается любая система:
 
 

👈 Предыдущая глава
Следующая глава 👉

 
💡
Подписывайтесь на телеграм канал 🦾 IT-Качалка Давида Шекунца 💪, где будут выходить анонсы новых глав и обновления существующих. Спасибо за внимание и мощной вам прокачки 💪