Философия простого ПО. Делаем код простым и понятным
Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.
Программисты — очень креативные люди, создающие удивительные вещи. Разработанные нами приложения постоянно развиваются, усложняются, обрастают функционалом. Вместе с этим становится сложнее удерживать все детали в голове, забываются случаи-исключения и зависимости между компонентами. Процесс разработки замедляется, ее стоимость растет, растет и количество багов.
Я работаю в LeverX Group с большими корпоративными системами, и именно они делают проблему особенно острой. Случается, что сложность доменной области и сложность бизнес-процессов накладываются друг на друга, и тогда приходится дополнительно ломать голову над многочисленными запутанными абстракциями.
Со сложностью программ нужно бороться. Чем дольше наши приложения будут оставаться простыми для понимания, тем дольше мы сможем быстро и дешево их улучшать. Простота понимания не противоречит другим положительным качествам: если мы можем без труда разобраться, как устроен компонент приложения, то и, вероятно, сможем улучшить его производительность и определить необходимые изменения для реализации новой фичи или облегчения масштабирования. Если мы понимаем поток данных, нам легче идентифицировать угрозы безопасности.
Поэтому при принятии любого решения я стараюсь выбирать самое простое, чтобы его можно было объяснить каждому члену команды.
«Как упростить нашу систему?» — спросите вы. Давайте избавляться от лишнего!
Лишние абстракции
Например, зачем делить монолит на микросервисы, если нас устраивает текущая надежность и гибкость развертывания? Готовы ли вы сражаться с сетевыми задержками, распределенными транзакциями, с мониторингом и отладкой, с разными платформами и языками программирования? Не будет ли проще разбить монолит на слабо связанные модули?
На уровне кода система становится сложной не в один миг — сложность накапливается постепенно. Каждая лишняя функция или метод, каждый класс или модуль усложняют приложение.
Мы часто устанавливаем бюрократические ограничения, например, «функция не должна быть длиннее X строк» или «класс не должен содержать более X методов». Да, понять именно маленькие компоненты проще по отдельности, однако становится ли проще понимание всей системы? Зачем нам делить один класс на три, если они всегда будут использоваться вместе? Особенно если все три находятся на одном уровне абстракции и протестировать их вместе так же просто, как и отдельно друг от друга? Ведь чем меньше компонентов в нашей системе, тем проще осознать полную картину происходящего.
Чем меньше абстракций в системе, тем меньше между ними зависимостей, которые тоже увеличивают сложность системы. Например, про неочевидную зависимость легко забыть, из-за чего мы сперва на этапе оценки занизим эстимации, а потом на этапе разработки можем внести регрессии в уже существующий код. Нестабильные связи между компонентами приведут к каскадным изменениям, когда изменения в базовом компоненте вынуждают изменять и все от него зависящие. Спроектировать очевидную и стабильную зависимость — задача сложная, тем более если мы плохо разбираемся в доменной области.
Рассмотрим пример, где можно избавиться от лишнего метода:
// Исходный вариант class SubmissionsService { public submit(submission) { // примерно 50 строк кода на среднем уровне абстракции [отправка заявки] // уровень абстракции выше, чем у остального кода метода "submit" await this.updateJobStatusAfterSubmit() } // Зависимость от порядка вызова: // нужно помнить, что выполнялась основная логика "submit" private updateJobStatusAfterSubmit() { // зависимость от родительского метода // при этом сам метод всего 5 строк кода [пара дополнительных проверок, связанных с самой отправкой] this.jobsService.update(...) } } // После упрощения: class SubmissionsService { public submit(submission) { // после объединения получилось 55 строк кода на среднем уровне абстракции [вся логика отправки заявки] } }
Внешние API, фреймворки и библиотеки тоже являются полноценными компонентами системы. Но, в отличие от наших собственных абстракций, мы не можем их полностью контролировать, из-за этого риски внешних зависимостей еще выше.
Я не призываю объединять все приложение в одну огромную процедуру. Однако необходимо осознавать цену деления абстракций на более мелкие. Каждый раз спрашивайте себя: «Станет ли приложение проще после этого изменения?»
Внутреннее состояние
Внутреннее состояние — еще один источник проблем: приходится помнить не только о наличии компонента, но и обо всех состояниях, в которые он может перейти. Кроме упрощения понимания системы, компоненты без состояний имеют и другие положительные стороны: сервисы проще масштабировать горизонтально, также упрощается тестирование классов (или функций), появляется множество других дополнительных преимуществ. Кстати, на подобных компонентах и строится принцип функционального программирования.
Рассмотрим пример:
// Исходный вариант: class EfficiencyIntervalsValidator { public async validateChanges(changes: ChangedInterval[]) { await this.generateNewIntervals(changes); // зависимость от вызова generateNewIntervals this.assertNoDuplicates(); this.assertNoIntervalGaps(); return this.intervals; } private async generateNewIntervals(changes: ChangedInterval[]) { const currentIntervals = this.repository.getIntervals(); // для тестирования нужен репозиторий, // и его нужно либо мокать, либо запускать реальную БД [слияние интервалов с изменениями] // одну сущность класса нельзя использовать для параллельных вычислений this.intervals = mergedIntervals; } private assertNoDuplicates() { [проверяется this.intervals] } private assertNoIntervalGaps() { [проверяется this.intervals] } } // После упрощения: class SimpleEfficiencyIntervalsValidator { public async validateChanges(changes: ChangedInterval[]) { const currentIntervals = await this.repository.getIntervals(); return applyChangesToIntervals(changes, currentIntervals); } } // Функция также экспортируется отдельно, ее можно легко протестировать export function applyChangesToIntervals(changes, intervals) { // достаточно единственного прохода по массиву для слияния и валидации [применение изменений к интервалам] }
Итог
Чем больше в системе подвижных частей (абстракций, зависимостей и состояний), тем сложнее работать над ней. Не следует множить сущее без необходимости!
19 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів