В прошлой части я перечислил советы по развитию качества принятия решений. В этот раз поговорим о практических принципах, которые можно брать и использовать «здесь и сейчас».

Подходов, советов и принципов существует очень много, и их можно отобрать и классифицировать по-разному, в зависимости от того, чему отдать приоритет. В данном случае я отобрал то, что влияет на поддерживаемость кода, надежность и удобство работы с системой.

Я исхожу из того, что по мере развития система должна упрощаться: упрощаться кодовая база, увеличиваться прозрачность работы компонентов и их взаимодействия. Звучит абсурдно, ведь новые фичи добавляются и добавляются. Но в то же время разработчики лучше понимают, с чем имеют дело — проясняется предметная область и сценарии использования. Выявляются похожие по смыслу вещи, они объединяются. Неудачные решения заменяются на более удачные.

Принципы, которые я перечислю дальше, конечно не 100-процентная истина. Как всегда, все хорошо к месту. Например, если сопоставляются два варианта, и обычно, при прочих равных, один вариант лучше, то все-равно найдется ситуация, когда лучше использовать другой вариант. Но даже сама идея о том, что существуют варианты и есть некий критерий выбора, поможет принять осознанное решение.

Когда это все может пригодиться? На стадии дизайна, при реализации и на код-ревью. Другими словами — всегда.

Итак, вот несколько идей, которые можно использовать, когда нужно сделать выбор.

Разделить всегда успеете

Если возникает вопрос — вводить ли в систему новую сущность, когда уже есть похожая, или расширить/доработать существующую, то, как правило, выгоднее расширить существующую.

Да, мы можем не угадать, как будет лучше в будущем. И, возможно, когда-нибудь придется поменять решение. Но, в случае чего, разделять будет проще, чем объединять. Почему? Причин много, но я приведу одну — разделенные сущности быстро начинают жить своей жизнью. Например, ими могут занимаются разные разработчики. Отличия неизбежно множатся и вскоре становится проблематично соединить то, что по смыслу является одной вещью, но реализовано в виде нескольких.

По моим наблюдениям, именно необоснованное разделение — это то, что скрывается за фразой "так исторически сложилось". То есть об этом не подумали и хорошо бы когда-нибудь переделать, но это сложно.

Но если совсем не делить, то можно получить химеру, нечто с головой льва и хвостом змеи. Как определить разумную грань?

Найти разумную грань нам поможет принцип единой ответственности (single-responsibility principle). Принцип понятен и известен, поэтому я не стал выносить его в отдельный пункт. Но я отмечу, что из всего набора принципов SOLID, SRP наиболее универсален, так как не завязан на ООП или вообще на какой-либо подход. И не привязан к уровню проектирования — SRP можно применить к функции (даже в bash), классу или микросервису. И даже можно применять вне разработки. Например, каждая единица текста, такая как предложение, абзац или глава, тоже должна доносить одну мысль.

Также SRP эффективен, как начальная точка для рефакторинга запутанной кодовой базы. Помогает найти, с чего начать распутывать.

Подводя итог — если нет сильных доводов в пользу деления, то лучше не делить.

Убирайте логику из кода

Для иллюстрации возьмем такой паттерн — сначала проверяется некое условие и затем выполняется некое действие. Например, нет папки, значит надо создать папку.

При таком подходе есть проблема — вероятны гонки, когда между проверкой и действием состояние может поменяться. 

Про эту проблему часто пишут. Но это еще не все:

  • код усложняется, обрастает деталями. При этом детали эти второстепенные, но в коде все это — строки, объекты одного порядка. И чтобы понять, какие из них главные, а какие нет, надо вчитываться;

  • появляются фрагменты, которые логически связаны между собой. То есть, одна строка (один statement) — это единица программного текста, понятная вам, ревьюверу, машине. А вот несколько связанных логически между собой строк — это уже конструкция, которая сама себя не объяснит. Подсказки могут потребоваться человеку (комментарий) или машине (пометки для coverage, например);

  • рефакторинг усложняется — при переносе фрагментов кода надо не забыть про связи (возможно неявные) между фрагментами;

  • тестирование усложняется, ведь каждую ветку надо проверить.

