Шедевр безумного водопроводчика

Victor Bryksin
15 min readAug 8, 2017

--

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

Секция cross-cutting конечно немного портит впечатление

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

Интересная наверное дискуссия, если в ходе нее возникают такие картинки

Почему так получается? Почему при всей нашей внутренней любви к иерархии выстроить иерархичную систему получается крайне редко (обычно после десятого рефакторинга, если проект к тому времени не закрывается), если вообще получается? Почему даже выучив все эти бесконечные SOLID, CLEAN, DRY, KISS и прочие замечательные аббревиатуры, после их применения в итоге все равно получается нечто, состоящее из грязи и веток?

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

Если лень гуглить, то VP — это vice president

Картинка эта до боли напоминает картинку правильной архитектуры и представляет собой плод наших фантазий о той волшебной компании, в которой все процессы выстроены правильно и все находится на своих местах.

Интересно начинается тогда, когда вас просят нарисовать структуру вашей организации:

Я слева

Которая до боли напоминает архитектуру вашего последнего проекта:

Да, да, это тот самый синглтон посередине, который “в следующем релизе обязательно выпилим”

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

Со временем все портится

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

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

А в чем выражается эффективность архитектуры? Правильно—в ее гибкости, то есть в простоте внесения изменений. Но проблема в том, что архитектура — существо безмолвное и сама в себя изменения вносить не может. Изменения вносит программист, у которого есть свои метрики эффективности, которые могут конфликтовать с метриками архитектуры.

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

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

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

Итак, встретились программист и архитектура и один другой говорит:

— Мне надо внести в тебя изменения, чтобы багу починить.

— Не. — лениво отвечает архитектура.

— Как так? — Удивляется программист. — Тебя же проектировал К., наш лучший разработчик. Его даже в Гугл взяли потом, кофе варить.

— Ну вот так. Везде подумал ваш К., а тут не подумал. Ошибся. Не предусмотрел этот сценарий. Он же не пророк в конце концов и не мог догадываться, что через три года (огромный запас прочности!) вы захотите эти две вьюхи в одном контейнере анимировать.

И теперь у программиста есть два пути:

  1. пойти к менеджеру, рассказать сказку про плохую архитектуру и мерзкого К., который три года назад не подумал, получить квоту на рефакторинг и все починить;
  2. вставить залепу.

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

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

Кроме архитектуры. Она получила удар по почкам. Вносить в нее изменения стало сложнее. Но архитектура тварь безмолвная, её никто не спросил. О своих страданиях архитектура расскажет позже, в слезах программистов, в мозолях на пальцах, в мешках под глазами и в бесконечных митингах посвященных проблеме «как мы дошли до жизни такой».

А тропа натоптанная.

Незаменимых у нас есть

Вернемся к нашей крупной компании и ее Олечке из 205 кабинета, способной за шоколадку пересчитать последнюю сверку. Она же не просто так появилась. У нее в должностной инструкции нет никакого «пересчета за шоколадку» — она это делает, потому что ей так проще. Потому что в противном случае для коммуникации между двумя соседними отделами пришлось бы идти до их первого общего начальника со своими проблемами (знакомо, да?). А у первого общего начальника, который может быть начальником департамента закупок, своей головной боли хватает: то контрагенты отгрузку задерживают, то откаты не на те счета падают. И каждый раз, когда через него согласуют внесение правок в сверку, у него прямо пар из ушей идет, поэтому хитрые сотрудники, чтобы не беспокоить уважаемого человека лишний раз (и не подставляться под возможное лишение премии) научились делать нужные вещи в обход прямой иерархии подчинения.

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

Ведь для того, чтобы сделать все правильно, придется искать того самого первого общего предка и пытаться понять, в каких терминах он взаимодействует со своими зависимостями. Ведь Самый Главный Контроллер не может размениваться до каких-то там кнопок, ему нужны Самые Абстрактные Абстракции. Но делать их одна сплошная головная боль, и гораздо проще объяснить на ревью, что «архитектура плохая». Архитектура ни слова в свою защиту все равно не скажет. Годик поработает без багов и хорошо, а там, глядишь, и уволишься. Война все спишет.

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

