Это глава 44 раздела «SDK и UI-библиотеки» моей книги «API». Второе издание книги будет содержать три новых раздела: «Паттерны API», «HTTP API и REST», «SDK и UI‑библиотеки». Если эта работа была для вас полезна, пожалуйста, оцените книгу на GitHub, Amazon или GoodReads. English version on Substack. Перейдём к более предметному разговору и попробуем объяснить,  почему требование возможности замены одной из подсистем компонента  приводит к кратному усложнению интерфейсов. Продолжим рассматривать  пример компонента SearchBox из предыдущей главы. Напомним, что мы выделили следующие факторы, осложняющие проектирование API визуальных компонентов: объединение в одном объекте разнородной функциональности, а именно —  бизнес-логики, настройки внешнего вида и поведения компонента; появление разделяемых ресурсов, т.е. состояния объекта, которое  могут одновременно читать и модифицировать разные акторы (включая  конечного пользователя); неоднозначность иерархий наследования свойств и опций компонентов. Сделаем задачу более конкретной, и попробуем разработать наш SearchBox так, чтобы он допускал следующие модификации: Замена списочного представления предложений, например, на представление в виде карты с подсвечиваемыми метками: иллюстрирует проблему полной замены одного субкомпонента (списка  заказов) при сохранении поведения и дизайна остальных частей системы, а  также сложности имплементации разделяемого состояния; Комбинирование краткого и полного описания предложения в одном  интерфейсе (предложение можно развернуть прямо в списке и сразу сделать  заказ): иллюстрирует проблему полного удаления одного из субкомпонентов с передачей его бизнес-логики другим частям системы; Манипуляция доступными действиями для предложения через добавление  новых кнопок (вперёд, назад, позвонить) и управление их содержимым. В этом сценарии мы рассматриваем различные цепочки пропагирования  информации и настроек до панели предложения и динамическое построение UI  на их основе: часть данных является свойствами реального объекта (логотип, номер телефона), полученными из API поиска предложений; часть данных имеет смысл только в рамках конкретного UI и отражает механику его построения (кнопки «Вперёд» и «Назад»); часть данных (иконки отмены и звонка) связаны с типом кнопки (бизнес-логикой, которую она несёт в себе). Решение, которое напрашивается в данной ситуации — это выделение  двух дополнительных компонентов, отвечающих за представление списка  предложений и за панель показа конкретного предложения, назовём их OfferList и OfferPanel. В случае отсутствия требований кастомизации, псевдокод,  имплементирующий взаимодействие всех трёх компонентов, выглядел бы  достаточно просто: Интерфейсы ISearchBox / IOfferPanel / IOfferView также очень просты (конструкторы и деструкторы опущены): Если бы мы не разрабатывали SDK и у нас не было бы задачи разрешать  кастомизацию этих компонентов, подобная реализация была бы стопроцентно  уместной. Попробуем, однако, представить, как мы будем решать описанные  выше задачи: Показ списка предложений на карте: на первый взгляд, мы можем  разработать альтернативный компонент показа списка предложений,  скажем, OfferMap, который сможет использовать стандартную панель предложений. Но у нас есть одна проблема: если OfferList только отправляет команды для OfferPanel, то OfferMap  должен ещё и получать обратную связь — событие закрытия панели, чтобы  убрать выделение с метки. Наш интерфейс подобного не предусматривает.  Имплементация этой функциональности не так и проста: Нам пришлось создать новый класс CustomOfferPanel, который, в отличие от своего родителя, теперь работает не с любой имплементацией интерфейса IOfferList, а только с IOfferMap. Полные описания и кнопки действий в самом списке заказов — в этом  случае всё достаточно очевидно: мы можем добиться нужной  функциональности только созданием собственного компонента. Даже если мы  предоставим метод переопределения внешнего вида элемента списка для  стандартного компонента OfferList, он всё равно продолжит создавать OfferPanel и открывать его по выбору предложения. Для реализации новых кнопок мы можем только лишь предложить  программисту реализовать свой список предложений (чтобы предоставить  методы выбора предыдущего / следующего предложения) и свою панель  предложений, которая эти методы будет вызывать. Даже если мы придумаем  какой-нибудь простой способ кастомизировать, например, текст кнопки  «Сделать заказ», его поддержка всё равно будет ответственностью  компонента OfferList: Неприятная особенность всех вышеперечисленных решений — их очень  плохая расширяемость. Вернёмся к п.1: допустим, мы решили сделать  функциональность реакции списка предложений на закрытие панели  предложений частью интерфейса, чтобы программист мог ей  воспользоваться. Для этого нам придётся объявить новый метод, который  в целях обратной совместимости будет необязательным: и писать в коде OfferPanel что-то типа: Что, во-первых, совершенно не красит наш код и, во-вторых, делает связность OfferPanel и OfferList ещё более сильной. Как мы описывали ранее в главе «Слабая связность»,  избавиться от такого рода проблем мы можем, если перейдём от сильной  связности к слабой, например, через генерацию событий вместо вызова  методов. Компонент IOfferPanel мог бы бросать событие 'close', и тогда OfferList мог бы на него подписаться: Код выглядит более разумно написанным, но никак не уменьшает взаимозавимость компонентов: использовать OfferList без OfferPanel, как этого требует сценарий #2, мы всё ещё не можем. Заметим, что в вышеприведённых фрагментах кода налицо полный хаос с уровнями абстракции: OfferList инстанцирует OfferPanel и управляет ей напрямую. При этом OfferPanel  приходится перепрыгивать через уровни, чтобы создать заказ. Мы можем  попытаться разомкнуть эту связь, если начнём маршрутизировать потоки  команд через сам SearchBox, например, так: Теперь OfferList и OfferPanel стали  независимы друг от друга, но мы получили другую проблему: для их замены  на альтернативные имплементации нам придётся переписать сам SearchBox. Мы можем абстрагироваться ещё дальше, поступив вот так: То есть заставить SearchBox транслировать события, возможно, с преобразованием данных. Мы даже можем заставить SearchBox транслировать любые  события дочерних компонентов, и, таким образом, прозрачным образом  расширять функциональность, добавляя новые события. Но это совершенно  очевидно не ответственность высокоуровневого компонента — состоять, в  основном, из кода трансляции событий. К тому же, в этих цепочках  событий очень легко запутаться. Как, например, должна быть реализована  функциональность выбора следующего предложения в offerPanel (п. 3 в нашем списке улучшений)? Для этого необходимо, чтобы OfferList не только генерировал сам событие 'offerSelect',  но и прослушивал это событие на родительском контексте и реагировал на  него. В этом коде легко можно организовать бесконечный цикл: Во избежание таких циклов мы можем разделить события: Но тогда код станет окончательно неподдерживаемым: для того, чтобы открыть панель предложения, нужно будет сгенерировать 'click' на инстанции класса OfferList. Итого, мы перебрали уже как минимум пять разных вариантов организации  декомпозиции UI-компонента в самых различных парадигмах, но так и не  получили ни одного приемлемого решения. Вывод, который мы должны  сделать, следующий: проблема не в конкретных интерфейсах и не в  подходе к решению. В чём же она тогда? Давайте сформулируем, в чём состоит область ответственности каждого из наших компонентов: SearchBox отвечает за предоставление общего интерфейса.  Он является точкой входа и для пользователя, и для разработчика. Если  мы спросим себя: какой максимально абстрактный компонент мы всё ещё  готовы называть SearchBox-ом? Очевидно, некоторый UI для  ввода поискового запроса и его отображения, а также какое-то абстрактное  создание заказа по предложениям. OfferList выполняет функцию показа пользователю  какого-то списка предложений кофе. Пользователь может  взаимодействовать со списком — просматривать его и «активировать»  предложения (т.е. выполнять какие-то операции с конкретным элементом списка). OfferPanel представляет одно конкретное предложение и отображает всю  значимую информацию для пользователя. Панель предложения всегда ровно  одна. Пользователь может взаимодействовать с панелью, активируя  различные действия, связанные с этим конкретным предложением (включая  создание заказа). Следует ли из определения SearchBox необходимость наличия суб-компонента OfferList? Никоим образом: мы можем придумать самые разные способы показа пользователю предложений. OfferList — частный случай, каким образом мы могли бы организовать работу SearchBox-а по предоставлению UI к результатами поиска. Следует ли из определения SearchBox и OfferList необходимость наличия суб-компонента OfferPanel? Вновь нет: даже сама концепция существования какой-то краткой и полной  информации о предложении (первая показана в списке, вторая в панели)  никак не следует из определений, которые мы дали выше. Аналогично,  ниоткуда не следует и наличие действия «выбор предложения» и вообще  концепция того, что OfferList и OfferPanel выполняют разные действия и имеют разные настройки. На уровне SearchBox вообще не важно, как результаты поиска представлены пользователю и в каких состояниях может находиться соответствующий UI. Всё это приводит нас к простому выводу: мы не можем декомпозировать SearchBox  просто потому, что мы не располагаем достаточным количеством уровней  абстракции и пытаемся «перепрыгнуть» через них. Нам нужен «мостик» между  SearchBox, который не зависит от конкретной имплементации UI работы с предложениями и OfferList/OfferPanel,  которые описывают конкретную концепцию такого UI. Введём  дополнительный уровень абстракции (назовём его, скажем, «composer»),  который позволит нам модерировать потоки данных: Здесь методы-строители подчинённых компонентов, позволяющие  переопределять опции компонентов (и, потенциально, их расположение на  экране) выглядят как: Мы можем придать SearchBoxComposer-у функциональность трансляции любых контекстов. В частности: Трансляцию данных и подготовку данных. На этом уровне мы можем предположить, что OfferList показывает краткую информацию о предложений, а OfferPanel — полную, и предоставить (потенциально переопределяемые) методы генерации нужных срезов данных: Логику управления собственным состоянием (в нашем случае полем currentOffer): Логику преобразования действий пользователя на одном из  субкомпонентов в события или действия над другими компонентами или  родительским контекстом: Если теперь мы посмотрим на кейсы, описанные в начале главы, то мы можем наметить стратегию имплементации каждого из них: Показ компонентов на карте не меняет общую декомпозицию компонентов на список и панель. Для реализации альтернативного IOfferList-а нам нужно переопределить метод buildOfferList так, чтобы он создавал наш кастомный компонент с картой. Комбинирование функциональности списка и панели меняет концепцию, поэтому нам необходимо будет разработать собственный ISearchBoxComposer. Но мы при этом сможем использовать стандартный OfferList, поскольку Composer управляет и подготовкой данных для него, и реакцией на действия пользователей. Обогащение функциональности панели не меняет общую декомпозицию (значит, мы сможем продолжать использовать стандартный SearchBoxComposer и OfferList),  но нам нужно переопределить подготовку данных и опций при открытии  панели, и реализовать дополнительные события и действия, которые SearchBoxComposer транслирует с панели предложения. Ценой этой гибкости является чрезвычайное усложнение  взаимодействия. Все события и потоки данных должны проходить через  цепочку таких Composer-ов, удлиняющих иерархию сущностей.  Любое преобразование (например, генерация опций для вложенного  компонента или реакция на события контекста) должно быть  параметризуемым. Мы можем подобрать какие-то разумные хелперы для того,  чтобы пользоваться такими кастомизациями было проще, но никак не сможем  убрать эту сложность из кода нашего SDK. Таков путь. Пример реализации компонентов с описанными интерфейсами и  имплементацией всех трёх кейсов вы можете найти в репозитории  настоящей книги: исходный код доступен на www.github.com/twirl/The-API-Book/docs/examples  там же предложены несколько задач для самостоятельного изучения; песочница с «живыми» примерами доступна на twirl.github.io/The-API-Book.  Разрабатываю API более 10 лет