Почему абстракции фронтенда тихо превращаются в технический долг
Узнайте, почему преждевременные абстракции фронтенда создают скрытую сложность, и как определить, стоит ли вообще создавать общие компоненты, хуки или утилиты.
Почти в каждом кодовом базисе фронтенда наступает момент, когда он постепенно перестаёт быть приложением и превращается в фреймворк, предназначенный для его поддержки.
Сначала добавляется библиотека компонентов.
Затем появляется система дизайна.
Потом к ней присоединяется слой управления состоянием.
Далее появляется абстракция для получения данных.
Затем специальный хук оборачивает эту абстракцию.
После этого кто-то пишет универсальный компонент формы, принимающий объект конфигурации, описывающий поведение формы.
Вскоре изменение чего-то настолько простого, как кнопка, требует просмотра шести файлов, трёх уровней абстракции и соблюдения правил, происхождение которых никто не может восстановить до их создателя.
Странно то, что ни один из этих отдельных выборов в тот момент не казался неразумным.
В этом и заключается основная проблема того, как команды фронтенда работают с абстракциями.
Большинство абстракций по своей сути не являются плохими, и многие из них действительно помогают. Проблема в том, что разработчики фронтенда стали чрезвычайно искусны в создании абстракций до того, как у них появляются достаточные доказательства того, что эти абстракции действительно необходимы.
Мы больше не просто абстрагируем существующую сложность.
Мы абстрагируем даже саму возможность того, что сложность может появиться в будущем.
Такая привычка приводит к особому виду технического долга.
Абстракции обычно возникают из хороших намерений
Представьте три отдельных компонента, каждый из которых отвечает за загрузку данных пользователя.
У первого есть немного дублирующейся логики загрузки.
У второго почти такой же паттерн.
У третьего снова что-то немного иное.
Кто-то замечает эту повторяемость и предлагает:
«Вероятно, стоит перенести это в общую абстракцию».
Это само по себе разумное замечание.
Команда создаёт специальный хук:
const { data, loading, error } = useUserData(userId);
Всё аккуратно и организовано.
Затем другому компоненту требуется небольшая корректировка поведения.
Вместо прямого обращения к API команда находит альтернативу:
useUserData(userId, {
includePermissions: true,
cache: true,
retry: 3,
});
Через несколько месяцев хук уже не просто загружает записи пользователей.
Теперь он обрабатывает кэширование, повторные попытки, проверку прав, преобразование данных, нормализацию ошибок, оптимистичные обновления и различные особенности конкретных функций.
Исходное дублирование исчезло.
Но появилась другая проблема: растущий разрыв между кодом и тем, что он на самом деле делает.
При просмотре компонента уже невозможно понять, откуда берутся его данные.
Сначала необходимо понять абстракцию, находящуюся перед нами.
Это та компромиссная ситуация, о которой никто не говорит.
Абстракция не устраняет сложность.
Она лишь перемещает её.
Иногда такое перемещение действительно имеет смысл.
В других случаях вы просто заменяете пять строк простой логики на триста строк внутренних механизмов.
Абстракция имеет свою цену
У разработчиков с самого начала прививают мысль о том, что дублирование — это недостаток.
Это вполне обосновано, ведь часто так и бывает.
Но дублирование — это далеко не единственный вид сложности.
Другие формы включают:
- косвенность
- настройки
- непрямое поведение
- универсальные API
- скрытые зависимости
- конвенции
- наследование
- компоненты-обёртки
- отладка, существующая только из-за самой абстракции
- когнитивная нагрузка
Иногда дублирование действительно дешевле, чем сложная абстракция.
Сравните два подхода.
В одном случае небольшой фрагмент логики повторяется три раза.
В другом создается универсальный инструмент с дюжиной параметров, что оправдывается тем, что три текущих сценария использования покрываются примерно на 60 процентов.
Второй вариант выглядит более отлаженным, более «спроектированным».
Однако его может оказаться гораздо сложнее поддерживать.
Именно здесь разработка фронтенда часто сталкивается с проблемами.
Команды оптимизируют код в соответствии с принципом DRY, а не в пользу кода, который легко понимать.
Эти цели не являются взаимозаменяемыми.
Экосистема фронтенда поощряет это
У разработки фронтенда есть своеобразные отношения с абстракцией, в основном потому, что вся экосистема по своей конструкции состоит из слоев.
Одно современное приложение может легко включить в себя фреймворк для рендеринга для создания компонентов, какую-либо библиотеку маршрутизации, менеджер состояния на стороне клиента, отдельный инструмент для управления состоянием на стороне сервера, а также библиотеку форм с собственным пакетом валидации. Помимо этого используется система дизайна, набор готовых UI-компонентов, абстракция стилизации, построенная поверх обычного CSS, инструмент сборки для координации всего процесса и фреймворк тестирования для контроля всей конфигурации.
Каждый из этих элементов решает свою конкретную проблему.
Проблемы возникают, когда приложение начинает добавлять свои собственные пользовательские слои поверх всего этого.
Вместо прямого использования фреймворка разработчик обращается к внутреннему оберточному инструменту вокруг него.
Этот оберточный инструмент, в свою очередь, зависит от ещё одного оберточного инструмента.
Вскоре модель ежедневной разработки команды почти перестаёт соответствовать основной платформе.
Такая ситуация особенно часто встречается в крупных организациях.
Команда может оказаться в ситуации, когда структура выглядит примерно так:
<AppPage>
<DataBoundary>
<PermissionGate>
<FormContainer>
<EntityEditor />
</FormContainer>
</PermissionGate>
</DataBoundary>
</AppPage>
У каждого элемента есть определённая цель.
У каждого уровня есть задокументированная причина существования.
Но как только что-то ломается, разработчику приходится мысленно воссоздавать всю структуру, прежде чем добраться до самого кода функции.
Это воссоздание требует затрат как с точки зрения когнитивных усилий, так и времени.
Универсальные компоненты часто являются главной причиной проблем
Один из самых простых способов увеличения сложности фронтенда — это создание компонента, предназначенного для учёта всех будущих требований.
Всё начинается безобидно:
<Button />
Затем структура немного усложняется:
<Button variant="primary" />
После этого она продолжает расширяться:
<Button
variant="primary"
size="large"
loading
icon={...}
permission="admin"
analyticsEvent="save"
confirm
/>
В итоге у вас остаётся что-то, что технически уже не является кнопкой.
Это миниатюрная рамка для отображения произвольных действий.
Команда чувствует себя более продуктивной, поскольку теперь новые кнопки можно создавать с помощью конфигурации вместо полной переработки кода.
Но конфигурация всё равно остаётся кодом, независимо от того, кажется ли это таким образом.
В некоторых отношениях это даже хуже, потому что конфигурация скрывает реальный порядок выполнения операций.
Прочитав двадцать строк простого кода, вы можете точно понять, что происходит.
Прочитав двадцать строк конфигурации, вам может потребоваться изучить реализацию компонента, проследить работу парсера конфигурации, выяснить значение по умолчанию и понять, какие опции тихо взаимодействуют друг с другом.
Явный код фактически заменён на своего рода словарь команд.
Такой словарный запас действительно может оказаться мощным инструментом.
Но он так же легко может превратиться в диалект, поддержание которого никто не захочет брать на себя.
Ловушка «защиты от будущего»
Защита от будущих изменений обычно является самым сильным аргументом в пользу добавления какой-либо абстракции.
«Возможно, это понадобится позже».
«Вероятно, в будущем появится больше вариантов».
«Это можно будет использовать в других частях приложения».
«Давайте с самого начала создадим его в универсальном виде».
Иногда такой инстинкт бывает верным. Но гораздо чаще у вас просто ещё недостаточно информации, чтобы что-то знать наверняка.
Проблема в том, что любая абстракция содержит предположения относительно проблемы. Чрезмерная абстракция означает фиксацию архитектурных решений до того, как вы действительно поймете, что создаете. И как только другие части кодовой базы начинают зависеть от этой абстракции, изменение курса становится очень дорогостоящим.
Именно поэтому ранняя абстракция опаснее, чем кажется. Дублированный код обычно легко перерабатывать, когда вы готовы. Однако некорректная абстракция склонна распространяться дальше.
Представьте три компонента, которые выглядят похоже. Вы можете пока оставить дублирование как есть. Если со временем появится реально общий шаблон, тогда его и извлеките. Но если вы сразу перейдете к обобщенной абстракции, каждый будущий случай использования будет принужден соответствовать тем предположениям, которые вы сделали в начале.
В этот момент абстракция перестаёт быть удобством и становится ограничением. То, что должно было устранить повторения, в итоге затрудняет изменения больше, чем само дублирование когда-либо могло бы.
Хорошие абстракции обычно рождаются из трудностей
Самые сильные абстракции, как правило, не планируются заранее. Они открываются в процессе работы.
Команда реализует одно и то же поведение несколько раз. В конце концов они замечают, какие части действительно идентичны, а какие кажутся похожими лишь поверхностно. Они понимают, в чём на самом деле разница. Только тогда они выделяют стабильную, общую основу.
Это создаёт гораздо более прочную основу. Эту последовательность можно описать так:
Дублирование приводит к повторениям, повторения приводят к пониманию, а понимание приводит к абстракции.
Однако команды фронтенда часто следуют другому пути:
Возможность приводит непосредственно к абстракции, затем к конфигурированию, а потом к путанице.
Первый подход требует больше времени на начальном этапе. Однако за весь срок существования проекта он обычно оказывается быстрее в целом, поскольку полученная абстракция отражает знания, которые команда действительно приобрела благодаря опыту.
Не всю дублированную часть следует удалять
Для разработчиков это неприятная истина, поскольку дублирование по умолчанию воспринимается как ошибка. Но иногда дублирование — это правильное решение.
Предположим, два компонента используют практически идентичную логику проверки. Если эта логика состоит всего из пяти строк, а каждый компонент соответствует разным бизнес-правилам, то сохранение дублированного кода может оказаться более разумным вариантом.
Почему? Потому что разделение их сохраняет локальное понимание. Человек, работающий позже над одним компонентом, может скорректировать его поведение, не беспокоясь о том, что случайно нарушит работу другого.
Дублированный код подразумевает, что эти два элемента сейчас просто похожи друг на друга.
Абстракция говорит о чем-то гораздо более сильном: эти два элемента предназначены быть идентичными, и они должны меняться вместе в будущем.
Это более серьезное утверждение. К абстракции следует обращаться только тогда, когда вы действительно верите в его истинность.
У абстракций должен быть небольшой API
Полезная практическая проверка заключается в следующем: сколько времени и знаний на самом деле требуется, чтобы правильно использовать данную абстракцию?
Если этот список продолжает расти, скорее всего, вы наблюдаете, как абстракция превращается в фреймворк.
Хорошая абстракция скрывает сложность. Плохая абстракция скрывает решения, принятые разработчиками. Это звучит похоже, но на самом деле это совершенно разные вещи.
Хорошо спроектированный API может выглядеть так просто:
const user = useUser(id);
А плохо спроектированный API начинает заполняться флагами и опциями, в результате чего выглядит примерно так:
const user = useUser(id, {
cache: true,
normalize: true,
permissions: true,
optimistic: false,
retry: 3,
suspense: false,
transform: customTransform,
mode: "editor",
});
В какой-то момент абстракция перестаёт упрощать что-либо. Она просто перемещает первоначальную проблему в объект конфигурации. Такой сдвиг следует рассматривать как тревожный сигнал.
Командам фронтенда нужен бюджет на абстракции
Команды уже регулярно говорят о бюджетах на производительность, размер пакетов, количество ошибок и инфраструктуру. Стоит добавить к этому списку ещё и бюджет на абстракции.
Цель не обязательно заключается в уменьшении количества абстракций в абсолютных цифрах. Речь идёт о том, чтобы количество абстракций оставалось в пределах того, что команда может легко запомнить.
Прежде чем добавлять что-то новое, полезно задать несколько вопросов.
Сколько раз уже появлялась именно такая схема? Если ответ — один раз, пока не стоит создавать абстракцию. Если два раза, это следует рассматривать как повод для подозрений, а не для действий. Пять случаев уже начинают выглядеть как реальные доказательства.
Далее: действительно ли эти изменения происходят по тем же причинам? Похожесть сама по себе не является достаточным основанием. Два компонента могут казаться практически идентичными, но развиваться по совершенно разным путям по разным деловым причинам — в таком случае объединение их в одну абстракцию может оказаться ошибкой.
Наконец: действительно ли эта абстракция упрощает повседневные ситуации? Речь не о каких-то гипотетических будущих случаях, а о тех, с которыми люди сталкиваются постоянно. Если использование абстракции означает необходимость каждый раз перечитывать её документацию, скорее всего, вы заменили одну проблему на более серьезную.
Лучший код фронтенда часто бывает скучным
В процессе разработки программного обеспечения существует своя странная иерархия ценностей. Умное абстрагирование воспринимается как более впечатляющее, чем три простых компонента. Генерическая система кажется более серьезной с архитектурной точки зрения, чем простая функция. Переиспользуемая рамка выглядит более профессиональной, чем небольшой фрагмент дублирующегося кода.
Но производственные системы не оценивают код за его сложность внешнего вида. Они оценивают код, который разработчики могут безопасно изменять.
Самый ценный код фронтенда, как правило, является именно таким скучным. При открытии компонента сразу становится ясно, откуда берутся его данные. Видно точно, что происходит при нажатии пользователя на какой-либо элемент. Можно напрямую редактировать маркировку. Возможно отслеживание того, как меняется состояние. Не составляет труда найти вызов API.
Вам не следует вынужденно усваивать внутреннюю философию проектирования команды лишь для того, чтобы исправить баг. Это не признак некомпетентного инжиниринга — это признак качественного инжиниринга.
Абстракция должна упрощать мышление, а не усложнять его
Абстракция существует не для того, чтобы код казался переиспользуемым. Её цель — сделать систему более понятной для анализа. Именно этому стандарту должны соответствовать все абстракции в командах фронтенд-разработки.
Если абстракция позволяет десяти компонентам совместно использовать сложное поведение, не заставляя каждого разработчика понимать, как это поведение реализовано, она выполняет свою функцию. Если же каждому разработчику приходится изучать сложный API лишь для того, чтобы немного изменить один простой компонент, скорее всего, абстракция работает против вас.
Это различие важно, потому что работа с фронтендом и так по своей природе сложна. Браузеры сложны, пользовательские интерфейсы сложны, синхронизация состояния — сложна, доступность — сложна, производительность — сложна. Нет необходимости создавать дополнительную сложность сверх всего этого лишь для того, чтобы архитектура выглядела более изысканной на бумаге.
Следующий этап развития фронтенда, скорее всего, не будет связан с открытием ещё одного уровня абстракции. Он будет связан с улучшением способности понимать, когда не стоит его создавать.
Инженеры, которые выделятся, не обязательно будут теми, кто может спроектировать самую сложную систему переиспользуемых компонентов. Это будут те, кто может рассмотреть проблему и правильно определить, действительно ли она требует наличия такой системы.
Иногда подходящей абстракцией является отдельная функция. Иногда — компонент. Иногда — просто хорошо названный модуль. А иногда — пять строк дублирующегося кода, которые любой член команды может сразу прочитать и понять.
Умение различать эти ситуации — вот настоящий навык, который стоит развивать.
Связанные материалы
- Десять архитектурных привычек, которые позволяют сохранять код фронтенда пригодным к использованию в течение многих лет — рассказывает о структурных привычках, таких как оптимизация для удаления элементов, четкий поток данных и изоляция бизнес-логики, которые помогают кодовым базам оставаться пригодными к использованию на протяжении многих лет изменений.