Про инкрементальную архитектуру

Возможно, более употребимым термином является эволюционная архитектура (evolutionary architecture / design), но я впервые познакомился с этим понятием именно под таким названием, так что буду использовать его1. В этой статье я собираюсь рассказать о своем понимании этого предмета и еще немного о том, как я к этому пониманию шел.

Немного ворчания

Если полистать какие-нибудь ресурсы про разработку ПО, то складывается впечатление, что все нынче знают, как нужно разрабатывать софт: stories, спринты, continuous integration (лучше — delivery), рефакторинг, тесты, бла-бла-бла. Возможно я немного недоучка и что-то пропустил в этой жизни, но лично для меня все эти штуки в какой-то момент были в новинку. Я имею в виду, что все эти agile-штуки — это не то, чему я учился, не то, как я начинал разрабатывать софт. И не то, чтобы я был каким-то там динозавром, даже по меркам нашей индустрии. Может быть, я просто увлекался не теми вещами в юном возрасте. C++, велосипедные контейнеры, заигрывание с байтами и cast’ами… как-то не до передовых практик разработки, когда вокруг столько всякого интересного!

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

Знаете, это чем-то похоже на изучение ФП. Я помню, как тяжело было что-то делать в функциональном стиле в первые разы!.. Вот есть задача, которую нужно решить, вот вроде бы есть язык. Алгоритм вроде бы понятен, но ты сидишь, как идиот, и не знаешь, как подступиться. Очень хочется взять и написать старый добрый for. С agile у меня было похоже. Изучать что-то, что не вписывается в привычную картину мира, крайне сложно. Ну, по крайней мере для меня это так.

Инкрементальная архитектура курильщика

Итак, инкрементальная архитектура.

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

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

Потому что набрасывать нужно не структуру системы, которую мы собрались строить, а структуру системы на текущую стори. Максимум — на несколько стори вперед. Не более того. И это очень тяжело сделать в первый раз. Очень тяжело сказать ок, пока что нам хватит текстового файла вместо БД. Или, ок, сейчас нам не нужен SPA, сформируем страничку на сервере. Или, наоборот, нам не нужен сервер — хватит стандартного development server’а, раздающего наше JS-приложение.

Собственно, в такую ловкушку я и попал. Появились какие-то модули, какие-то потоки, какие-то скрипты… всякая шелуха, которая не имела отношения к решаемой задаче. Должен сказать, что строгое следование agile-практикам, вроде выпуска версии каждую итерацию и прочего, дало эффекты даже в таком изначально “криво посаженном” проекте. Вся ирония ситуации в том, что я не сразу осознал эту ошибку. Я верил, что все делаю правильно, с инкрементальной архитектурой, все как положено. Как же мы слепы в том, чего не знаем!

И, мне кажется, большинство проектов все-таки не использует инкрементальную архитектуру “на полную”. Ну, правда, какой смысл делать вид, что БД не понадобится, если мы знаем, что понадобится? Уже сто раз делали такие проекты. Я не оспариваю разумность такого решения. В каких-то случаях это, наверное, правильный подход. Понятно, что это не вопрос “все или ничего”. Не обязательно изображать из себя аквариумную рыбку и делать вид, что вообще ничего не знаешь о потребностях завтрашнего дня. Равно как и не обязательно выделять спринт на архитектуру. Но как же найти золотую середину?

Как сделать лучше?

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

Наверняка вам знакомы подобные цепочки размышлений: так, значит игрок перемещается по клеточкам, а некоторые клеточки имеют особое поведение, ну, скажем, разрушаемые блоки. Значит у нас будет специальный интерфейс IEnterable для объектов, в которые игрок может зайти. Так, логически игрок перемещается скачком, но сама моделька должна перемещаться плавно. А еще у нас будет Undo, поскольку мы делаем головоломку, значит у IEnterable еще должен быть метод для отката. Но, стоп, нам еще понадобятся такие IEnterable, в которые игрок, при определенных условиях, не сможет зайти — скажем, у него нет ключа нужного цвета. Значит у IEnterable должен быть еще один метод для проверки, можно ли туда зайти? Но как этот метод поймет, есть ли у игрока ключ? Мы же не хотим делать циклическую зависимость? Наверное, нужно сделать отдельный класс? Или тупо перенести логику в игрока?

Бла-бла-бла, все эти бесконечные диаграммы и размышления — не знаю как вы, но я в своей жизни немало времени провел за подобными размышлениями. Я не говорю, что думать совсем не надо. Просто не нужно брать слишком много за раз. В этом мало смысла. Хотя бы потому, что весь “придуманный” код все равно не удастся написать за один подход. Все равно придется обдумывать заново, когда дойдет до этих фич. Если вообще дойдет. Так, может быть, ограничиться “игрок перемещается по клеткам”? А потом отдельно обдумать и реализовать “разрушаемые блоки”? А если окажется, что задумка — отстой, то будет легко откатиться к раннему, более простому дизайну.

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

Рыбки наносят ответный удар

После был у меня и другой проект, который я делал уже фрилансером. На сей раз я был единственным программистом и уже стал немного рыбкой. И как же гладко все в этом проекте шло! За одним исключением, но то была просто сложная (для меня) задача, на понимание и решение которой ушло много времени. Возможно, я когда-нибудь даже напишу об этом.