Что же с этим делать?

  • использовать операции, которые инкапсулируют проверки в себе. Например, mkdir -p в shell-скриптах, upsert в операциях с базой данных и т.д. Или, например, сами выносите вспомогательные функции, которые скрывают логику;

  • использовать декларативный подход. То есть описывать результат, который нужен, а не действия для его достижения (например, констрейны в БД вместо валидации в коде, регулярки вместо кода для разбора строк).

Если сформулировать проще, то нет кода — нет багов.

Чем строже модель данных, тем проще с данными работать

Здесь можно вспомнить разные ситуации, но я остановлюсь на одной. Часто при проектировании таблицы в реляционной БД возникает соблазн часть данных засунуть в поле с типом JSON, так как это гибче, не надо думать наперед, и т.д.

Да, иногда это оправдано, но есть цена, которую придется заплатить, независимо от того, насколько это решение было необходимым. Например, более хитрые миграции, если формат данных в JSON колонке придется менять. Или совместимость кода с разными вариантами схемы. С валидацией будет сложнее – возможно ее придется вынести в код и не забыть везде вызывать. И так далее.

Получается куча кода, которая разбросана по всей системе, и эту кучу кода тоже надо будет тестировать.

То есть все это возможно, но гибкость имеет цену. Подумайте, стоит ли оно того. 

Не смешивайте слои абстракций

Допустим, вы (или ваш коллега) разделили код на модули, проработали интерфейсы. Каждый объект одного слоя использует несколько объектов/методов/чего угодно другого слоя. Получается понятно, получается красиво. В голове легко построить схему и легко абстрагироваться от деталей, когда они не нужны. Понятно, куда добавлять, когда надо что-то добавить. А когда надо залогировать некий тип операций, это делается в одном месте, а не разбросано по коду. В тестах тоже все хорошо — если надо, то можно подменить слой на заглушку.

Но проблема в том, что можно взять и начать добавлять функционал в обход слоев. Ведь вариантов получить работающий код гораздо больше, чем вариантов получить хороший код.

Что можно сделать? Сформулируйте основные идеи дизайна. То есть из чего исходили, когда придумывали. Почему решили именно так? И периодически проверяйте, соответствует ли текущее состояние системы тем идеям, которые были на этапе проектирования. Возможно идеи устарели и не соответствуют текущей реальности. Но это тоже вывод, который сам себя не сделает.

Снижайте связанность частей

Можно встретить разные вариации этого принципа - ортогональность, Low Coupling/High Cohesion и т.д.. Где-то акцент будет на независимости частей друг от друга, где-то на распределении ответственности между частями. Но в итоге это про одно и то же - распределение ответственности делает части независимыми друг от друга.

Разберем на примере таких задач, как обработка ошибок, повторные попытки выполнения, кэширование, персистирование и т.д. То есть некие утилиты. Я называю такой код/сервисы инфраструктурными.

Лучше, когда такой код обобщен и не зависит от специфики основного кода. Например, код для запуска функции в потоке не является специфичным для проекта. Есть некий контракт, его надо соблюдать. Остальное обеспечивает инфраструктурный код сам.

Что это дает? Разделение ответственности, возможность переиспользовать, и упрощение тестирования. Остановлюсь на последнем пункте. Допустим, есть основной код (сама функция) и что-то такое сбоку, например, повторные попытки выполнения. Если между этими единицами кода сильная связь, то скорее всего придется тестировать их, как одно целое, усложняя тест. А у обеих частей, как правило, есть свои варианты выполнения, среди которых могут быть интересные комбинации, что также усложняет тест.