После двадцатого изменения архитектура становится «запутанной». После пятидесятого — «ужасной». После сотого проходит тот самый год, когда уже не надо краснея объяснять HR, почему вы увольняетесь с прошлого места работы.

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

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

В конце концов, продуктивность работы не падает, так что все хорошо.

Здесь подкрадывается методологическая проблема. Кажется, что по мере накопления технического долга, производительность программистов должна уменьшаться, потому что вносить изменения в программу станет все сложнее. Но на самом деле если производительность не растет, это уже должно настораживать. Потому что программисты непрерывно обучаются, повышают квалификацию. Инструменты также непрерывно совершенствуются, и посему выходит, что производительность программистов должна непрерывно расти. Хотя бы как наша экономика — на 2% в год — но расти. А если она не растет, это может косвенно судить о том, что накопление технического долга в программе растет примерно сравнимо с ростом квалификации программиста.

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

Еще один очень важный момент хорошо проясняется, если вернуться к нашей корпоративной аналогии. Как вы понимаете, если на каждую задачу, которую тяжело решить в нормальной иерархии подчинения, находится своя Олечка из 205 кабинета, то по мере роста числа таких задач будет расти и число Олечек. И очень скоро может случиться так, что при увольнении одного, казалось бы, малозначительного сотрудника, встает раком очень важный и нужный для компании процесс, который раньше держался на личных договоренностях между этим сотрудником и всеми причастными.

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

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

Украли последний станок

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

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

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

Если получается так, что для некоторых реализаций у вас вставлены предусмотрительные костыли типа «если вдруг тут REST API, то покрути значком «работаю с сетью»» — то это случай частичной взаимозаменяемости.

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

Из инженерной практики известно, что полная взаимозаменяемость — это сложно, дорого, необходимо хорошее оборудование и рентабельно только при крупносерийном или массовом производстве. То есть, на языке программистов, если у вас два десятка классов за одним интерфейсом (например, Command) — то обеспечить полную взаимозаменяемость вам сам Б-г велел. В штучном и опытном производстве (то есть при прототипировании и создании уникальных классов) гораздо проще использовать операцию притирки. Выглядит она довольно странно — слесарь берет две детали, которые должны работать вместе, смазывает пастой ГОИ, полирует, а потом пробует вставить одну в другую. Если работает как надо — значит притирка удалась.

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

До боли напоминает те залепы, которые расставлены во множестве по коду, не правда ли?

И на самом деле, притирка в инженерной практики — это не признак плохой квалификации инженера, а просто самый дешевый способ обеспечить требуемую функциональность в единичном и опытном производстве.

А теперь давайте посчитаем:

  • сколько интерфейсов в вашей программе имеют единственную реализацию,
  • сколько классов и структур не имеют выделенного интерфейса,
  • сколько используется анонимных классов, структур, блоков, замыканий,

и сравним с числом интерфейсов, которые реализуют более чем один класс.

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

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

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

И по мере роста программы это станет огромной проблемой.

К вам коллекторы

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

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

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

С другой стороны, если вы уже в пятнадцатый раз пишете сетевой стек, то вполне можете написать его, даже не приходя в сознание.

«Проектирование архитектуры» каждый раз может быть совершенно новой задачей, похожая на предыдущую только названием. Это примерно как «написать класс». Некоторые пишутся за 5 минут, некоторые — несколько дней, в некоторых невозможно ошибиться, а к некоторым без тестов лучше вообще не приближаться. Поэтому фраза «мы умеем проектировать архитектуру» звучит для меня примерно как «мы умеем копать». Хорошо, вот скала — копайте.

Что значит «лопаты не берут»? Кто сказал, что копать можно только лопатами?

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

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

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

