Технические проблемы в крупных цифровых продуктах далеко не всегда проявляются в виде аварий и остановок. Гораздо чаще система продолжает работать, пользователи заходят в приложения, а новые функции регулярно появляются. При этом внутри постепенно растет стоимость любых изменений: задача, которую раньше можно было сделать за два дня, начинает занимать неделю, а небольшая доработка требует проверки сразу нескольких частей программы.
Так проявляется технический долг. Он возникает постепенно, когда старые решения годами соседствуют с новыми. В какой-то момент сама архитектура начинает ограничивать скорость развития продукта.
О том, как распознать такой момент, почему полный отказ от старой системы далеко не всегда является решением и как менять архитектуру работающего продукта без остановки его работы, мы поговорили с Юсупом Изриповым, инженером-разработчиком программного обеспечения. Он специализируется на модернизации старых программных систем в крупных продуктах с высокой нагрузкой и участвовал в постепенном переходе больших приложений на современные технологии.
— Юсуп, в какой момент техническая проблема внутри программы становится проблемой бизнеса?
— Обычно это происходит не тогда, когда система перестает работать, а значительно раньше. Продукт продолжает обслуживать пользователей, новые функции выходят, но каждое изменение становится все дороже.
Например, раньше небольшую задачу можно было выполнить за два дня, а через несколько лет на нее уже требуется неделя. При этом функционально ничего особенно сложного не добавилось. Разработчику приходится разбираться в большем количестве старого кода, искать зависимости между разными частями программы, проверять исторические сценарии. Для бизнеса это означает потерю скорости. Конкурент может выпустить новую функцию за неделю, а компании приходится тратить на аналогичную задачу месяц. Технический долг напрямую начинает влиять на способность продукта развиваться.
— Почему технический долг возникает? Разве архитектуру системы изначально не проектируют так, чтобы подобных проблем не было?
— Почти никогда никто специально не создает плохую архитектуру. На конкретном этапе решение обычно выглядит вполне разумным. Нужно быстро проверить новую идею, поэтому разработчик выбирает самый простой способ. Нужно сохранить старую страницу, потому что ею продолжают пользоваться. Поджимают сроки, и новый компонент добавляют в уже существующую архитектуру. Другая команда решает похожую задачу своим способом, потому что так быстрее и не нужно ждать согласований.
Каждое такое решение может сэкономить несколько дней. Но если компромиссы накапливаются годами, в системе появляется несколько способов сделать одно и то же, старые и новые программные интерфейсы, разные подходы к хранению данных и управлению состоянием, компоненты разных поколений. В результате то, что когда-то ускоряло разработку, начинает ее замедлять.
— Как понять, что технический долг уже стал серьезной проблемой?
— Один из самых простых признаков — соотношение между размером задачи и количеством частей системы, которых приходится коснуться. Если нужно добавить небольшую функцию, а для этого приходится изучать несколько уровней архитектуры, старое хранилище данных, общие компоненты, соседние приложения и исторические сценарии, значит проблема уже не в конкретной задаче.
Еще один показательный сигнал — фраза: "Давайте лучше это не трогать, мы не знаем, что сломается". Когда разработчик не может примерно определить последствия изменения, значит в системе накопилось слишком много скрытых связей. Архитектура начинает диктовать бизнесу, что можно менять, а что лучше оставить как есть.
— В вашей практике был проект, где старую систему нужно было постепенно заменить. С чего начинается такая работа?
— В одном из крупных продуктов была задача перейти со старой технологии разработки интерфейса на React и TypeScript. При этом речь не шла о том, чтобы просто выбросить старую систему и написать новую. Сначала нужно было понять, из чего вообще состоит существующее приложение. Мы изучали зависимости между его частями, старые программные интерфейсы, получение данных, авторизацию, аналитику, общие компоненты и то, какие другие части продукта используют их поведение.
Можно увидеть старый модуль, который выглядит самостоятельным, а затем выяснить, что его состояние неявно используется еще в нескольких местах. Поэтому сначала нужно составить карту зависимостей и только после этого определять, какую часть системы безопасно менять первой.
— Почему нельзя просто переписать старую систему целиком? Что в постепенной модернизации самое сложное?
— На презентации полный переход выглядит очень привлекательно: была старая система, команда ее переписала, появилась новая. В работающем крупном продукте все сложнее. Пока новая система создается, старая продолжает обслуживать пользователей и развивается вместе с бизнесом. Через полгода новая версия может уже не соответствовать тому продукту, который существует в реальности.
Поэтому для больших систем обычно безопаснее постепенная замена. Берется ограниченная часть, для нее создается новый путь, проверяется его работа, на него постепенно переводятся пользователи, а старый вариант после этого удаляется. Это похоже на ремонт здания, в котором люди продолжают жить. Нельзя просто снести все сразу. Нужно менять его частями, сохраняя возможность пользоваться зданием.
Самая сложная часть часто происходит еще до написания нового кода. Нужно понять, кто использует старое решение, какие зависимости существуют между частями системы, какие функции нельзя нарушить, какие изменения происходили в прошлом и почему архитектура вообще стала такой.
Большая часть этой работы не выглядит впечатляюще. Это чтение старого кода, изучение истории изменений, поиск владельцев разных частей системы, сравнение поведения старой и новой реализации. Но именно здесь часто определяется успех всей дальнейшей работы. Если неправильно понять зависимости, новая система может выглядеть аккуратно, но при этом нарушить важный сценарий, который годами работал незаметно.
— Еще одна проблема крупных компаний — дублирование. Почему разные команды создают несколько решений для одной и той же задачи?
— Потому что на локальном уровне это может быть выгодно. Команде нужно решить свою задачу сейчас. Если создавать общее решение, придется договариваться с другими командами, менять их планы, согласовывать требования. Поэтому несколько команд независимо друг от друга создают похожие механизмы. Сначала это ускоряет работу. Через несколько лет появляется пять реализаций одного сценария. И тогда изменение, которое должно было произойти в одном месте, приходится повторять в пяти.
В одном из проектов, над которым я работал, была задача постепенно объединить несколько разрозненных приложений в единое хранилище исходного кода. Цель была не просто собрать все в одной папке, а постепенно выровнять используемые инструменты, зависимости и подходы, чтобы каждой команде не приходилось отдельно поддерживать собственную инфраструктуру разработки.
При этом объединять все похожее подряд тоже неправильно. Два компонента могут выглядеть одинаково, но иметь разные требования. Общее решение имеет смысл тогда, когда оно действительно уменьшает стоимость последующих изменений.
— Почему компании не могут выделить несколько месяцев и полностью заняться техническим долгом?
— Потому что у бизнеса есть другие задачи. Нужно выпускать новые функции, выполнять обязательства перед пользователями, развивать продажи, проводить эксперименты. Поэтому гораздо эффективнее работать с техническим долгом одновременно с развитием продукта.
Например, появляется новая функция, которую нужно добавить в старый модуль. Можно просто продолжить использовать старый подход. А можно использовать эту задачу как точку входа: определить границу нового решения, сделать новую часть более современной, постепенно вывести из нее старый код. Так технический долг уменьшается не отдельным большим проектом, а вместе с обычной разработкой.
— Получается, технический долг измеряется не количеством старого кода?
— Количество старого кода само по себе мало о чем говорит. Есть модули, которые давно написаны, но практически не меняются. Если они стабильно работают и не мешают развитию продукта, переписывать их только ради красоты может быть бессмысленно.
А есть сравнительно небольшой участок системы, который приходится постоянно менять. Если каждая новая функция требует в нем большого количества ручной работы и проверок, именно он становится настоящей проблемой. Поэтому я бы задавал вопрос не "Насколько старый этот код?", а "Во сколько нам обойдутся следующие десять изменений, если оставить все как есть?".
— Как в такой ситуации должен действовать сильный инженер? Можно ли вообще полностью избавиться от технического долга?
— Не обязательно начинать с самого сложного технического решения. Хороший инженер сначала пытается понять систему. Где находятся зависимости? Кто использует этот код? Что произойдет, если его изменить? Какой участок можно отделить без риска для всего продукта?
И часто результат выглядит довольно скучно. Пользователь ничего не замечает. Но внутри часть системы уже работает по новой схеме, старый код можно удалить, а следующую задачу команда выполняет быстрее. Для меня хороший результат модернизации именно такой: продукт продолжает работать, пользователи не сталкиваются с последствиями изменений, а команде становится проще развивать систему дальше.
Полностью избавиться от технического долга, наверное, нельзя. Любая работающая система постоянно меняется, и новые решения со временем тоже становятся устаревшими. Важно не пытаться сделать систему идеальной, а не позволять техническому долгу становиться препятствием для развития продукта.
Если определенная часть системы годами не меняется и не создает проблем, ее можно оставить в покое. Если же одно и то же старое решение начинает влиять на скорость разработки, стабильность выпусков и возможность запускать новые функции, тогда с ним уже стоит работать.
Технический долг действует медленно. Он редко ломает продукт одним днем. Гораздо чаще он постепенно забирает у компании скорость, которую она когда-то сэкономила, выбрав более быстрое решение. И задача инженера не в том, чтобы бесконечно переписывать старый код. Его задача в том, чтобы вовремя понять, какие накопившиеся компромиссы уже мешают продукту двигаться дальше, и убрать их с минимальным риском для работающей системы.