Физик стремится сделать сложные вещи простыми, а поэт – простые вещи – сложными. - Лев Давидович Ландау
Давно хотел написать статью о наболевшем. За более чем 12 лет разработки и работы в разных компаниях, командах, на рынках запада и России я вижу самый главный и самый жуткий бич всего ИТ - переусложнение на ровном месте. В статье я попробую раскрыть что я имею в виду, приведу примеры переусложнения и предложу варианты как с этим бороться.
Что такое переусложнение
Попробую дать формальное определение. Всем известно такое понятие как "преждевременная оптимизация". В данном случае, это лишь частность от более общего понятия - "переусложнение".
Переусложнение - это деструктивная практика разработки программного обеспечения, ведущая не к улучшению, а к ухудшению процесса. Это негативная концепция, которая быстро обретает сторонников, она заразна как коклюш и порой не менее смертоносна. Чаще всего корни этого явления лежат в слабых компетенциях команды или в сознательном усложнении в угоду личных или общекомандных целей, чаще всего контрпродуктивных.
Рассмотрим переусложнение в разрезе 3 китов - основы разработки программного продукта (планирование, архитектура, кодинг).
Переусложненное планирование
Истоки переусложнения появляются еще на этапе разработки фичи в кулуарах бизнесовых отделов и более низких по иерархии продукт-оунер отделов. Простейшие фичи вроде того, чтобы в дейтинг-приложение вроде тиндера или badoo добавить возможность покупать себе продвижение в ленте - обретает совершенно выдуманные и даже безумные идеи вроде того, чтобы продвижение можно было покупать за TON или за индийские рупии или покупать еще страховку жизни в придачу и тп.
Такое рождает сложности и дальше. Команда садится перегонять требования бизнеса в задачи в jira. Тут начинаются танцы с тем, чтобы даже самые мелкие фичи бить на задачи, каждую задачу описывать простынями текста, а каждое принятое решение апрувить у 5-отделов.
Дальше каждая мини-фича обретает зависимости от 50 других команд, это и аналитика которая неделями изучает и никак не ставит апрув так как видят бесконечные риски, это и дизайнеры, которые зачем-то вместо простого макета полностью перерисовывают существующие интерфейсы.
А ведь надо было всего лишь дать возможность юзерам покупать продвижение, но по итогу спроектирован космический корабль, который умеет все, покупать и продавать и продвигать что угодно и кого угодно и где угодно за любые валюты.
Вот фичу утвердили, вот задачи нарезали, что дальше? Дальше кодинг.
Переусложненный код
Переходим теперь к коду. Продемонстрируем усложнение кода на ровном месте прямо на примерах.
Итак, первый пример, использование академического синтаксиса:
import functools
pairs = [(1, 'a'), (2, 'b'), (3, 'c')]
functools.reduce(lambda acc, pair: acc + pair[0], pairs, 0)
Вместо того, чтобы написать:
sm = 0
for v, _ in pairs:
sm += v
Вынос всего и сразу в функции:
def create_user(user)
check_user()
change_name_user()
get_current_user()
get_previous_user()
get_time()
def check_user():
def change_name_user():
def get_current_user():
def get_previous_user():
def get_time():
Вместо того, чтобы все оставить в 1 функции create_user. Это простейшие
операции, зачем их выносить? Код сложно читать, постоянно надо скакать
по функциям. Почему бы не выносить большие куски с отдельной логикой вместо
того чтобы выносить все подряд бездумно, усложняя чтение кода.
Все интерфейс, интерфейс интерфейсов передается в другой интерфейс и тп:
type UserGetter interface {
Get()
GetWithError()
GetAndSet()
}
type UserBetterCreator interface {
Create()
Add()
AddIfExists()
}
type UserNotBadCreator interface {
Create()
Add()
AddIfExists()
}
Есть места, где интерфейсы нужны, например, чтобы передать разные
источники данных (редис, БД, файловая система). Но часто вижу код, где думно
или бездумно в интерфейсы оборачивается все что угодно, что даже в теории никак
не может быть полиморфным. Вместо передачи класса карандаш лезет класс предмет,
а предмет идет как сущность и тп.
Использование асинхронности там где она не нужна:
package main
import "fmt"
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
Считаем фибоначи в 10 горутин, ведь операция очень сложная и юзеру она нужна
вот прям сейчас, дождаться сотые доли микросекунд он точно не сможет. Понимаю,
что пример условный, но такого обычно полон и рабочий код в некоторых командах.
Прочитал про горутины мануал и сразу применил, а почему бы и нет? Считаться
будет же быстрее - типичный пример где код никак не связан с продуктовой
составляющей.
Я рассмотрел лишь часть (буквально 4 примера, которые на слуху, но в отдельной статье можно собрать добрые 40 или даже 50 пунктов).
Коснемся и архитектуры, если это не просто подкодить в 5 местах, а собрать какие-то новые сервисы.
Переусложенная архитектура
Допустим имеется типичная команда, в ней 5 бекендеров, 2 фронта, 1 менеджер и 2 тестировщика. Представим, что это продуктовая команда и менеджер приносит тимлиду команды следующую фичу: надо сделать копию тиндера (ушедшего с рынка в России).
Итак, представим, что команда опытная и это уже их не первый сервис, представим, что ничего нет, кроме готовой инфры и инструмента планирования. Происходит груминг (не более 1-2 часов), после которого рождается примерно такое архитектурное решение:
Схема проста и лаконичная, она демонстрирует концептуальное разбиение на основные компоненты. Нет ничего лишнего, она подробна именно на столько насколько это необходимо, чтобы начать резать задачи и собирать код.
А теперь взглянем на переусложненную схему:
Тут наше простейшее приложение (MVP) обрастает кучей логики и зависимостей. Рождаются сервисы оплаты, премиум, декомпозируется авторизация и показ ленты. Откуда взялось, что нам нужна оплата? Откуда взялся премиум? Это нас просили делать? Нет. Это есть в планах? Нет. Даже если это и есть в планах через 30 лет, надо ли нам не имея пока еще ничего, начинать уже это собирать? Перегруженная схема усложняет декомпозицию, усложняет оценку сроков, растягивает архитектурное ревью и усложняет заблаговременное распознавание узких мест. К тому же, появляется какой-то gateway, видимо под невероятный хайлоад, все обвешивается кешами и валидаторами.
Риски
К чему обычно приводит переусложнение:
Срыв сроков. А в свою очередь, факт нарушения дедлайна может повлечь последствия от низких оценок перфоманса до увольнения (сокращения) точечных, а иногда и всей команды.
Невероятно сложная система. Чем сложнее система - тем сложнее в ней разобраться, начинает требоваться очень высокая квалификация, джуны теряются или пишут плохой код, а новички горят с необходимости не только осваивать домен, но и вариться в кодовой базе.
Переписать пол проекта. Минимальные изменения в 1 строчке (добавить кнопку) приводят к необходимости менять 20 интерфейсов, переносить из модуля в модуль, писать простыни тестов и бесконечно проходить ревью.
Это уже никому не нужно. Система так сложна и столько потребовала времени на написание, что она уже банально не нужна. Я видел такое, что пока 1 команда 2 месяца писала проект и бесконечно холиварила на архитектурных комитетах, более ушлая и оперативная команда написала и запустила проект за неделю и сорвала все лавры и премии.
Текучка людей. Из команды начинают уходить по настоящему сильные и опытные, смелые и амбициозные люди, так как бесконечные проволочки, куча согласований и постоянные холивары - выжигают любые инициативы. Зачем делать еще 1 сервис, если надо пол года будет ходить вымаливать на него апрувы? - закатать в легаси и норм.
Вредители
Кто может заниматься переусложнением, сознательно или неосознанно:
Не на своем месте. Технарь, который попал на руководящую позицию. Я видел огромное число действительно классных и крутых технических специалистов, которые сами так решили или их заставили, оказываются на больших позициях, отсюда начинаются непонимания концепций MVP, наличия сроков, планов на сервисы и проекты. Мой знакомый работал в команде, где такой технарь заставлял команду делать 99% покрытие тестами и довел команду до увольнения за неэффективность.
Сознательный вредитель. Есть те, кто видя большой перфоманс соседней команды - пытается ставить бесконечные палки в колеса, он постоянно капает всем на мозги тем, что надо еще доработать, что он ничего не понял, что он видит риски, с этим он постоянно ходит к вышестоящему руководству и пытается утопить соседнюю команду.
Любители все переусложнять. Есть просто те, кто везде видит риск. Там где никогда не будет больше 2 РПС (например управление печью крематория для собак и кошек) он видит 2000 РПС через 2 года. Там, где система будет хранить 12 записей о месяцах (приложение контроля менструаций) он видит принятие нового календаря Майя на 1000 лет. Там, где можно подождать сборки проекта 5 минут он видит пути оптимизации до сборки за 4 минуты и 55 секунд.
Слабость руководства. Видя весь этот раздрай, переусложнение и плывущие сроки, грамотный и сильный руководитель должен принять взвешенное решение о балансе между качеством и глупостью, когда одно и то же мусолится изо дня в день. Хороший руководитель имеет вес и может в связи с этим брать ответственность, он должен быть арбитром между соперничающими сторонам. Его взгляд, одновременно и безучастный и одновременно всеобъемлющий, он может видеть пути расширения приложения или наоборот осознавать, что этот код на 1 раз (сделать и забыть).
Отличие грамотной архитектуры от переусложнения
Самое главное - это то, что не нужно путать грамотно построенную архитектуру, адекватные груминги и холивары по делу, от трешовых размусоливаний на бесконечных обсуждениях. В таблице приведу отличительные атрибуты и того и другого:
Атрибут | Переусложение | Нормальный процесс |
Расширяемость | Учтена расширяемость уровня: сервис создания юзеров можно расширить в сервис управления атомной станцией. | Нет проблемы взять и накинуть пару новых ручек и фичей. |
РПС | Сервис написан на выдерживание 300к РПС в не пиковые часы | Сервис с запасом реального РПС может удержать нагрузочный тест (x5) |
Интерфейсы | Сервис сверх меры набит дженериками и интерфейсам, все есть абстрактый тип и абстрактный класс. Можно юзера подать как мешок картошки. | Интерфейсы использованы только там, где это реально уместно. Адекватно учтено, что внезапно вместо юзера мешки с картошкой не пойдут. |
Код | Код вылизан до абсолюта, сборка забита всевозможными линтерами и проверками. Правила работы с кодовой базой занимают 2 страницы А4. Ревью - настоящее мучение для любого контрибьютера. | В целом поддерживаются общепринятые практики. Код пишется в соответствии с гайдлайнами. Но нет холиваров на тему как назвать функцию checkMyGeo или checkMyGeolocation. |
Тесты | Весь код обвешан с ног до головы всевозможными unit и интеграционными тестами. Проверяется даже что 2+2 = 5 и тп. По итогу, замена цвета кнопки тянет за собой 2 недели упражнений с тестами. | Тестами покрыты ключевые участки системы. Простейшая логика не проверяется. Упор сделан на интеграционные тесты, проверяющие ключевую логику с реальными данными. |
Деплой | Для деплоя нужно 12 апрувов, разработчиков, тестировщиков, руководителя, нужно написать в 5 чатах и пол дня ждать ок или нет. После 14-00 деплоить нельзя, так как кого-то может не быть в середине рабочего дня онлайн и тп. | За деплой отвечает сам разработчик. Он оценивает риски, берет нужные апрувы. Полностью сам отвечает за продакшн, следит за стабильностью. За ошибочные деплои - следует соизмеримое наказание и разбор полетов. |
Как бороться
Сразу скажу, есть компании и команды, где бороться бесполезно, любые увещевания и объяснения - как горох об стену. Есть команды где "архитектор с лычками" может просто заявить, что схема ему не нравится, на вопрос что именно не нравится - ответ "все". На предложение сделать схему ему самому - отказ с обоснованием "ну вы же эксперты". Видел и такое, что вы готовитесь день и ночь в течении недели к архитектурному комитету, вы рассказываете на нем о схеме 2-3 часа, разжевывая каждый момент и в конце вам заявляют "я ничего не понял, ставь новую встречу". То есть банально на комитете сидят люди с достаточно низкой компетенцией в конкретном вопросе, но с умным видом заявляют что схема им не понятна и поэтому они не поставят апрув. То есть фронт может реджектнуть бекенд или тестировщик заблокировать дев-опс архитектуру.
Я вижу хороший путь борьбы с этим - оперировать сроком, это работает на всех, указание на то, что срок у нас 3 недели и будут разборы полетов если этого не окажется на проде в срок - действует, но тоже не всегда.
Еще добавлю варианты борьбы c переусложнением на всех этапах:
Сжатые сроки. Указать на сжатые сроки проекта и поиск виноватых, если они будут превышены. Часто это охлаждает самых буйных и накидываемые на вентилятор концепции быстро обретают форму, так как предлагать совсем уж странные идеи вроде написать все не на питоне, а на плюсах для ускорения - уже не рискнут.
Концепция MVP. Аргументированно объяснить, что проекты (сервисы) живут 2 года и РПС будет 2.5, а не 2500. Указать на то, что это MVP, что важно как можно раньше выпустить приложение и понять в реальности как оно живет на проде и какой отклик от юзеров.
Призвать руководителя. Если у вас хорошие доверительные отношения с руководством и вы на хорошему счету - часто работает просто ему все объяснить. Указать, что в целом все проработано и оно соберется.
Техдолг. Иногда работает то, что можно сказать, что ничего не мешает сделать проект способным держать 5000 РПС через техдолг задачи. Дескать сейчас мы соберем рабочий вариант, уложимся в сроки, а потом уже в более спокойной обстановке сделаем супер пупер конфету. С вер-стью 99.99% эти задачи никто конечно же делать не будет и не будет не по потому что лень, а потом что они никогда не получат приоритет, так как 5000 РПС там никогда не будет.
Сервис живет 2 года. Любой сервис живет 2, максимум 4 года. Если проекту больше 2 лет, значит скорее всего половины кто его писал уже в команде нет, проект сильно устарел и его поддержка стоит огромных усилий. Платформы меняются, меняются либы, гайдлайны, какие-то проекты взлетают, а какие-то нет. Надо попытаться объяснить, что нет смысла пилить проекты на 100 лет.
Страхи
Часто слышу такие страхи и обоснования почему 2 года думать над архитектурой и описывать каждый чих на А4 листа - это ок.
Давайте разберем:
РПС может вырасти. Что значит вырасти? Он что, растет внезапно? На сколько он может вырасти. Вы делали проект на коленке дома как стартап, бахнули его в гугл-плей и он взял 200 млн юзеров за 2 недели? С какой вер-стью это случится? Надо ли на такие доли процентов создавать сложные системы, способные держать такие нагрузки? Большинство компаний и команд и 10к РПС видели лишь во снах.
Будут добавлять новые фичи. Я не представляю себе синьера или мидла, которому дадут писать сервисы и фичи и он напишет так, что туда невозможно будет добавить фичу. Какие фичи? Перекрасить кнопку? Поменять название? Или новая фича уровня, вчера это был гугл, а сегодня это стал сайт по приюту для собак?
Если ошибемся придется все переписать. Еще раз, если вам пришлось все переписать, то даже 5 лет проектирования и описание каждой строчки аналитиком и тех. писателем - вас бы не спасло. Я не говорю сразу броситься кодить и собрать непонятно что, грумминг 1-2 часа - это ок, не надо только каждую строчку холиварить и каждый новый сервис гонять через 20 комитетов.
Выводы
Я не призываю начать писать плохой код, забить на архитектуру и начать делать как рука лежет. Нет, конечно надо следить за кодом, за тем, чтобы не делалась ерунда. Но, задайте себе вопрос, а почему мои синьеры или я сам выдаю такой код, почему архитектура которая рождается без круглосуточных архитектурных ревью и холиваров кривая по итогу. Я верю, что опытный и сильный синьер или техлид способен на основе требований от продукта собрать сильный рабочий код, который покроет потребности бизнеса. Если нет - тогда вопрос, почему нет? Чего не хватает, обучения, мотивации, опыта?
Многие руководители боятся доверить своим подчиненным самим сделать проектирование, собрать рабочий код, тут вопрос доверия и делегирования. Есть и вредители, кто специально топит рабочие решения, есть и прирожденные переусложнятели, которые всегда собирают атомные станции даже там где сервис обсуживает bluetooth-ошейники для собак. Пожалуйста, не надо переусложнять, не делайте преждевременную оптимизацию. Не ну будет такого, что проект придется в 0 переписать, потому что заложили сразу плохую архитектуру. Если код пришлось переписать полностью чтобы расширить под выросший РПС или добавление новой логики, тут проблема не в недостаточной проработке или кривой архитектуре, а в чем-то еще, не исключено, что в хард-компетенциях.
Если вам понравилась статья, то приглашаю вас в канал https://t.me/artur_speaking, там есть похожие темы, мысли и также я провожу трансляции и митапы на самые наболевшие темы.