Архитектура должна отвечать потребностям проекта. А определить потребности в процессе разработки, когда ты еще толком не понимаешь, какие у тебя компоненты и как они взаимодействуют между собой, довольно проблематично. Особенно с учетом того, что «требования могут меняться» и других замечательных игр имени менеджмента. Конечно, есть CLEAN, есть SOLID, но примеры их применения в реальных проектах вы ежедневно видите в собственном проекте. Никто намерено не пишет плохой код. И никто не даст вам гарантий, что те две вьюхи, которые вы разнесли по разным компонентам в разных частях системы, не придется завтра связать классной анимацией перехода на масках.

Гордые программисты конечно могут заявить о том, что у меня просто руки кривые, а у них как раз все получается. Проблема только в том, что именно у них «получается» — действительно хорошая расширяемая вещь с запасом прочности на годы вперед, или очередная притертая система.

Но тогда сразу встает вопрос — является ли архитектура проекта чем-то большим, чем выражением боли и ретроспективой ошибок человека, который её разработал?

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

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

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

Для самых ленивых

Резюмируя все то, что я хотел сказать.

  1. Программист способен решить задачу хорошо только в том случае, если он ее уже решал, причем неоднократно. «Написать программу» — это не задача. Даже «написать класс» это не задача. «Написать вью контроллер для мессенджера с баблами и аватарками» уже больше похоже на задачу. «Спроектировать клиент для REST API на CRUD» — это задача.
  2. Наша работа устроена таким образом, что большую часть времени мы решаем новые задачи, а если вам повезло, и вы решаете одну и ту же задачу раз за разом, то я не понимаю, почему вас еще не уволили. Поэтому мы решаем их плохо: у нас просто нет опыта и понимания, мы пробуем то, что кажется нам правильным, но нет никакой гарантии, что это не ложный путь.
  3. В рамках агила от нас требуется скорость и результативность. И самый простой способ добиться их — это действовать рефлексивно, не оглядываясь на высокие материи. Есть всевозможные методики контроля качества кода, но они дают возможность избежать лишь самых экстремальных случаев плохого кода, по которому нет никаких вопросов.
  4. Результатом этого процесса является поток плохого кода, в который все сложнее вносить изменения. Но так как квалификация команды также растет со временем, а все двусмысленности кода со временем эффективно размещаются в их головах, то снижение качества кода может быть незаметно, так как видимая эффективность команды разработчиков не падает.
  5. Бороться с плохим кодом в процессе его появления бессмысленно, так как он является следствием недостатка опыта (или квалификации) в проектировании подобных вещей с одной стороны, и нечеткостью требований с другой стороны. От кода на этапе его написания требуется выполнение требуемой функциональность в форме абстракций, которые можно хотя бы объяснить на пальцах.
  6. Не надо намерено писать плохой код. Делайте хорошо то, что можете. Делайте правильно там, где нет сомнений в правильности выбранного подхода. А там, где вопросов больше чем ответов, лучше реализовать первое попавшееся решение, чем потратить время на бессмысленные споры и проектирование конвертоплана на паровой тяге.
  7. После того, как функциональность устаканится а у разработчиков появится понимание, как должен быть устроен этот компонент, исходя из тех проблем и ошибок, с которыми они столкнулись при разработке и сопровождении, можно собрать круглый стол или отправиться разгребать авгиевы конюшни в одиночестве.
  8. Так как у устоявшейся функциональности уже есть бизнес-ценность, то вероятность того, что она радикально поменяется, довольно мала (уточнить вероятность вы можете у вашего менеджера). Соответственно, архитектура, спроектированная исходя из потребностей сегодняшнего дня, будет гораздо лучше отвечать нуждам разработки, чем эфемерная архитектура, отвечающая вызовам дня завтрашнего.

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

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

Резюме twitter-style

Хорошая архитектура начинается с «мы уже так делали».

Плохая – с «давайте попробуем».

О практических подходах к «глобальному рефакторингу» мы поговорим чуть позднее.

--

--