Апофеозом же инкрементального подхода на этом проекте стал момент, когда я без особых усилий провел масштабный рефакторинг. На момент начала рефакторинга поведение системы уже было довольно сложным. И это была именно сложность задачи, а не т.н. accidential complexity. Судите сами: есть виртуальная комната, в которой пользователь может менять интерьер: добавлять, двигать, удалять и заменять мебель, менять обои, люстры, двери и т.п. Предметов интерьера много, загрузка их занимает заметное время (это WebGL). То есть, делать загрузку синхронно — не вариант. Кроме загрузки есть еще расчет позиции, который тоже асинхронный, потому что алгоритм довольно тяжелый, и, когда мебели в комнате становится много, расчет допустимых позиций растягивается на несколько кадров, чтобы не ломать плавность.

А вот теперь пример кейса, который нужно корректно обработать. Пользователь перетаскивает несколько картин на стену. Когда картины загрузятся, они должны разместится одна возле другой, не перекрывая друг друга. После этого пользователь выбирает одну из картин и начинает перебирать другие варианты из коллекции: он нажимает на иконки в списке и картина заменяется на новую. Новая картина может быть больше. В этом случае ее нужно подвинуть так, чтобы она не залезала на соседние. Если же после этого пользователь выберет картину, которая умещается и в предыдущей позиции, мы должны вернуть ее обратно. Это предотвращает “убегание” картины от исходной позиции при переборе вариантов. Если же картину невозможно подвинуть так, чтобы она уместилась, ничего не задев, она подсвечивается красным. Если пользователь займется чем-то другим (например, поставит в комнату диван), то мы должны откатить картину к тому виду, в каком она была до начала любых замен.

Я бы не сказал, что это какая-то космически сложная задача. Просто набор асинхронных операций, которые нужно правильно синхронизировать. При этом выполнение операции может порождать новые зависимые операции и их число заранее неизвестно (мы не знаем, сколько картин пользователь захочет перепробовать).

До рефакторинга в системе уже была возможность выполнять асинхронные изменения последовательно (построенная на корутинах, которые активно используются в Unity). При этом код, который обрабатывал UI, был синхронным. Какое-то время я пытался решать эту задачу таким образом, но это требовало большого количества костылей, вело к большому количеству багов, и в целом дизайн получался слишком сложным. Поэтому я задумал завернуть пользовательские действия в так называемые операции. То есть, завести какую-то новую концепцию, которой в системе до этого не было (были только Change, почти по GoF, но только асинхронные). И переделать на эту концепцию все пользовательские операции. Вспоминается гифка с котом?

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

Про рефакторинг

Иногда кажется, что новый дизайн принципиально не совместим со старым. Отсюда и появляются сказки про “неделю рефакторинга”. Но это не совсем так.

Вот есть у нас архитектура А, хотим прийти к архитектуре Б. Можно соединить их прямой линией — сделать все за один шаг, уйдя в рефакторинг с головой. А можно соединить ломаной линией из кучи маленьких сегментов: выделили класс, перенесли метод, добавили делегирование и т.п. — все те маленькие приемы, о которых писал Фаулер. Да, в итоге путь будет длиннее, мы затратим больше времени. Но. Зато мы гарантированно дойдем до конца! И не добавим кучу новых багов. И можем совместить этот процесс с разработкой новых фич, растянув его на недели и месяцы. А что бывает, когда начинаешь переделывать все сразу… ну, вы, наверное, сами знаете.

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

Почему же мы тогда не живем в мире идеального ПО? Тому я вижу несколько причин.

Во-первых, о такой возможности можно просто не догадываться. Все знают слово рефакторинг, но все ли его понимают одинаково? Вам, наверное, доводилось слышать (или говорить) что-то в духе “программа сейчас не комплируется, я в процессе рефакторинга”? Так вот, это не рефакторинг, по крайней мере, в том смысле, какой в это слово изначально вкладывался. Многие разработчики хотели бы улучшить систему, над которой они работают. Хотели бы все причесать, почистить, упростить. Но для них это выглядит как “месяц не делать фичи”, что, конечно же, никогда не будет одобрено. Но ведь на самом деле стопорить всю работу не требуется. Это даже вредно для рефакторинга, т.к. добавляет соблазн откусить за раз больше, чем следует. Если же взять на вооружение модель крошечных изменений, то все становится возможным.

Во-вторых, очень легко “упустить момент”. Особенность инкрементальной разработки в том, что она… ну, инкрементальная. То есть, мы каждый раз добавляем по чуть-чуть. Каждый раз работаем с маленьким кусочком системы. И когда мы смотрим на маленький кусочек, то он обычно понятен. И если мы к маленькому понятному кусочку добавляем еще один маленький понятный кусочек, то ведь все отлично? Ну и что, что это уже 15-й метод в классе. И хорошо, если всего лишь 15-й! А если 159-й? Тогда уже поздно. Тогда уже нужен “месяц на рефакторинг” или, лучше, вообще все переписать. Так вот сложность в том, чтобы понять, что класс требует рефакторинга при добавлении 15-го метода, а не 159-го. Например, в классическом TDD есть обязательная фаза рефакторинга. Сделал, чтобы работало — сделай, чтобы было красиво. Такой подход нужно применить не только с TDD.

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

Заключение

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

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

 

Примечания

  1. Кроме того, я очень хотел поставить картинку с кирпичами на обложку, а к эволюционной архитектуре кирпичи как-то не очень подходят…
  2. Забавный факт: второе издание вышло в 2004 году. В этом же году Мартин Фаулер написал Is Design Dead. В этом же году вышло второе издание XP Кента Бека. Не те вещи я в детстве читал?

One thought on “Про инкрементальную архитектуру

  1. Сильно сказано про ответственность, возьму на вооружение.

Leave a Reply