Головна / Статті / Чому абстракції фронтенду тихо перетворюються на технічний борг

Чому абстракції фронтенду тихо перетворюються на технічний борг

Дізнайтеся, чому передчасні абстракції фронтенду створюють приховану складність, та як визначити, чи справді варто створювати спільні компоненти, хуки чи утиліти.

2389 слів

У майже кожній базі коду фронтенду настає момент, коли вона поступово перестає бути звичайним додатком та стає фреймворком, створеним для підтримки додатку.

Спочатку вводиться бібліотека компонентів.

Потім з’являється система дизайну.

Далі до неї додається шар керування станом.

Потім з’являється абстракція для отримання даних.

Потім цю абстракцію обгортає власний хук.

Потім хтось пише універсальний компонент форми, який приймає об’єкт конфігурації, що описує поведінку форми.

Незабаром зміна чогось настільки простого, як кнопка, означає необхідність пройтися через шість файлів, три рівні абстракції та правила, винахідника яких неможливо знайти.

Дивно те, що жоден з цих окремих виборів на той момент не здавався нерозумним.

Саме в цьому полягає справжня проблема з тим, як команди фронтенду розбираються з абстракціями.

Більшість абстракцій за своєю суттю не є поганими, і багато з них дійсно допомагають. Проблема полягає у тому, що розробники фронтенду стали надзвичайно вправними у створенні абстракцій ще до того, як отримають достатньо доказів того, що вони справді потрібні.

Ми більше не просто абстрагуємо існуючу складність.

Ми абстрагуємо навіть саму можливість того, що складність може з’явитися колись у майбутньому.

Ця звичка створює особливий вид технічного боргу.

Абстракція зазвичай починається з добрих намірів

У першому є трохи дубльованої логіки завантаження.

У другому повторюється майже та сама схема.

У третьому знову робиться щось трохи інше.

Хтось помічає це повторення та пропонує:

"Мабуть, варто перенести це у спільну абстракцію."

Це саме по собі є слушним спостереженням.

Тож команда створює власний хук:

const { data, loading, error } = useUserData(userId);

Гарно та охайно.

Потім іншому компоненту потрібна невелика корекція поведінки.

Замість прямого звернення до API команда використовує інший варіант:

useUserData(userId, {
  includePermissions: true,
  cache: true,
  retry: 3,
});

Через кілька місяців хук більше не лише отримує дані користувачів.

Тепер він також займається кешуванням, повторними спробами, перевіркою дозволів, трансформацією даних, нормалізацією помилок, оптимістичними оновленнями та різними особливостями, притаманними конкретній функції.

Початкове дублювання зникло.

Але з’явилась ще одна проблема: зростаюча розбіжність між кодом та тим, що він насправді робить.

Подивившись на компонент, ви більше не можете зрозуміти, звідки беруться його дані.

Спочатку потрібно зрозуміти абстракцію, яка стоїть перед нею.

Це та жертва, про яку ніхто не згадує.

Абстракція не усуває складність.

Вона лише переміщує її.

Іноді таке переміщення справді має сенс.

Іноді ж ви просто обміняєте п’ять рядків прямолінійної логіки на триста рядків внутрішнього механізму.

Абстракція має вартість

Розробників з самого початку вчать розглядати дублювання як недолік.

Це цілком логічно, адже часто так і є.

Але дублювання — це зовсім не єдиний вид складності.

Інші форми включають:

  • непрямість
  • конфігурація
  • неявна поведінка
  • універсальні API
  • приховані залежності
  • конвенції
  • спадкування
  • компоненти-обгортки
  • відлагоджування, яке існує лише через саму абстракцію
  • когнітивне навантаження

Іноді просте повторення є справді дешевшим, ніж складна абстракція.

Порівняйте два підходи.

У одному випадку невеликий фрагмент логіки повторюється тричі.

У іншому створюється універсальний інструмент із десятком параметрів, що виправдовується тим, що три поточні сценарії використання перетинаються приблизно на 60 відсотків.

Другий варіант виглядає більш витонченим, більш „спроектованим“.

Однак його може бути значно складніше підтримувати.

Саме тут робота над фронтендом часто стикається з проблемами.

Команди оптимізують код за принципом DRY, а не за критерієм легкості розуміння.

Ці цілі не є взаємозамінними.

Екосистема фронтенду заохочує це

У розробці фронтенду існує особливий зв’язок із абстракцією, головним чином тому, що вся екосистема за своєю конструкцією побудована у шарах.

Одна сучасна додаткова програма може легко використовувати фреймворк для рендерингу для створення компонентів, якусь бібліотеку маршрутизації, менеджер стану на клієнтській стороні, окремий інструмент для керування станом на серверській стороні, а також бібліотеку форм разом із власним пакетом верифікації. До цього додається система дизайну, набір готових компонентів користувацького інтерфейсу, абстракція стилізації, що працює поверх звичайного 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 лише для того, щоб змінити один простий компонент, то абстракція, ймовірно, діє проти вас.

Це розрізнення має значення, тому що робота з фронтендом і так за своєю природою є складною. Браузери складні. Інтерфейси користувача складні. Підтримання синхронності даних складне. Доступність складна. Продуктивність складна. Немає потреби створювати додаткову складність замість того, щоб архітектура виглядала більш витонченою на папері.

Наступний етап розвитку фронтенду, ймовірно, не виникне завдяки відкриттю ще одного рівня абстракції. Він виникне завдяки кращому розумінню того, коли не варто його створювати.

Інженери, які виділяться, не обов’язково будуть тими, хто може розробити найбільш деталізовану систему компонентів, придатну для повторного використання. Це будуть ті, хто може розглянути проблему та правильно вирішити, чи взагалі потрібна система для її вирішення.

Іноді правильною абстракцією є окрема функція. Іноді — компонент. Іноді — просто добре названий модуль. А іноді — це п’ять рядків дубльованого коду, який кожен у команді може одразу прочитати та зрозуміти.

Вміння розрізняти ці ситуації — ось справжня навичка, яку варто розвивати.

Пов’язана література

  • Шаблони дизайну React: від класичного OOP до сучасних хуків — Пояснює, як класичні шаблони програмного забезпечення, такі як Singleton, Factory та Observer, застосовуються в React, а також шаблони, специфічні для React, як-от HOCs, хуки та складові компоненти.
  • Розуміння принципів SOLID за допомогою практичних прикладів коду — Цей посібник детально розглядає всі п’ять принципів SOLID за допомогою конкретних прикладів коду, показуючи, як вони застосовуються у реальних проектах та додатках на React.
  • Шість технік TypeScript, які перетворюють типи на ефективний засіб запобігання помилкам — Дізнайтеся, як satisfied, tagged unions, never checks, unknown, derived types та branded IDs допомагають TypeScript виявляти справжні помилки ще на етапі компіляції, а не під час роботи програми.