1
0
mirror of https://github.com/twirl/The-API-Book.git synced 2025-08-10 21:51:42 +02:00

SDK continued

This commit is contained in:
Sergey Konstantinov
2023-07-12 00:50:19 +03:00
parent fcd9d49f12
commit 125025d4ae
32 changed files with 335 additions and 159 deletions

View File

@@ -45,4 +45,6 @@ Thanks [Knut Sveidqvist and Mermaid Comminuty](https://mermaid.js.org/) for Merm
Thanks [Figma, Inc.](https://www.figma.com/) for Figma.
Thanks [Joey Banks](https://www.figma.com/@joey) for UI Kit.
Thanks [@lual](https://openclipart.org/artist/lual) for the coffee icon.

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -124,32 +124,31 @@
<ul>
<li><a href="API.ru.html#sdk-toc">Глава 41. О содержании раздела</a></li>
<li><a href="API.ru.html#sdk-problems-solutions">Глава 42. SDK: проблемы и решения</a></li>
<li><a href="API.ru.html#chapter-43">Глава 43. Кодогенерация</a></li>
<li><a href="API.ru.html#chapter-44">Глава 44. UI-компоненты</a></li>
<li><a href="API.ru.html#chapter-45">Глава 45. Декомпозиция UI-компонентов. MV*-подходы</a></li>
<li><a href="API.ru.html#chapter-46">Глава 46. MV*-фреймворки</a></li>
<li><a href="API.ru.html#chapter-47">Глава 47. Backend-Driven UI</a></li>
<li><a href="API.ru.html#chapter-48">Глава 48. Разделяемые ресурсы и асинхронные блокировки</a></li>
<li><a href="API.ru.html#chapter-49">Глава 49. Вычисляемые свойства</a></li>
<li><a href="API.ru.html#chapter-50">Глава 50. В заключение</a></li>
<li><a href="API.ru.html#sdk-ui-components">Глава 43. Проблемы встраивания UI-компонентов</a></li>
<li><a href="API.ru.html#chapter-44">Глава 44. Декомпозиция UI-компонентов. MV*-подходы</a></li>
<li><a href="API.ru.html#chapter-45">Глава 45. MV*-фреймворки</a></li>
<li><a href="API.ru.html#chapter-46">Глава 46. Backend-Driven UI</a></li>
<li><a href="API.ru.html#chapter-47">Глава 47. Разделяемые ресурсы и асинхронные блокировки</a></li>
<li><a href="API.ru.html#chapter-48">Глава 48. Вычисляемые свойства</a></li>
<li><a href="API.ru.html#chapter-49">Глава 49. В заключение</a></li>
</ul>
</li>
<li>
<h4><a href="API.ru.html#section-7">Раздел VI. API как продукт</a></h4>
<ul>
<li><a href="API.ru.html#api-product">Глава 51. Продукт API</a></li>
<li><a href="API.ru.html#api-product-business-models">Глава 52. Бизнес-модели API</a></li>
<li><a href="API.ru.html#api-product-vision">Глава 53. Формирование продуктового видения</a></li>
<li><a href="API.ru.html#api-product-devrel">Глава 54. Взаимодействие с разработчиками</a></li>
<li><a href="API.ru.html#api-product-business-comms">Глава 55. Взаимодействие с бизнес-аудиторией</a></li>
<li><a href="API.ru.html#api-product-range">Глава 56. Линейка сервисов API</a></li>
<li><a href="API.ru.html#api-product-kpi">Глава 57. Ключевые показатели эффективности API</a></li>
<li><a href="API.ru.html#api-product-antifraud">Глава 58. Идентификация пользователей и борьба с фродом</a></li>
<li><a href="API.ru.html#api-product-tos-violations">Глава 59. Технические способы борьбы с несанкционированным доступом к API</a></li>
<li><a href="API.ru.html#api-product-customer-support">Глава 60. Поддержка пользователей API</a></li>
<li><a href="API.ru.html#api-product-documentation">Глава 61. Документация</a></li>
<li><a href="API.ru.html#api-product-testing">Глава 62. Тестовая среда</a></li>
<li><a href="API.ru.html#api-product-expectations">Глава 63. Управление ожиданиями</a></li>
<li><a href="API.ru.html#api-product">Глава 50. Продукт API</a></li>
<li><a href="API.ru.html#api-product-business-models">Глава 51. Бизнес-модели API</a></li>
<li><a href="API.ru.html#api-product-vision">Глава 52. Формирование продуктового видения</a></li>
<li><a href="API.ru.html#api-product-devrel">Глава 53. Взаимодействие с разработчиками</a></li>
<li><a href="API.ru.html#api-product-business-comms">Глава 54. Взаимодействие с бизнес-аудиторией</a></li>
<li><a href="API.ru.html#api-product-range">Глава 55. Линейка сервисов API</a></li>
<li><a href="API.ru.html#api-product-kpi">Глава 56. Ключевые показатели эффективности API</a></li>
<li><a href="API.ru.html#api-product-antifraud">Глава 57. Идентификация пользователей и борьба с фродом</a></li>
<li><a href="API.ru.html#api-product-tos-violations">Глава 58. Технические способы борьбы с несанкционированным доступом к API</a></li>
<li><a href="API.ru.html#api-product-customer-support">Глава 59. Поддержка пользователей API</a></li>
<li><a href="API.ru.html#api-product-documentation">Глава 60. Документация</a></li>
<li><a href="API.ru.html#api-product-testing">Глава 61. Тестовая среда</a></li>
<li><a href="API.ru.html#api-product-expectations">Глава 62. Управление ожиданиями</a></li>
</ul>
</li>
</ul>

View File

@@ -120,10 +120,18 @@ code {
font-size: 80%;
}
.img-wrapper {
text-align: center;
}
.img-wrapper img {
max-width: 100%;
}
.app-img-wrapper img {
max-width: 390px;
}
pre {
margin: 1em auto;
padding: 1em;

View File

@@ -210,4 +210,8 @@ Code generation allows for solving trivial problems such as adapting code style,
Such a generated SDK is usually referred to as a “client to an API.” The convenience of usage and the functional capabilities of code generation are so formidable that many API vendors restrict themselves to this technology, only providing their SDKs as generated clients.
However, for the aforementioned reasons, higher-level problems (such as receiving callbacks, dealing with business logic-bound errors, etc.) cannot be solved with code generation without writing some arbitrary code for the specific API. In the case of complex APIs with a non-trivial workcycle it is highly desirable that an SDK also solves high-level problems. Otherwise, an API vendor will end up with a bunch of applications using the API and making all the same “rookie mistakes.” This is, of course, not a reason to fully abolish code generation as it's quite convenient to use a generated client as a basis for developing a high-level SDK.
However, for the aforementioned reasons, higher-level problems (such as receiving callbacks, dealing with business logic-bound errors, etc.) cannot be solved with code generation without writing some arbitrary code for the specific API. In the case of complex APIs with a non-trivial workcycle it is highly desirable that an SDK also solves high-level problems. Otherwise, an API vendor will end up with a bunch of applications using the API and making all the same “rookie mistakes.” This is, of course, not a reason to fully abolish code generation as it's quite convenient to use a generated client as a basis for developing a high-level SDK.
#### Other Tooling
The word “Kit” in “Software Development Kit” implies that the technology comes with auxiliary tools such as emulators / simulators, sandboxes, plugins for IDEs, etc. In this Section, we will not delve into this topic and will discuss it in more detail in the “API Product” section.

View File

@@ -1 +1,81 @@
### The Code Generation Pattern
### [Problems of Introducing UI Components][sdk-ui-components]
Introducing UI components to an SDK brings an additional dimension to an already complex setup comprising a low-level API and a client wrapper on top of it. Now both developers (who write the application) and end users (who use the application) interact with your API. This might not appear as a game changer at first glance; however, we assure you that it is. Involving an end-user has significant consequences from the API / SDK design point of view as it requires much more careful and elaborate program interfaces compared to a “pure” client-server API. Let us explain this statement with a concrete example.
Imagine that we decided to provide a client SDK for our API that features ready-to-use components for application developers. The functionality is simple: the user enters a search phrase and observes the results in the form of a list.
[![APP](/img/mockups/01.png "The main screen of an application with search results")]()
The user can select an item and view the offer details with available actions.
[![APP](/img/mockups/02.png "Offer view panel")]()
To implement this scenario, we provide an object-oriented API in the form of, let's say, a class named `SearchBox` that realizes the aforementioned functionality by utilizing the `search` method in our client-server API.
#### The Problems
At first glance, it might appear that this UI is a superstructure atop the `search` method that simply visualizes the results. Unfortunately, that is not the case. Let us enumerate the problems we have never encountered while developing APIs without visual components.
##### Coupling Heterogeneous Functionality in One Entity
We have placed two buttons (to make an order and to show the coffee shop's location) plus a cancel action onto the offer view panel. These buttons may look identical and they react to the user's actions in the same way, but the way the `SearchBox` component handles pressing each of them is completely different.
Imagine if we allow developers to add their own action buttons onto the panel, for which purpose we introduce a `Button` class. We will soon learn that this functionality will be used to cover two diametrically opposite scenarios:
* Adding extra buttons to the panel, such as “Call the coffee shop,” while *sharing the design with the standard ones*
* Changing the appearance of the standard buttons to match the partner's corporate design guidelines *while preserving the functionality inact*.
Furthermore, a third scenario is possible: developers might want to create a “Call” button that both looks different and performs different actions but *inherits the UX* of the standard button, such as animating button presses, stacking with other buttons, etc.
From the developers' perspective, this means that the `Button` class should allow redefining the appearance of the button, the actions it performs, and the UX elements — in other words, each of these three subsystems might be replaced with an alternative implementation so that the other two subsystems continue working normally.
##### Shared Resources
Imagine that we need to allow developers to programmatically create a `SearchBox` to process a query. This functionality seems reasonable as it would allow displaying a “find lungo nearby” banner in the application, clicking on which would show a `SearchBox` with the pre-entered “lungo” query. Developers will just need to open the corresponding screen in the app and call a method that we are to design. Let's simply name it `search`.
Two of our `search` methods (the “pure” client-server one and the component-bound `SearchBox.search`) accept the same parameters and emit the same results. However, their *behavior* is totally different:
* If requested several times, `SearchBox.search` must discard all server responses except for the one corresponding to the latest request (even if it is not the one received last).
* Additional question: What should `SearchBox.search` return if it is interrupted by another search? If an error, then what was the error of the caller? If a success, then why are the results not displayed?
* This leads to another problem: What should happen if `SearchBox.search` was called when it was processing a request by an end user? Which of the callers is more important — a developer or a user?
While implementing a client-server API, we don't typically face this issue. Every actor calling a search function will receive the response independently. With UI components this approach doesn't work as all the components ultimately share one common resource: the screen of the application and the user's attention.
Any asynchronous operation in a UI component, especially if it is visibly indicated with animation or other continuous action, could disrupt other visual operations, including cases when the disruption happened because of the user's actions.
##### Multiple Inheritance in Entity Hierarchies
Imagine that a developer decided to enhance the design of the offer list with icons of coffee shop chains. If the icon is set, it should be shown in every place related to a specific coffee shop's offer.
[![APP](/img/mockups/03.png "Search results with a coffee shop chain icon")]()
Now let's also imagine that the developer additionally customized all buttons in the SDK by adding action icons.
[![APP](/img/mockups/04.png "The offer view panel with action icons")]()
A question arises: If an offer of the coffee chain is shown in the panel, which icon should be featured on the order creation button: the one inherited from the offer properties (the coffee chain logo) or the one inherited from the action type of the button itself? The order creation control element is incorporated into two entity hierarchies (visual one and data-bound one) and inherits from both equally.
It is very easy to demonstrate how coupling several subject areas in one entity leads to highly sophisticated and unobvious logic. As the same arguments are applicable to the “Show location” button as well, it is kind of obvious that specialized options should take precedence over general ones. In our case, the type of a button should have more priority than some abstract “icon” data property.
But it is not the end of the story. If the developer still wants exactly this, i.e., to show a coffee shop chain icon (if any) on the order creation button, then what should they do? Following the same logic, we should provide an even more specialized possibility to do so. For example, we can adopt the following logic: if there is a `checkoutButtonIconUrl` property in the data, the icon will be taken from this field. Developers could customize the order creation button by overwriting this `checkoutButtonIconUrl` field for every search result:
```
const searchBox = new SearchBox({
// For simplicity, let's allow
// to override the search function
searchFunction: function (params) {
const res = await api.search(params);
res.forEach(function (item) {
item.checkoutButtonIconUrl =
<the URL of the icon>;
});
return res;
}
})
```
*Formally speaking*, this code is correct and does not violate any agreements. However, the readability and maintainability of this code are a catastrophe. The last place the next developer asked to change the *button icon* will look is the *offer search function*.
This functionality would appear more maintainable if no such customization opportunity was provided at all. Developers will be unhappy as they would need to implement their own search control from scratch just to replace an icon, but this implementation would be at least *logical* with icons defined somewhere in the rendering function.
**NB**: There are many other possibilities to allow developers to customize a button nested deeply within a component, such as exposing dependency injection or sub-component class factories, giving direct access to a rendered view, allowing to provide custom button layouts, etc. All of them are inherently subject to the same problem: it is a very complicated task to consistently define the order and the priority of injections / rendering callbacks / custom layouts.
Consistently solving all the problems listed above is unfortunately a very complex task. In the following chapters, we will discuss design patterns that allow for splitting responsibility areas between the component's sub-entities. However, it is important to understand one thing: full separation of concerns, meaning developing a functional SDK+UI that allows developers to independently overwrite the look, business logic, and UX of the components, is extremely expensive. In the best-case scenario, the nomenclature of entities will be tripled. So the universal advice is: *think thrice before exposing the functionality of customizing UI components*. Though the price of design mistakes in UI library APIs is typically not very high (customers rarely request a refund if button press animation is broken), a badly structured, unreadable and buggy SDK could hardly be viewed as a competitive advantage of your API.

View File

@@ -1 +1 @@
### The UI Components
### Decomposing UI Components

View File

@@ -1 +1 @@
### Decomposing UI Components
### The MV* Frameworks

View File

@@ -1 +1 @@
### The MV* Frameworks
### The Backend-Driven UI

View File

@@ -1 +1 @@
### The Backend-Driven UI
### Shared Resources and Asynchronous Locks

View File

@@ -1 +1 @@
### Shared Resources and Asynchronous Locks
### Computed Properties

View File

@@ -1 +1 @@
### Computed Properties
### Conclusion

BIN
src/img/mockups/01.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
src/img/mockups/02.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

BIN
src/img/mockups/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

BIN
src/img/mockups/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

View File

@@ -4,7 +4,7 @@
Тем не менее, у термина SDK есть и более узкое значение, в котором он часто используется: это клиентская библиотека, которая предоставляет высокоуровневый (обычно, нативный) интерфейс для работы с некоторой нижележащей платформой (и в частности с клиент-серверным API). Чаще всего речь идёт о библиотеках для мобильных ОС или веб браузеров, которые работают поверх HTTP API сервиса общего назначения.
Среди подобных клиентских SDK особо выделяются те, которые предоставляют не только программные интерфейсы для работы с API, но также и готовые визуальные компоненты, которые разработчик может использовать. Классический пример такого SDK — это библиотеки карточных сервисов; в силу исключительной сложности самостоятельной реализации движка работы с картами (особенно векторными) вендоры API карт предоставляют и «обёртки» к HTTP API (например, поисковому), и готовые библиотеки для работы с географическими сущностями, которые часто включают в себя и визуальные компоненты общего назначения — кнопки, метки, контекстные меню — которые могут применяться и совершенно самостоятельно вне контекста API (SDK) как такового.
Среди подобных клиентских SDK особо выделяются те, которые предоставляют не только программные интерфейсы для работы с API, но также и готовые визуальные компоненты, которые разработчик может использовать. Классический пример такого SDK — это библиотеки картографических сервисов; в силу исключительной сложности самостоятельной реализации движка работы с картами (особенно векторными) вендоры API карт предоставляют и «обёртки» к HTTP API (например, поисковому), и готовые библиотеки для работы с географическими сущностями, которые часто включают в себя и визуальные компоненты общего назначения — кнопки, метки, контекстные меню — которые могут применяться и совершенно самостоятельно вне контекста API (SDK) как такового.
Настоящий раздел будет посвящён именно двум этим видам программных инструментов:
* клиентским «обёрткам» поверх клиент-серверных API;

View File

@@ -216,3 +216,7 @@
Такой сгенерированный SDK часто называют термином «клиент к API». Удобство использования и функциональные возможности кодогенерации столь привлекательны, что многие вендоры API только ей и ограничиваются, предоставляя свои SDK в виде сгенерированных клиентов.
Как мы, однако, видим из написанного выше, проблемы более высокого порядка — получение серверных событий, обработка ошибок в бизнес-логике и т.п. — никак не может быть покрыта кодогенерацией, во всяком случае — стандартным модулем без его доработки применительно к конкретному API. В случае нетривиальных API со сложным основным циклом работы очень желательно, чтобы SDK решал также и высокоуровневые проблемы, иначе вы просто получите множество разработанных поверх API приложений, раз за разом повторяющие одни и те же «детские ошибки». Тем не менее, это не повод отказываться от кодогенерации полностью — её можно использовать как базис, на котором будет разработан высокоуровневый SDK.
#### Другие инструменты
Слово «Kit» в «Software Development Kit» подразумевает, что в комплекте с технологией поставляются вспомогательные инструменты, такие как эмулятор / симулятор, песочница, плагины для популярных IDE и т.д. В рамках настоящего раздела мы не будем фокусироваться на этом аспекте, и обсудим его подробнее в разделе «API как продукт».

View File

@@ -1 +1,81 @@
### Кодогенерация
### [Проблемы встраивания UI-компонентов][sdk-ui-components]
Введение в состав SDK UI-компонентов обогащает и так не самую простую конструкцию из клиент-серверного API и клиентской библиотеки дополнительным измерением: теперь с вашим API взаимодействуют одновременно и разработчики (которые написали код приложения), и пользователи (которые используют приложение). Хотя это изменение на первый взгляд может показаться не очень значительным, с точки зрения дизайна API добавление конечного пользователя — огромная проблема, которая требует на порядок более глубокой и качественной проработки дизайна программных интерфейсов по сравнению с «чистым» клиент-серверным API. Попробуем объяснить, почему так происходит, на конкретном примере.
Пусть мы решили поставлять в составе нашего кофейного API также и клиентский SDK, который предоставляет готовые компоненты для разработчиков приложений. Достаточно простая функциональность: пользователь вводит поисковый запрос и видит результаты в виде списка.
[![APP](/img/mockups/01.png "Основной экран приложения с результатами поиска")]()
Пользователь может выбрать какой-либо из объектов, и тогда откроется экран просмотра предложения с панелью доступных действий.
[![APP](/img/mockups/02.png "Панель просмотра предложения")]()
Для реализации этого сценария мы предоставим объектно-ориентированный API в виде, ну скажем, класса `SearchBox`, который реализует описанную функциональность поверх клиент-серверного метода `search` нашего клиент-серверного API.
#### Проблемы
С одной стороны нам может показаться, что наш UI — это просто надстройка над клиент-серверным `search`, визуализирующая результаты поиска. Увы, это не так; перечислим проблемы, с которыми мы никогда не сталкивались при разработке API без визуальных компонент.
##### Объединение в одном объекте разнородной функциональности
Посмотрим на панель действий с предложениями. Допустим, мы размещаем на ней две кнопки — «заказать» и «показать на карте» — плюс действие «отменить». Эти кнопки выглядят одинаково и реагируют на действия пользователя одинаково — но при этом осуществляют абсолютно не имеющие ничего общего друг с другом действия.
Допустим, мы предоставили программисту возможность добавить свои кнопки действий на панель, для чего предоставим в составе SDK класс `Button`. Достаточно быстро мы выясним, что этой функциональностью будут пользоваться в двух основных диаметрально противоположных сценариях:
* для размещения на панели дополнительных кнопок, ну скажем, «позвонить в кафе», *выполненных в том же дизайне, что и стандартные*;
* для изменения дизайна стандартных кнопок в соответствии с фирменным стилем заказчика, *сохраняя ту же самую функциональность в неизменном виде*.
Более того, возможен и третий сценарий: разработчики заходят сделать кнопку «позвонить», которая будет и выглядеть иначе, и программно выполнять другие действия, но при этом будет *наследовать UX* кнопки — т.е. нажиматься при клике, располагаться в ряд с другими кнопками и так далее.
С точки зрения разработчика SDK это означает, что класс `Button` должен позволять независимо переопределять и внешний вид кнопки, и реакцию на действия пользователя, и элементы UX — или, иначе говоря, каждая из трёх подсистем может быть заменена альтернативной имплементацией так, чтобы две остальные системы продолжили работать без изменений.
##### Разделяемые ресурсы
Предположим, что мы хотим разрешить разработчику подставить в наш `SearchBox` свой поисковый запрос — например, чтобы дать возможность разместить в приложении баннер «найти лунго рядом со мной», по нажатию на который происходит переход к нашему компоненту с введённым запросом «лунго». Для этого разработчику потребуется программно показать соответствующий экран и вызвать нужный метод `SearchBox`-а — допустим, не мудрствуя лукаво, мы назовём его просто `search`.
Два наших метода `search` («чистый» клиент-серверный и компонентный `SearchBox.search`) принимают одни и те же параметры и выдают один и тот же результат. Но *ведут себя* эти методы совершенно по-разному:
* если вызвать несколько раз `SearchBox.search`, не дожидаясь ответа сервера, то все запросы, кроме последнего во времени, должны быть проигнорированы; даже если ответы пришли вразнобой, только тот из них, который соответствует новейшему запросу, должен быть показан в UI;
* дополнительная задача — что должен вернуть вызов метода `SearchBox.search`, если он был прерван выполнением другого запроса? Если неуспех, то в чём состоит ошибка вызывающего? Если успех, то почему результат не был отражён в UI?
* что порождает другую проблему: а если в момент вызова `SearchBox.search` уже исполнялся какой-то запрос, инициированный пользователем — *что должно произойти*? Какой из вызовов приоритетнее — выполненный разработчиком или выполненный самим пользователем?
В реализации клиент-серверного API такой проблемы у нас нет — каждый актор, вызывающий функцию поиска, получит свой ответ независимо. Но с UI-компонентами этот подход не работает, поскольку все они, в конечном итоге, разделяют один общий ресурс — экран приложения и внимание пользователя на нём.
Любая асинхронная операция в UI-компонентах, особенно если она индицируется визуально (с помощью анимации или другого длящегося действия), может помешать любой другой визуальной операции — в том числе вследствие действий пользователя.
##### Множественная иерархия подчинения сущностей
Предположим, что разработчик хочет обогатить дизайн списка предложений иконками сетей кофеен. Если изображение известно, оно должно быть показано всюду, где происходит работа с предложением конкретной кофейни.
[![APP](/img/mockups/03.png "Результаты поиска с иконкой кофейни")]()
Теперь предположим, что разработчик также переопределил внешний вид всех кнопок в SDK, добавив иконки действий.
[![APP](/img/mockups/04.png "Панель показа предложения с иконками действий")]()
Возникает вопрос: если выбрано предложение сетевой кофейни, какая иконка должна быть на кнопке подтверждения заказа — та, что унаследована из данных предложения (логотип кофейни) или та, что унаследована от «рода занятий» самой кнопки? Элемент управления «создать заказ», таким образом, встроен в две иерархии сущностей (по визуальному отображению и по данным) и в равной степени наследует обоим.
Можно легко продемонстрировать, как пересечение нескольких предметных областей в одном объекте быстро приводит к крайне запутанной и неочевидной логике. Поскольку те же соображения справедливы и для кнопки «Показать на карте», вроде бы очевидно, что по умолчанию более частные свойства должны побеждать более общие, т.е. тип кнопки должен быть приоритетнее какой-то абстрактной «иконки» в данных.
Но на этом история не заканчивается. Если разработчик всё-таки хочет именно этого, т.е. показывать иконку сети кофеен (если она есть) на кнопке создания заказа — как ему это сделать? Из той же логики, нам необходимо предоставить ещё более частную возможность такого переопределения. Например, представим себе следующую функциональность: если в данных предложения есть поле `checkoutButtonIconUrl`, то иконка будет взята из этого поля. Тогда разработчик сможет кастомизировать кнопку заказа, подменив в данных поле `checkoutButtonIconUrl` для каждого результата поиска:
```
const searchBox = new SearchBox({
// Предположим, что мы разрешили
// переопределять поисковую функцию
searchFunction: function (params) {
const res = await api.search(params);
res.forEach(function (item) {
item.checkoutButtonIconUrl =
<URL нужной иконки>;
});
return res;
}
})
```
*Формально* этот подход корректен и никаких рекомендаций не нарушает. Но с точки зрения связности кода, его читабельности — это полная катастрофа, поскольку следующий разработчик, которого попросят заменить иконку *кнопке*, очень вряд ли пойдёт читать код *функции поиска предложений*.
Если бы возможность кастомизации вообще не предоставлялась, эту функциональность было бы гораздо проще поддерживать. Да, разработчики были бы не рады необходимости разработать с нуля собственную панель поиска просто для замены иконки. Но в их коде замена иконки хотя бы будет находиться в *ожидаемом* месте — где-то в функции рендеринга панели.
**NB**: существует много других возможностей позволить разработчику кастомизировать кнопку, запрятанную где-то глубоко в дебрях компонента: разрешить dependency injection или переопределение фабрик суб-компонентов, предоставить прямой доступ к отрендеренному представлению компонента, настроить пользовательские макеты кнопок и так далее. Все они страдают от той же проблемы: крайне сложно консистентно описать порядок и приоритет применения инъекций / обработчиков событий рендеринга / пользовательских шаблонов.
С решением вышеуказанных проблем, увы, всё обстоит очень сложно. В следующих главах мы рассмотрим паттерны проектирования, позволяющие в том числе разделить области ответственности составляющих компонента; но очень важно уяснить одну важную мысль: полное разделение, то есть разработка функционального SDK+UI, дающего разработчику свободу в переопределении и внешнего вида, и бизнес-логики, и UX компонентов — невероятно дорогая в разработке задача, которая в лучшем случае утроит вашу иерархию абстракций. Универсальный совет здесь ровно один: *три раза подумайте прежде чем предоставлять возможность программной настройки UI-компонентов*. Хотя цена ошибки дизайна программных интерфейсов для UI-библиотек, как правило, не очень высока (вряд ли клиент потребует рефанд из-за неработающей анимации нажатия кнопки), плохо структурированный, нечитабельный и глючный SDK вряд ли может рассматриваться как сильное клиентское преимущество вашего API.

View File

@@ -1 +1 @@
### UI-компоненты
### Декомпозиция UI-компонентов. MV*-подходы

View File

@@ -1 +1 @@
### Декомпозиция UI-компонентов. MV*-подходы
### MV*-фреймворки

View File

@@ -1 +1 @@
### MV*-фреймворки
### Backend-Driven UI

View File

@@ -1 +1 @@
### Backend-Driven UI
### Разделяемые ресурсы и асинхронные блокировки

View File

@@ -1 +1 @@
### Разделяемые ресурсы и асинхронные блокировки
### Вычисляемые свойства

View File

@@ -1 +1 @@
### Вычисляемые свойства
### В заключение

View File

@@ -1,75 +0,0 @@
### UI-компоненты
Введение в состав SDK UI-компонентов обогащает и так не самую простую конструкцию из клиент-серверного API и клиентской библиотеки дополнительным измерением: теперь с вашим API взаимодействуют одновременно и разработчики (которые написали код приложения), и пользователи (которые непосредственно тыкают пальцами в экран). Хотя это изменение на первый взгляд может показаться не очень значительным, с точки зрения дизайна API добавление конечного пользователя — огромная проблема, которая требует на порядок более глубокой и качественной проработки дизайна программных интерфейсов по сравнению с «чистым» клиент-серверным API. Попробуем объяснить, почему так происходит, на конкретном примере.
Пусть мы решили поставлять в составе нашего кофейного API также и клиентский SDK, который предоставляет готовые компоненты для разработчиков приложений. Достаточно простая функциональность: пользователь вводит поисковый запрос и видит результаты в виде списка.
(здесь будет картинка)
Пользователь может выбрать какой-либо из объектов, и тогда откроется экран просмотра предложения с панелью доступных действий.
(здесь будет картинка)
Для реализации этого сценария мы предоставим объектно-ориентированный API в виде, ну скажем, класса `SearchBox`, который реализует описанную функциональность поверх клиент-серверного метода `search` нашего API.
#### Проблемы
С одной стороны нам может показаться, что наш UI — это просто надстройка над клиент-серверным `search`, визуализирующая результаты поиска. Увы, это не так; перечислим проблемы, с которыми мы никогда не сталкивались при разработке API без визуальных компонент.
##### Объединение в одном объекте разнородной функциональности
Посмотрим на панель действий с предложениями. Допустим, мы размещаем на ней три кнопки — «заказать», «показать на карте» и «отменить». Эти кнопки выглядят одинаково и реагируют на действия пользователя одинаково — но при этом осуществляют абсолютно не имеющие ничего общего друг с другом действия.
Допустим, мы предоставили программисту возможность добавить свои кнопки действий на панель, для чего предоставим в составе SDK класс `Button`. Достаточно быстро мы выясним, что этой функциональностью будут пользоваться в двух основных диаметрально противоположных сценариях:
* для размещения на панели дополнительных кнопок, ну скажем, «позвонить в кафе», *выполненных в том же дизайне, что и стандартные*;
* для изменения дизайна стандартных кнопок в соответствии с фирменным стилем заказчика, *сохраняя ту же самую функциональность в неизменном виде*.
Более того, возможен и третий сценарий: разработчики заходят сделать кнопку «позвонить», которая будет и выглядеть иначе, и программно выполнять другие действия, но при этом будет *наследовать UX* кнопки — т.е. нажиматься при клике, располагаться в ряд с другими кнопками и так далее.
С точки зрения разработчика SDK это означает, что класс `Button` должен позволять независимо переопределять и внешний вид кнопки, и реакцию на действия пользователя, и элементы UX — или, иначе говоря, каждая из трёх подсистем может быть заменена альтернативной имплементацией так, чтобы для двух других подсистем ничего не изменилось (интерфейс взаимодействия сохранился).
##### Разделяемые ресурсы
Предположим, что мы хотим разрешить разработчику подставить в наш `SearchBox` свой поисковый запрос — например, чтобы дать возможность разместить в приложении баннер «найти лунго рядом со мной», по нажатию на который происходит переход к нашему компоненту с введённым запросом «лунго». Программно, разработчику потребуется показать соответствующий экран и вызвать метод `SeachBox.search`.
Два наших метода `search` («чистый» клиент-серверный и компонентный `SearchBox.search`) принимают одни и те же параметры и выдают один и тот же результат. Но *ведут себя* эти методы совершенно по-разному:
* если вызвать несколько раз `SearchBox.search`, не дожидаясь ответа сервера, то все запросы, кроме последнего во времени, должны быть проигнорированы; даже если ответы пришли вразнобой, только тот из них, который соответствует новейшему запросу, должен быть показан в UI;
* дополнительная задача — что должен вернуть вызов метода `SearchBox.search`, если он был прерван выполнением другого запроса? Если неуспех, то в чём состоит ошибка вызывающего? Если успех, то почему результат не был отражён в UI?
* что порождает другую проблему: а если в момент вызова `SearchBox.search` уже исполнялся какой-то запрос, инициированный пользователем — *что должно произойти*? Какой из вызовов приоритетнее — выполненный разработчиком или выполненный самим пользователем?
В реализации клиент-серверного API такой проблемы у нас нет — каждый актор, вызывающий функцию поиска, получит свой ответ независимо. Но с UI-компонентами этот подход не работает, поскольку все они, в конечном итоге, разделяют один общий ресурс — экран приложения и внимание пользователя на нём.
Любая асинхронная операция в UI-компонентах, особенно если она индицируется визуально (с помощью анимации или другого длящегося действия), может помешать любой другой визуальной операции — в том числе вследствие действий пользователя.
##### Множественная иерархия подчинения сущностей
Предположим, что разработчик хочет обогатить дизайн списка предложений иконками сетей кофеен. Если изображение известно, оно должно быть показано всюду, где происходит работа с предложением конкретной кофейни.
(здесь будет картинка)
Теперь предположим, что разработчик также переопределил внешний вид всех кнопок в SDK, добавив иконки действий:
(здесь будет картинка)
Возникает вопрос: если выбрано предложение сетевой кофейни, какая иконка должна быть на кнопке подтверждения заказа — та, что унаследована из данных предложения (логотип кофейни) или та, что унаследована от «рода занятий» самой кнопки? Элемент управления «создать заказ», таким образом, встроен в две иерархии сущностей (по визуальному отображению и по данным) и в равной степени наследует обоим.
Можно легко продемонстрировать, как пересечение нескольких предметных областей в одном объекте быстро приводит к крайне запутанной и неочевидной логике. Например, представим себе следующую функциональность: если в данных предложения есть поле `checkoutButtonIconUrl`, то иконка будет взята из этого поля — вполне разумное соглашение, если мы хотим позволить выставлять на кнопке иконку сети кофеен, в которой делается заказ. Но тогда разработчик сможет её кастомизировать и показывать не сеть кофеен, а какую-то свою фирменную иконку для действия «заказ», подменив в данных поле `checkoutButtonIconUrl` для каждого результата поиска:
```
const searchBox = new SearchBox({
// Предположим, что мы разрешили
// переопределять поисковую функцию
searchFunction: function (params) {
const res = await api.search(params);
res.forEach((item) {
item.checkoutButtonIconUrl =
<URL нужной иконки>;
});
return res;
}
})
```
*Формально* этот подход корректен и никаких рекомендаций не нарушает. Но с точки зрения связности кода, его читабельности — это полная катастрофа, поскольку следующий разработчик, которого попросят заменить иконку *кнопке*, очень вряд ли пойдёт читать код *функции поиска предложений*.
С решением вышеуказанных проблем, увы, всё обстоит очень сложно. В следующих главах мы рассмотрим паттерны проектирования, позволяющие в том числе разделить области ответственности составляющих компонента; но очень важно уяснить одну важную мысль: полное разделение, то есть разработка функционального SDK+UI, дающего разработчику свободу в переопределении и внешнего вида, и реакции на действия, и UX — невероятно дорогая в разработке задача, которая в лучшем случае утроит вашу иерархию абстракций. Универсальный совет здесь ровно один: *три раза подумайте прежде чем предоставлять возможность программной настройки UI-компонентов*. Хотя цена ошибки дизайна программных интерфейсов для UI-библиотек, как правило, не очень высока (вряд ли клиент потребует рефанд из-за неработающей анимации нажатия кнопки), плохо структурированный, нечитабельный и глючный SDK вряд ли может рассматриваться как сильное клиентское преимущество вашего API.

View File

@@ -234,18 +234,26 @@ module.exports = {
},
aImg: ({ src, href, title, alt, l10n, className = 'img-wrapper' }) => {
const fullTitle = escapeHtml(
`${title}${title.at(-1).match(/[\.\?\!\)]/) ? ' ' : '. '} ${
alt == 'CTL' ? l10n.ctl : `${l10n.imageCredit}: ${alt}`
`${title}${title.at(-1).match(/[\.\?\!\)]/) ? ' ' : '. '}${
alt != 'APP'
? ` ${
alt == 'CTL'
? l10n.ctl
: `${l10n.imageCredit}: ${alt}`
}`
: ''
}`
);
const fullClass =
alt == 'APP' ? `${className} app-img-wrapper` : className;
return `<div class="${escapeHtml(
className
fullClass
)}"><a href="${src}" target="_blank"><img src="${escapeHtml(
src
)}" alt="${fullTitle}" title="${fullTitle}"/></a><h6>${escapeHtml(
title
)}. ${
alt == 'CTL'
alt == 'CTL' || alt == 'APP'
? l10n.ctl
: `${escapeHtml(l10n.imageCredit)}: ${
href
@@ -254,7 +262,8 @@ module.exports = {
)}</a>`
: escapeHtml(alt)
}`
}</h6></div>`;
}
</h6></div>`;
},
graphHtmlTemplate: (graph) => `<!DOCTYPE html>
<html>