Бывало, открываешь код-ревью — и чувствуешь себя археологом. Каждый кусок кода — как артефакт из разных времен: тут блестит бронзовая монетка, там торчит бивень мамонта, а чуть дальше — отпечатки времен .NET 4, пережившие три рефакторинга. Все это чудом взаимодействует, но порой страшно тронуть — вдруг вся конструкция рассыплется.
Эта история знакома многим командам. Мы привыкли думать, что хороший фреймворк — это гибкий фреймворк. Что чем больше у него возможностей, тем лучше. И действительно: гибкость помогает выйти на рынок, быстрее выпустить первую версию, подстроиться под новые требования. Но в какой-то момент эта гибкость начинает мешать.
Мы в команде разработки пользовательского интерфейса поняли это, когда наш общий код перестал быть общим: его было слишком много, он жил своей жизнью, и никто уже не знал, что в нем есть и как оно работает. С этого начался Kaspirin — наш внутренний фреймворк, который мы создали не для расширения возможностей, а чтобы навести порядок и убрать лишнюю вариативность. Название придумалось само собой: смесь Kaspersky и aspirin — лекарство от головной боли, вызванной избыточной гибкостью.
Как свобода разработки превращается в квест по археологии
На старте проекта гибкость — спасение. Команда пробует подходы, пишет вспомогательный код, торопится выпустить первый релиз. На этом этапе свобода важна: нужно быстро собирать функциональность и реагировать на изменения.
Но по мере взросления продукта гибкость теряет ту ценность, что имела на старте. Появляются библиотеки, выстраиваются процессы, а набор задач постепенно стабилизируется. Теперь от фреймворка ждут не новых возможностей, а стабильности: все нужное уже придумано и работает. И вот тут гибкость превращается из помощницы в источник хаоса.
Когда фреймворк предлагает десятки способов сделать одно и то же, команда рано или поздно начинает пользоваться ими всеми сразу. Так и появляется «зоопарк решений» — каждый пишет по-своему, потому что может.
Пример №1: троттлеры и таймеры
При разработке UI потребность в троттлерах и таймерах — не редкость, но каждая ситуация требует тонкой настройки. Сами они писались редко — проблема была в другом. В проекте был общий набор таких инструментов, и у каждого разработчика со временем появлялись свои фавориты. Кто-то предпочитал один троттлер, кто-то другой — и тянул за собой именно тот, с которым уже разобрался. Если требовалась доработка, то человек правил именно знакомый вариант, не оглядываясь на остальные. В итоге инструменты развивались вразнобой, и фреймворк, разумеется, не мог это ограничить. На деле требовались инвентаризация и унификация.
Пример №2: кнопка (особенно показательный)
На макете к тексту кнопки добавили иконку. В кодовой базе не было стандартного способа сделать это, и каждый реализовывал такую задачу по-своему. Потом появились версии со спиннером, с несколькими строками текста — и кнопки начинали отличаться. Это плохо вдвойне: если «зоопарк решений» в коде видят только разработчики, то разномастные кнопки в продукте замечают и пользователи. От окна к окну одну и ту же кнопку «шатает» по-разному. Теряется единообразие, и портится ощущение надежности продукта — будто что-то делали на коленке.
Так шаг за шагом гибкость перестает помогать. Когда в проекте нет единого способа решить задачу, ревью превращается в формальность. Появляется ощущение «и так сойдет!», код начинает плыть — вместе с уверенностью, что система под контролем.
Когда гибкость ломает процесс
Иногда последствия проявляются не в архитектуре, а прямо в повседневной работе, например, когда в соседнем отделе решают похожую задачу — но делают это по-своему. В итоге получается несколько разных реализаций одного и того же, и каждая работает по чуть отличным правилам. Потом кто-то уходит в отпуск, приходит новый сотрудник, а в итоге разобраться, что было правильным, уже невозможно.
Можно сказать, что это вопрос процессов и коммуникации. Но если бы фреймворк не позволял так свободно отходить от стандартных решений, таких расхождений могло бы просто не возникнуть. Ограничения не мешали бы — наоборот, помогали бы командам действовать согласованно.
Наш ответ — Kaspirin: фреймворк, который наводит порядок
Когда стало ясно, что общий код живет сам по себе и никто не знает, что в нем лежит, мы решили сделать надстройку над WPF — внутренний фреймворк Kaspirin. Его задача — не расширять возможности, а наоборот, ограничивать их, наводя порядок и выравнивая подходы.
Архитектурно Kaspirin — это набор библиотек, каждая из которых предоставляет инструментарий, помогающий в разработке UI: визуальные компоненты, работу с темами, продуктовую палитру, наборы шрифтов и иконок, навигацию, локализацию, механизмы кастомизации, анимации, логирование, работу с асинхронностью и так далее. Идея в том, чтобы все эти части жили по единым правилам.
Ограничения и унификации, которые мы ввели:
Мы зафиксировали сквозной набор базовых сущностей, который одинаково используется и в макетах, и в коде.
Фиксированный набор цветовых токенов (продуктовая палитра), шрифтов, иконок и иллюстраций совпадает с тем, что дизайнеры используют в Figma.
Названия в макетах и названия в продукте идентичны — разработчику не нужно гадать, какой шрифт, цвет или компонент выбрать. Он просто берет нужный элемент из Kaspirin, и он полностью соответствует тому, что есть на макете.
Эта унификация сквозная — от дизайн-системы до фреймворка — и поддерживается автоматически. Благодаря ей дизайнеры и разработчики говорят на одном языке: каждому компоненту в макете соответствует свой компонент в Kaspirin, и визуальная часть компонентов закрыта для изменений — менять можно только содержимое, но не внешний вид. Например, базовый фреймворк позволяет перекрасить кнопку прямо в разметке:
<Button Background="#FF0000" ... />
В нашем случае такое не пройдет — фон нельзя менять точечно. Можно только выбрать тип кнопки:
<visuals:Button Type="Secondary" ... />
То же самое с наследованием и ��идимостью классов. Если раньше все методы были публичными и их можно было переопределять как угодно, то при переносе кода в Kaspirin мы осознанно задаем уровень видимости так, чтобы использовать класс можно было только предусмотренным способом. Формируются внешняя и внутренняя части фреймворка — осознанное ограничение во имя стабильности.
Мы не принуждаем — мы воспитываем. У нас есть и Roslyn-анализаторы, и элементы кодогенерации, и кастомные MSBuild-таски, и, конечно, все эти инструменты помогают нам соблюдать чистоту и порядок в общей кодовой базе, но главное не в этом. Люди бы с радостью не писали кастомные решения, если бы знали, где искать стандартные. Поэтому мы делаем демки для отдела, ведем канал с новостями о коммитах и пишем документацию — чтобы каждый знал, что у него под рукой есть готовый инструмент.
Как мы внедряли Kaspirin на живых проектах
Интеграция Kaspirin шла не одномоментно, а по шагам. После разработки каждого компонента или инструмента создавалась отдельная задача на его внедрение — по одной на каждый продукт. Эти задачи постепенно попадали в скоуп релиза, и так фреймворк внедрялся в живые проекты. Критерий успеха был прост: чем больше старого кода удалось удалить — тем лучше. Удаление зоопарка решений и их замена на унифицированные инструменты из фреймворка означали, что цель достигнута.
Со временем эффект стал ощутимым. Количество косметических багов снизилось, интерфейс стал выглядеть консистентнее. Исчезли постоянные вопросы от коллег — теперь всем понятно, куда идти за нужным инструментом и где искать решение. Замечания по ревью вроде «так делать не надо» почти перестали появляться. Команда сосредоточилась на реализации бизнес-требований и продуктовых сценариев, а не на разборе того, почему «менюшку перекосило» или «в RTL все съехало».
Средняя скорость ревью не уменьшилась — даже наоборот, проверки стали строже, потому что принцип «и так сойдет!» больше не работает. Заодно и ревью стали точнее: на них обсуждается не возникший хаос, а конкретные улучшения.
Как не превратить лекарство от хаоса в новую зависимость
Наполнение таких библиотек — не тот случай, когда стоит торопиться. Всегда есть соблазн добавить туда что-то новенькое, только что сделанное, но на этом легко обжечься. Каждую доработку стоит рассматривать через простые вопросы:
Насколько она действительно нужна?
Как часто ей будут пользоваться?
Впишется ли она в общий инструментарий?
Если все складывается — добавляем. Если нет, держим в уме: такая наработка теперь есть в продукте и, если похожие решения будут появляться снова, мы вернемся к ней и подумаем, стоит ли ее обобщить и вынести в общий код.
Здесь важно не загонять всех в рамки ради рамок. Ведь потребности выходить за них все ровно возникают, и в таких случаях мы стараемся делить креатив на осознанный (например, инициативы по комплексному редизайну), и случайный (вроде внезапных экспериментов от джунов). Первый поддерживаем, второй стараемся не пускать в общий код.
Что насчет планов развития?
Мы еще не выложили все, что хотели: на очереди полный набор иконок, обновленная палитра, наши шрифты. Также готовим к публикации наш ToolKit — утилиту для просмотра инструментов фреймворка.
За кулисами Kaspirin есть еще одна часть, о которой уже шла речь ранее, — подсистема кодогенерации стилей компонентов. Она интегрируется с Figma и позволяет переносить стили из веба в продукт. Мы планируем открыть ее, но пока не можем: код еще не в финальном виде.
В остальном мы просто движемся дальше — добавляем новые компоненты, обновляем и дорабатываем уже имеющиеся, дополняем документацию и улучшаем инструменты. Планируем обновлять наш код на GitHub примерно три-четыре раза в год, чтобы держать систему в актуальном состоянии.
Чему нас научил Kaspirin
Работа над Kaspirin показала, что для эффективной разработки большой кодовой базы важно не столько создавать новые инструменты, сколько контролировать вариативность уже существующих решений. «Зоопарк» подходов к одной и той же задаче всегда оборачивается усталостью — от ревью, от попыток разобраться, кто, как и зачем это сделал.
Когда команда развивает собственный инструментарий, вовремя замечает общие потребности и фиксирует лучшие практики в единых правилах, у людей просто пропадает желание сочинять одноразовые решения. Код становится понятнее, ревью — спокойнее, плюс формируется культура бережного отношения к общему коду.
Иногда действительно нужно не добавлять фреймворку возможностей, а закрывать лишние. Это не ограничение ради контроля, а классный способ освободить команду от хаоса и вернуть фокус на задачу.
Гибкость нужна, чтобы стартовать.
А порядок — чтобы жить дальше.