Как перестать переусложнять и начать жить

в 8:36, , рубрики: teamlead
Типичный переусложенный код в представлении нейросети

Типичный переусложенный код в представлении нейросети

Физик стремится сделать сложные вещи простыми, а поэт – простые вещи – сложными. - Лев Давидович Ландау

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

Что такое переусложнение

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

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

Рассмотрим переусложнение в разрезе 3 китов - основы разработки программного продукта (планирование, архитектура, кодинг).

Переусложненное планирование

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

Такое рождает сложности и дальше. Команда садится перегонять требования бизнеса в задачи в jira. Тут начинаются танцы с тем, чтобы даже самые мелкие фичи бить на задачи, каждую задачу описывать простынями текста, а каждое принятое решение апрувить у 5-отделов.

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

Типичный дизайн для фичи "изменить иконку приложения на 8 марта"

Типичный дизайн для фичи "изменить иконку приложения на 8 марта"

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

А это типичная jira-декомпозиция, чтобы добавить 1 кнопку

А это типичная jira-декомпозиция, чтобы добавить 1 кнопку

Вот фичу утвердили, вот задачи нарезали, что дальше? Дальше кодинг.

Переусложненный код

Переходим теперь к коду. Продемонстрируем усложнение кода на ровном месте прямо на примерах.

  1. Итак, первый пример, использование академического синтаксиса:

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
  1. Вынос всего и сразу в функции:

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. Это простейшие
операции, зачем их выносить? Код сложно читать, постоянно надо скакать 
по функциям. Почему бы не выносить большие куски с отдельной логикой вместо
того чтобы выносить все подряд бездумно, усложняя чтение кода.
  1. Все интерфейс, интерфейс интерфейсов передается в другой интерфейс и тп:

type UserGetter interface {
	Get()
    GetWithError()
    GetAndSet()
}

type UserBetterCreator interface {
	Create()
    Add()
    AddIfExists()
}

type UserNotBadCreator interface {
	Create()
    Add()
    AddIfExists()
}

Есть места, где интерфейсы нужны, например, чтобы передать разные 
источники данных (редис, БД, файловая система). Но часто вижу код, где думно 
или бездумно в интерфейсы оборачивается все что угодно, что даже в теории никак
не может быть полиморфным. Вместо передачи класса карандаш лезет класс предмет,
а предмет идет как сущность и тп.
  1. Использование асинхронности там где она не нужна:

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 переусложнением на всех этапах:

  1. Сжатые сроки. Указать на сжатые сроки проекта и поиск виноватых, если они будут превышены. Часто это охлаждает самых буйных и накидываемые на вентилятор концепции быстро обретают форму, так как предлагать совсем уж странные идеи вроде написать все не на питоне, а на плюсах для ускорения - уже не рискнут.

  2. Концепция MVP. Аргументированно объяснить, что проекты (сервисы) живут 2 года и РПС будет 2.5, а не 2500. Указать на то, что это MVP, что важно как можно раньше выпустить приложение и понять в реальности как оно живет на проде и какой отклик от юзеров.

  3. Призвать руководителя. Если у вас хорошие доверительные отношения с руководством и вы на хорошему счету - часто работает просто ему все объяснить. Указать, что в целом все проработано и оно соберется.

  4. Техдолг. Иногда работает то, что можно сказать, что ничего не мешает сделать проект способным держать 5000 РПС через техдолг задачи. Дескать сейчас мы соберем рабочий вариант, уложимся в сроки, а потом уже в более спокойной обстановке сделаем супер пупер конфету. С вер-стью 99.99% эти задачи никто конечно же делать не будет и не будет не по потому что лень, а потом что они никогда не получат приоритет, так как 5000 РПС там никогда не будет.

  5. Сервис живет 2 года. Любой сервис живет 2, максимум 4 года. Если проекту больше 2 лет, значит скорее всего половины кто его писал уже в команде нет, проект сильно устарел и его поддержка стоит огромных усилий. Платформы меняются, меняются либы, гайдлайны, какие-то проекты взлетают, а какие-то нет. Надо попытаться объяснить, что нет смысла пилить проекты на 100 лет.

Страхи

Часто слышу такие страхи и обоснования почему 2 года думать над архитектурой и описывать каждый чих на А4 листа - это ок.

Давайте разберем:

  • РПС может вырасти. Что значит вырасти? Он что, растет внезапно? На сколько он может вырасти. Вы делали проект на коленке дома как стартап, бахнули его в гугл-плей и он взял 200 млн юзеров за 2 недели? С какой вер-стью это случится? Надо ли на такие доли процентов создавать сложные системы, способные держать такие нагрузки? Большинство компаний и команд и 10к РПС видели лишь во снах.

  • Будут добавлять новые фичи. Я не представляю себе синьера или мидла, которому дадут писать сервисы и фичи и он напишет так, что туда невозможно будет добавить фичу. Какие фичи? Перекрасить кнопку? Поменять название? Или новая фича уровня, вчера это был гугл, а сегодня это стал сайт по приюту для собак?

  • Если ошибемся придется все переписать. Еще раз, если вам пришлось все переписать, то даже 5 лет проектирования и описание каждой строчки аналитиком и тех. писателем - вас бы не спасло. Я не говорю сразу броситься кодить и собрать непонятно что, грумминг 1-2 часа - это ок, не надо только каждую строчку холиварить и каждый новый сервис гонять через 20 комитетов.

Выводы

Я не призываю начать писать плохой код, забить на архитектуру и начать делать как рука лежет. Нет, конечно надо следить за кодом, за тем, чтобы не делалась ерунда. Но, задайте себе вопрос, а почему мои синьеры или я сам выдаю такой код, почему архитектура которая рождается без круглосуточных архитектурных ревью и холиваров кривая по итогу. Я верю, что опытный и сильный синьер или техлид способен на основе требований от продукта собрать сильный рабочий код, который покроет потребности бизнеса. Если нет - тогда вопрос, почему нет? Чего не хватает, обучения, мотивации, опыта?

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

Если вам понравилась статья, то приглашаю вас в канал https://t.me/artur_speaking, там есть похожие темы, мысли и также я провожу трансляции и митапы на самые наболевшие темы.

Автор: Артур

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js