Конечно, есть обратная сторона, абстракции текут, и иногда может потребоваться тестирование комбинации основного кода с инфраструктурным. Например, по-дефолту в Python объекты datetime не сериализуются в JSON. Допустим, у вас есть код, который конвертирует модели ORM в JSON. И, допустим, в функции сериализации аргументом может прийти запись из любой таблицы. И вот это сломается, когда будет попытка сериализовать объект с datetime типом полей. К сожалению, даже 100-процентный coverage этого не покажет, если код сериализации обобщен. Надо обеспечивать соблюдение контракта другим способом.

Негативные сценарии — это норма

Если рассмотреть систему, как набор компонентов со связями между ними, то вот что можно заметить.

По мере увеличения количества компонентов количество связей между ними растет очень быстро. Например, добавили брокер очередей, и у каждого сервиса, работающего с очередью, будет связь с этим брокером. То есть на каждый новый компонент в системе добавляется несколько новых связей.

И не только компонент является точкой отказа, но и связь между двумя компонентами. Например, может быть stateful подключение между клиентом и сервером. И может быть такая реализация, что если сервер перезапускается, то клиент не сбрасывает подключение, или не чистит пул подключений. Получается оба компонента работают, но связь между ними не работает. Поэтому связь между компонентами тоже является точкой отказа. Выходит, что количество точек отказа растет быстрее, чем количество компонентов в системе.

Добавим к этому, что компоненты не работают в вакууме. И, например, на машине с неким сервисом вполне может закончиться память или место на диске. Это тоже связи в системе.

А еще, как отдельные компоненты, можно посчитать потоки в многопоточном приложении. А синхронизацию и общие данные, как связи.

И получается, что можно насчитать сотни, если не тысячи таких связей. При этом по каждой есть вероятность отказа — deadlock между потоками, утечки ресурсов, сетевые сбои, недоступность внешних сервисов. По каждой отдельной вероятность низкая, но если учесть все? Можно легко представить ситуацию, когда в системе что-то постоянно ломается. И вот ваша работа превращается в бесконечную отладку.

Подходов, как с этим бороться много: health чеки, pre-ping, квоты, стратегия fail fast, таймауты, резервирование ресурсов и т.д. Зависит от конкретного компонента, или связи между компонентами — под каждую проблему свой подход. Более подробно об этом рассказано в книге «Release It!». 

Например, в файловой системе ext4 есть резервирование места, которым может пользоваться только привилегированный пользователь (root). То есть процесс, запущенный под непривилегированным пользователем, не исчерпает все место.

Подводя итог – помните, что при добавлении компонентов и связей в систему, увеличивается ее сложность и вероятность проблем. Лучше об этих проблемах подумать заранее

К чему это все?

В первой части статьи я говорил о качестве принятия решений в условиях нехватки информации, как о критерии уровня разработчика. 

Дополню эту мысль. Рано или поздно в работе наступают моменты, когда все, что можно, пошло не так. Под реальной нагрузкой происходят какие-то чудеса. То, что должно было просто работать, постоянно отваливается. На отладку уходит куча времени. Вместо решения новых задач приходится возвращаться к старым. Это происходит каждый день и становится нормой. А в более долгой перспективе проект превращается в набор костылей.

И что гораздо хуже, не получается внятно сформулировать, в чем причина. А причина, скорее всего, в неправильных решениях, принимаемых перед внесением изменений в систему. Хотя часто достаточно подумать всего полчаса, чтобы избежать долгосрочных последствий

Да, это не спасает на 100 процентов, потому что обязательно возникнут другие проблемы, о которых вы и не подозревали раньше. Но ведь именно так и растет разработчик — сталкивается с новыми для себя проблемами, делает выводы, идет дальше. Как говорят, опыт растет прямо пропорционально выведенному из строя оборудованию.

Комментарии (1)


  1. Codecat_girl
    19.02.2025 05:20

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