You've already forked The-API-Book
mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-07-12 22:50:21 +02:00
clarifications & style fixes
This commit is contained in:
committed by
GitHub
parent
c39b588c23
commit
44b0b8ef3d
@ -1,6 +1,6 @@
|
||||
### Strong Coupling and Related Problems
|
||||
|
||||
To demonstrate the strong coupling problematics let us move to *really interesting* things. Let's continue our “variation analysis”: what if the partners wish to offer not only the standard beverages but their own unique coffee recipes to end-users? There is a catch in this question: the partner API as we described it in the previous chapter, does not expose the very existence of the partner network to the end-user, and thus describes a simple case. Once we start providing methods to alter the core functionality, not just API extensions, we will soon face next-level problems.
|
||||
To demonstrate the strong coupling problematics let us move to *really interesting* things. Let's continue our “variation analysis”: what if the partners wish to offer not only the standard beverages but their own unique coffee recipes to end-users? There is a catch in this question: the partner API as we described it in the previous chapter does not expose the very existence of the partner network to the end-user, and thus describes a simple case. Once we start providing methods to alter the core functionality, not just API extensions, we will soon face next-level problems.
|
||||
|
||||
So, let us add one more endpoint to register the partner's own recipe:
|
||||
|
||||
@ -26,7 +26,7 @@ The first problem is obvious to those who read [chapter 11](#chapter-11-paragrap
|
||||
|
||||
```
|
||||
"product_properties": {
|
||||
// "l10n" is a standard abbreviation
|
||||
// "l10n" is the standard abbreviation
|
||||
// for "localization"
|
||||
"l10n" : [{
|
||||
"language_code": "en",
|
||||
@ -37,7 +37,7 @@ The first problem is obvious to those who read [chapter 11](#chapter-11-paragrap
|
||||
]
|
||||
```
|
||||
|
||||
And here the first big question arises: what should we do with the `default_volume` field? From one side, that's an objective quality measured in standardized units, and it's being passed to the program execution engine. On the other side, in countries like the United States, we had to specify beverage volume not like “300 ml,” but “10 fl oz.” We may propose two solutions:
|
||||
And here the first big question arises: what should we do with the `default_volume` field? From one side, that's an objective property measured in standardized units, and it's being passed to the program execution engine. On the other side, in countries like the United States, we had to specify beverage volume not like “300 ml,” but “10 fl oz.” We may propose two solutions:
|
||||
|
||||
* either the partner provides the corresponding number only, and we will make readable descriptions on our own behalf,
|
||||
* or the partner provides both the number and all of its localized representations.
|
||||
@ -46,17 +46,17 @@ The flaw in the first option is that a partner might be willing to use the servi
|
||||
|
||||
The localization flaws are not the only problem with this API. We should ask ourselves a question — *why* do we really need these `name` and `description`? They are simply non-machine-readable strings with no specific semantics. At first glance, we need them to return them back in the `/v1/search` method response, but that's not a proper answer: why do we really return these strings from `search`?
|
||||
|
||||
The correct answer lies a way beyond this specific interface. We need them *because some representation exists*. There is a UI for choosing beverage type. Probably the `name` and `description` fields are simply two designations of the beverage for a user to read, a short one (to be displayed on the search results page) and a long one (to be displayed in the extended product specification block). It actually means that we are setting the requirements to the API based on some very specific design. But *what if* a partner is making their own UI for their own app? Not only they might not actually need two descriptions, but we are also *deceiving* them. The `name` is not “just a name” actually, it implies some restrictions: it has recommended length which is optimal to some specific UI, and it must look consistently on the search results page. Indeed, “our best quality™ coffee” or “Invigorating Morning Freshness®” designation would look very weird in between “Cappuccino,” “Lungo,” and “Latte.”
|
||||
The correct answer lies a way beyond this specific interface. We need them *because some representation exists*. There is a UI for choosing beverage type. Probably the `name` and `description` fields are simply two designations of the beverage for a user to read, a short one (to be displayed on the search results page) and a long one (to be displayed in the extended product specification block). It actually means that we are setting the requirements to the API based on some very specific design. But *what if* a partner is making their own UI for their own app? Not only they might not actually need two descriptions, but we are also *deceiving* them. The `name` is not “just a name” actually, it implies some restrictions: it has recommended length which is optimal to some specific UI, and it must look consistently on the search results page. Indeed, “our best quality™ coffee” or “Invigorating Morning Freshness®” designation would look very weird in-between “Cappuccino,” “Lungo,” and “Latte.”
|
||||
|
||||
There is also another side to this story. As UIs (both ours and partners) tend to evolve, new visual elements will be eventually introduced. For example, a picture of a beverage, its energy value, allergen information, etc. `product_properties` will become a scrapyard for tons of optional fields, and learning how setting what field results in what effects in the UI will be an interesting quest, full of probes and mistakes.
|
||||
There is also another side to this story. As UIs (both ours and partners') tend to evolve, new visual elements will be eventually introduced. For example, a picture of a beverage, its energy value, allergen information, etc. `product_properties` will become a scrapyard for tons of optional fields, and learning how setting what field results in what effects in the UI will be an interesting quest, full of probes and mistakes.
|
||||
|
||||
Problems we're facing are the problems of *strong coupling*. Each time we offer an interface like described above, we in fact prescript implementing one entity (recipe) based on implementations of other entities (UI layout, localization rules). This approach disrespects the very basic principle of the “top to bottom” API design because **low-level entities must not define high-level ones**.
|
||||
|
||||
#### The rule of contexts
|
||||
|
||||
To make things worse, let us state that the inverse principle is actually correct either: high-level entities must not define low-level ones, since that simply isn't their responsibility. The exit from this logical labyrinth is that high-level entities must *define a context*, which other objects are to interpret. To properly design adding a new recipe interface we shouldn't try to find a better data format; we need to understand what contexts, both explicit and implicit, exist in our subject area.
|
||||
To make things worse, let us state that the inverse principle is actually correct either: high-level entities must not define low-level ones as well, since that simply isn't their responsibility. The exit from this logical labyrinth is that high-level entities must *define a context*, which other objects are to interpret. To properly design the interfaces for adding a new recipe we shouldn't try to find a better data format; we need to understand what contexts, both explicit and implicit, exist in our subject area.
|
||||
|
||||
We have already found a localization context. There is some set of languages and regions we support in our API, and there are requirements — what exactly the partner must provide to make our API work in a new region. More specifically, there must be some formatting function to represent beverage volume somewhere in our API code:
|
||||
We have already found a localization context. There is some set of languages and regions we support in our API, and there are the requirements — what exactly partners must provide to make our API work in a new region. More specifically, there must be some formatting function to represent beverage volume somewhere in our API code, either internally or within an SDK:
|
||||
|
||||
```
|
||||
l10n.volume.format(
|
||||
@ -70,7 +70,7 @@ l10n.volume.format(
|
||||
// ) → '10 fl oz'
|
||||
```
|
||||
|
||||
To make our API work correctly with a new language or region, the partner must either define this function or point which pre-existing implementation to use. Like this:
|
||||
To make our API work correctly with a new language or region, the partner must either define this function or point which pre-existing implementation to use through the partner API. Like this:
|
||||
|
||||
```
|
||||
// Add a general formatting rule
|
||||
@ -83,7 +83,7 @@ PUT /formatters/volume/ru
|
||||
// for Russian language in the “US” region
|
||||
PUT /formatters/volume/ru/US
|
||||
{
|
||||
// in US we need to recalculate
|
||||
// in the US, we need to recalculate
|
||||
// the number, then add a postfix
|
||||
"value_preparation": {
|
||||
"action": "divide",
|
||||
@ -93,10 +93,13 @@ PUT /formatters/volume/ru/US
|
||||
}
|
||||
```
|
||||
|
||||
so the above-mentioned `l10n.volume.format` function implementation might retrieve the formatting rules for the new language-region pair and use them.
|
||||
|
||||
**NB**: we are more than aware that such a simple format isn't enough to cover real-world localization use-cases, and one either relies on existing libraries or designs a sophisticated format for such templating, which takes into account such things as grammatical cases and rules of rounding numbers up or allow defining formatting rules in a form of function code. The example above is simplified for purely educational purposes.
|
||||
|
||||
Let us deal with the `name` and `description` problem then. To lower the coupling level there we need to formalize (probably just to ourselves) a “layout” concept. We are asking for providing `name` and `description` not because we just need them, but for representing them in some specific user interface. This specific UI might have an identifier or a semantic name.
|
||||
|
||||
|
||||
```
|
||||
GET /v1/layouts/{layout_id}
|
||||
{
|
||||
@ -150,7 +153,7 @@ POST /v1/layouts
|
||||
{ "id", "properties" }
|
||||
```
|
||||
|
||||
or they may ultimately design their own UI and don't use this functionality at all, defining neither layouts nor data fields.
|
||||
or they may ultimately design their own UI and don't use this functionality at all, defining neither layouts nor corresponding data fields.
|
||||
|
||||
Then our interface would ultimately look like this:
|
||||
|
||||
@ -161,7 +164,7 @@ POST /v1/recipes
|
||||
{ "id" }
|
||||
```
|
||||
|
||||
This conclusion might look highly counter-intuitive, but lacking any fields in a “Recipe” simply tells us that this entity possesses no specific semantics of its own, and is simply an identifier of a context; a method to point out where to look for the data needed by other entities. In the real world we should implement a builder endpoint capable of creating all the related contexts with a single request:
|
||||
This conclusion might look highly counter-intuitive, but lacking any fields in a “Recipe” simply tells us that this entity possesses no specific semantics of its own, and is simply an identifier of a context; a method to point out where to look for the data needed by other entities. In the real world, we should implement a builder endpoint capable of creating all the related contexts with a single request:
|
||||
|
||||
```
|
||||
POST /v1/recipe-builder
|
||||
|
@ -56,7 +56,7 @@ POST /v1/recipes
|
||||
|
||||
Как бы парадоксально это ни звучало, обратное утверждение тоже верно: высокоуровневые сущности тоже не должны определять низкоуровневые. Это попросту не их ответственность. Выход из этого логического лабиринта таков: высокоуровневые сущности должны *определять контекст*, который другие объекты будут интерпретировать. Чтобы спроектировать добавление нового рецепта нам нужно не формат данных подобрать — нам нужно понять, какие (возможно, неявные, т.е. не представленные в виде API) контексты существуют в нашей предметной области.
|
||||
|
||||
Как уже понятно, существует контекст локализации. Есть какой-то набор языков и регионов, которые мы поддерживаем в нашем API, и есть требования — что конкретно необходимо предоставить партнёру, чтобы API заработал на новом языке в новом регионе. Конкретно в случае объёма кофе где-то в недрах нашего API есть функция форматирования строк для отображения объёма напитка:
|
||||
Как уже понятно, существует контекст локализации. Есть какой-то набор языков и регионов, которые мы поддерживаем в нашем API, и есть требования — что конкретно необходимо предоставить партнёру, чтобы API заработал на новом языке в новом регионе. Конкретно в случае объёма кофе где-то в недрах нашего API (во внутренней реализации или в составе SDK) есть функция форматирования строк для отображения объёма напитка:
|
||||
|
||||
```
|
||||
l10n.volume.format(
|
||||
@ -70,7 +70,7 @@ l10n.volume.format(
|
||||
// ) → '10 fl oz'
|
||||
```
|
||||
|
||||
Чтобы наш API корректно заработал с новым языком или регионом, партнёр должен или задать эту функцию, или указать, какую из существующих локализаций необходимо использовать. Для этого мы абстрагируем-и-расширяем API, в соответствии с описанной в предыдущей главе процедурой, и добавляем новый эндпойнт — настройки форматирования:
|
||||
Чтобы наш API корректно заработал с новым языком или регионом, партнёр должен или задать эту функцию через партнёрский API, или указать, какую из существующих локализаций необходимо использовать. Для этого мы абстрагируем-и-расширяем API, в соответствии с описанной в предыдущей главе процедурой, и добавляем новый эндпойнт — настройки форматирования:
|
||||
|
||||
```
|
||||
// Добавляем общее правило форматирования
|
||||
@ -93,6 +93,8 @@ PUT /formatters/volume/ru/US
|
||||
}
|
||||
```
|
||||
|
||||
Таким образом, реалиазция вышеупомянутой функции `l10n.volume.format` сможет извлечь правила для локали `ru/US` и отформатировать представление объёма согласно им.
|
||||
|
||||
**NB**: мы, разумеется, в курсе, что таким простым форматом локализации единиц измерения в реальной жизни обойтись невозможно, и необходимо либо положиться на существующие библиотеки, либо разработать сложный формат описания (учитывающий, например, падежи слов и необходимую точность округления), либо принимать правила форматирования в императивном виде (т.е. в виде кода функции). Пример выше приведён исключительно в учебных целях.
|
||||
|
||||
Вернёмся теперь к проблеме `name` и `description`. Для того, чтобы снизить связность в этом аспекте, нужно прежде всего формализовать (возможно, для нас самих, необязательно во внешнем API) понятие «макета». Мы требуем `name` и `description` не просто так в вакууме, а чтобы представить их во вполне конкретном UI. Этому конкретному UI можно дать идентификатор или значимое имя.
|
||||
|
Reference in New Issue
Block a user