mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-05-31 22:09:37 +02:00
proofreading
This commit is contained in:
parent
acba20ca3d
commit
682c864c93
BIN
docs/API.en.epub
BIN
docs/API.en.epub
Binary file not shown.
101
docs/API.en.html
101
docs/API.en.html
File diff suppressed because one or more lines are too long
BIN
docs/API.en.pdf
BIN
docs/API.en.pdf
Binary file not shown.
BIN
docs/API.ru.epub
BIN
docs/API.ru.epub
Binary file not shown.
@ -648,7 +648,7 @@ ul.references li p a.back-anchor {
|
||||
<p>Все перечисленные технологии оперируют существенно разными парадигмами — и вызывают естественным образом большое количество холиваров — хотя на момент написания этой книги можно констатировать, что для API общего назначения выбор практически сводится к триаде «REST API (фактически, JSON over HTTP) против gRPC против GraphQL».</p>
|
||||
<p>HTTP API будет посвящён раздел IV; мы также отдельно и подробно рассмотрим концепцию «REST API», поскольку, в отличие от GRPC и GraphQL, она является более гибкой и низкоуровневой — но и приводит к большему непониманию по той же причине.</p>
|
||||
<h4>SDK</h4>
|
||||
<p>Понятие SDK (Software Development Kit, «набор для разработки программного обеспечения»), вообще говоря, вовсе не относится к API: это просто термин для некоторого набора программных инструментов. Однако, как и за «REST», за ним закрепилось некоторое определённое толкование — как клиентского фреймворка для работы с некоторым API. Это может быть как обёртка над клиент-серверным API, так и UI-библиотека в рамках какой-то платформы. Существенным отличием от вышеперечисленных API является то, что «SDK» реализован для какого-то конкретного языка программирования, и его целью является как раз превращение абстрактного набора методов (клиент-серверного API или API операционной системы) в конкретные структуры, разработанные для конкретного языка программирования и конкретной платформы.</p>
|
||||
<p>Понятие SDK (Software Development Kit, «набор для разработки программного обеспечения»), вообще говоря, вовсе не относится к API: это просто термин для некоторого набора программных инструментов. Однако, как и за «REST», за ним закрепилось некоторое определённое толкование — как клиентского фреймворка для работы с некоторым API. Это может быть как обёртка над клиент-серверным API, так и UI-библиотека в рамках какой-то платформы. Существенным отличием от вышеперечисленных API является то, что «SDK» реализован для какого-то конкретного языка программирования и предоставляет возможность работать с низкоуровневым API нижележащей платформы.</p>
|
||||
<p>В отличие от клиент-серверных API, обобщить такие SDK не представляется возможным, т.к. каждый из них написан под конкретное сочетание язык программирования-платформа. Из интероперабельных технологий в мире SDK можно привести в пример кросс-платформенные мобильные (<a href="https://reactnative.dev/">React Native</a>, <a href="https://flutter.dev/">Flutter</a>, <a href="https://dotnet.microsoft.com/en-us/apps/xamarin">Xamarin</a>) и десктопные фреймворки (<a href="https://openjfx.io/">JavaFX</a>, QT) и некоторые узкоспециализированные решения (<a href="https://docs.unity3d.com/Manual/index.html">Unity</a>), однако все они направлены на работу с конкретными технологиями и весьма специфичны.</p>
|
||||
<p>Тем не менее, SDK обладают общностью <em>на уровне задач</em>, которые они решают, и именно этому (решению проблем трансляции и предоставления UI-компонент) будет посвящён раздел V настоящей книги.</p><div class="page-break"></div><h3><a href="#intro-api-quality" class="anchor" id="intro-api-quality">Глава 4. Критерии качества API</a><a href="#chapter-4" class="secondary-anchor" id="chapter-4"> </a></h3>
|
||||
<p>Прежде чем излагать рекомендации, нам следует определиться с тем, что мы считаем «хорошим» API, и какую пользу мы получаем от того, что наш API «хороший».</p>
|
||||
@ -1974,7 +1974,7 @@ POST /v1/offers/search
|
||||
"reason": "wrong_parameter_value",
|
||||
"localized_message":
|
||||
"Что-то пошло не так.⮠
|
||||
Обратитесь к разработчику приложения."
|
||||
Обратитесь к разработчику приложения.",
|
||||
"details": {
|
||||
"checks_failed": [
|
||||
{
|
||||
@ -4272,7 +4272,7 @@ PUT /formatters/volume/ru/US
|
||||
<li>запуск программы создаёт контекст её исполнения, содержащий все существенные параметры;</li>
|
||||
<li>существует поток обмена информацией об изменении состояния: исполнитель может читать контекст, узнавать о всех его модификациях и сообщать обратно о изменениях своего состояния.</li>
|
||||
</ul>
|
||||
<p>Организовать и то, и другое можно разными способами, однако по сути мы всегда имеем два контекста и поток событий между ними. В случае SDK эту идею можно мы бы выразили через генерацию событий:</p>
|
||||
<p>Организовать и то, и другое можно разными способами (см. <a href="#api-patterns-push-vs-poll">соответствующую главу</a> раздела «Паттерны дизайна API»); по сути мы всегда имеем два контекста и поток событий между ними. В случае SDK эту идею можно было бы выразить через генерацию событий:</p>
|
||||
<pre><code>/* Имплементация партнёром интерфейса
|
||||
запуска программы на его кофемашинах */
|
||||
registerProgramRunHandler(
|
||||
@ -4306,7 +4306,7 @@ registerProgramRunHandler(
|
||||
}
|
||||
);
|
||||
</code></pre>
|
||||
<p><strong>NB</strong>: в случае HTTP API соответствующий пример будет выглядеть более громоздко, поскольку потребует создания отдельных эндпойнтов для обмена сообщениями типа <code>GET /program-run/events</code> и <code>GET /partner/{id}/execution/events</code> — это упражнение мы оставляем читателю. Следует также отметить, что в реальных системах потоки событий часто направляют через внешнюю шину типа Apache Kafka или Amazon SNS/SQS.</p>
|
||||
<p><strong>NB</strong>: в случае HTTP API соответствующий пример будет выглядеть более громоздко, поскольку потребует создания отдельных эндпойнтов для обмена сообщениями типа <code>GET /program-run/events</code> и <code>GET /partner/{id}/execution/events</code> — это упражнение мы оставляем читателю.</p>
|
||||
<p>Внимательный читатель может возразить нам, что фактически, если мы посмотрим на номенклатуру возникающих сущностей, мы ничего не изменили в постановке задачи, и даже усложнили её:</p>
|
||||
<ul>
|
||||
<li>вместо вызова метода <code>takeout</code> мы теперь генерируем пару событий <code>takeout_requested</code> / <code>takeout_ready</code>;</li>
|
||||
@ -5304,7 +5304,7 @@ X-OurCoffeeAPI-Error-Kind:⮠
|
||||
"reason": "wrong_parameter_value",
|
||||
"localized_message":
|
||||
"Что-то пошло не так.⮠
|
||||
Обратитесь к разработчику приложения."
|
||||
Обратитесь к разработчику приложения.",
|
||||
"details": {
|
||||
"checks_failed": [
|
||||
{
|
||||
|
BIN
docs/API.ru.pdf
BIN
docs/API.ru.pdf
Binary file not shown.
@ -59,7 +59,7 @@
|
||||
<ul>
|
||||
<li><a href="API.en.html#intro-structure">Chapter 1. On the Structure of This Book</a></li>
|
||||
<li><a href="API.en.html#intro-api-definition">Chapter 2. The API Definition</a></li>
|
||||
<li><a href="API.en.html#intro-api-solutions-overview">Chapter 3. Overview of Existing API Development Solutions</a></li>
|
||||
<li><a href="API.en.html#intro-api-solutions-overview">Chapter 3. An Overview of Existing API Development Solutions</a></li>
|
||||
<li><a href="API.en.html#intro-api-quality">Chapter 4. API Quality Criteria</a></li>
|
||||
<li><a href="API.en.html#intro-api-first-approach">Chapter 5. The API-First Approach</a></li>
|
||||
<li><a href="API.en.html#intro-back-compat">Chapter 6. On Backward Compatibility</a></li>
|
||||
@ -114,7 +114,7 @@
|
||||
<li><a href="API.en.html#http-api-rest-myth">Chapter 35. The REST Myth</a></li>
|
||||
<li><a href="API.en.html#http-api-requests-semantics">Chapter 36. Components of an HTTP Request and Their Semantics</a></li>
|
||||
<li><a href="API.en.html#http-api-rest-organizing">Chapter 37. Organizing HTTP APIs Based on the REST Principles</a></li>
|
||||
<li><a href="API.en.html#http-api-urls-crud">Chapter 38. Designing a Nomenclature of URLs. CRUD Operations</a></li>
|
||||
<li><a href="API.en.html#http-api-urls-crud">Chapter 38. Designing a Nomenclature of URLs. The CRUD Operations</a></li>
|
||||
<li><a href="API.en.html#http-api-errors">Chapter 39. Working with HTTP API Errors</a></li>
|
||||
<li><a href="API.en.html#http-api-final-recommendations">Chapter 40. Final Provisions and General Recommendations</a></li>
|
||||
</ul>
|
||||
@ -137,18 +137,18 @@
|
||||
<li>
|
||||
<h4><a href="API.en.html#section-7">Section VI. The API Product</a></h4>
|
||||
<ul>
|
||||
<li><a href="API.en.html#api-product">Chapter 51. API as a Product</a></li>
|
||||
<li><a href="API.en.html#api-product-business-models">Chapter 52. The API Business Models</a></li>
|
||||
<li><a href="API.en.html#api-product">Chapter 51. The API as a Product</a></li>
|
||||
<li><a href="API.en.html#api-product-business-models">Chapter 52. API Business Models</a></li>
|
||||
<li><a href="API.en.html#api-product-vision">Chapter 53. Developing a Product Vision</a></li>
|
||||
<li><a href="API.en.html#api-product-devrel">Chapter 54. Communicating with Developers</a></li>
|
||||
<li><a href="API.en.html#api-product-business-comms">Chapter 55. Communicating with Business Owners</a></li>
|
||||
<li><a href="API.en.html#api-product-range">Chapter 56. The API Services Range</a></li>
|
||||
<li><a href="API.en.html#api-product-kpi">Chapter 57. The API Key Performance Indicators</a></li>
|
||||
<li><a href="API.en.html#api-product-range">Chapter 56. An API Services Range</a></li>
|
||||
<li><a href="API.en.html#api-product-kpi">Chapter 57. API Key Performance Indicators</a></li>
|
||||
<li><a href="API.en.html#api-product-antifraud">Chapter 58. Identifying Users and Preventing Fraud</a></li>
|
||||
<li><a href="API.en.html#api-product-tos-violations">Chapter 59. The Technical Means of Preventing ToS Violations</a></li>
|
||||
<li><a href="API.en.html#api-product-customer-support">Chapter 60. Supporting customers</a></li>
|
||||
<li><a href="API.en.html#api-product-documentation">Chapter 61. The Documentation</a></li>
|
||||
<li><a href="API.en.html#api-product-testing">Chapter 62. The Testing Environment</a></li>
|
||||
<li><a href="API.en.html#api-product-testing">Chapter 62. Testing Environments</a></li>
|
||||
<li><a href="API.en.html#api-product-expectations">Chapter 63. Managing Expectations</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
@ -569,7 +569,7 @@ POST /v1/coffee-machines/search
|
||||
"reason": "wrong_parameter_value",
|
||||
"localized_message":
|
||||
"Something is wrong.⮠
|
||||
Contact the developer of the app."
|
||||
Contact the developer of the app.",
|
||||
"details": {
|
||||
"checks_failed": [
|
||||
{
|
||||
|
@ -1,6 +1,6 @@
|
||||
### [Weak Coupling][back-compat-weak-coupling]
|
||||
|
||||
In the previous chapter, we've demonstrated how breaking strong coupling of components leads to decomposing entities and collapsing their public interfaces down to a reasonable minimum. But let us return to the question we have previously mentioned in the “[Extending through Abstracting](#back-compat-abstracting-extending)” chapter: how should we parametrize the order preparation process implemented via a third-party API? In other words, what *is* the `order_execution_endpoint` required in the API type registration handler?
|
||||
In the previous chapter, we demonstrated how breaking strong coupling of components leads to decomposing entities and collapsing their public interfaces down to a reasonable minimum. But let us return to the question we previously mentioned in the “[Extending through Abstracting](#back-compat-abstracting-extending)” chapter: how should we parametrize the order preparation process implemented via a third-party API? In other words, what *is* the `order_execution_endpoint` required in the API type registration handler?
|
||||
|
||||
```
|
||||
PUT /v1/api-types/{api_type}
|
||||
@ -12,7 +12,7 @@ PUT /v1/api-types/{api_type}
|
||||
}
|
||||
```
|
||||
|
||||
Out of general considerations, we may assume that every such API would be capable of executing three functions: run a program with specified parameters, return the current execution status, and finish (cancel) the order. An obvious way to provide the common interface is to require these three functions to be executed via a remote call, let's say, like this:
|
||||
From general considerations, we may assume that every such API would be capable of executing three functions: running a program with specified parameters, returning the current execution status, and finishing (canceling) the order. An obvious way to provide the common interface is to require these three functions to be executed via a remote call, let's say, like this:
|
||||
|
||||
```
|
||||
PUT /v1/api-types/{api_type}
|
||||
@ -32,40 +32,40 @@ PUT /v1/api-types/{api_type}
|
||||
}
|
||||
```
|
||||
|
||||
**NB**: by doing so, we transfer the complexity of developing the API onto the plane of developing appropriate data formats, i.e., developing formats for order parameters to the `program_run_endpoint`, and what format the `program_get_state_endpoint` shall return, etc., but in this chapter, we're focusing on different questions.
|
||||
**NB**: by doing so, we transfer the complexity of developing the API onto the plane of developing appropriate data formats, i.e., developing formats for order parameters to the `program_run_endpoint`, determining what format the `program_get_state_endpoint` shall return, etc. However, in this chapter, we're focusing on different questions.
|
||||
|
||||
Though this API looks absolutely universal, it's quite easy to demonstrate how once simple and clear API ends up being confusing and convoluted. This design presents two main problems:
|
||||
Though this API looks absolutely universal, it's quite easy to demonstrate how a once simple and clear API ends up being confusing and convoluted. This design presents two main problems:
|
||||
|
||||
1. It describes nicely the integrations we've already implemented (it costs almost nothing to support the API types we already know) but brings no flexibility to the approach. In fact, we simply described what we'd already learned, not even trying to look at the larger picture.
|
||||
1. It nicely describes the integrations we've already implemented (it costs almost nothing to support the API types we already know), but it brings no flexibility to the approach. In fact, we simply described what we had already learned, without even trying to look at the larger picture.
|
||||
2. This design is ultimately based on a single principle: every order preparation might be codified with these three imperative commands.
|
||||
|
||||
We may easily disprove the \#2 statement, and that will uncover the implications of the \#1. For the beginning, let us imagine that on a course of further service growth, we decided to allow end-users to change the order after the execution started. For example, request a contactless takeout. That would lead us to the creation of a new endpoint, let's say, `program_modify_endpoint`, and new difficulties in data format development (as new fields for contactless delivery requested and satisfied flags need to be passed both directions). What *is* important is that both the endpoint and the new data fields would be optional because of the backward compatibility requirement.
|
||||
We can easily disprove the second statement, which will uncover the implications of the first. Let's imagine, for example, that as the service grows further, we decide to allow end-users to change the order after the execution has started. For example, they may request a contactless takeout. This would lead us to the creation of a new endpoint, let's say, `program_modify_endpoint`, and new difficulties in data format development (as new fields for contactless delivery requested and satisfied flags need to be passed in both directions). What *is* important is that both the endpoint and the new data fields would be optional due to the backward compatibility requirement.
|
||||
|
||||
Now let's try to imagine a real-world example that doesn't fit into our “three imperatives to rule them all” picture. That's quite easy as well: what if we're plugging not a coffee house, but a vending machine via our API? From one side, it means that the `modify` endpoint and all related stuff are simply meaningless: the contactless takeout requirement means nothing to a vending machine. On the other side, the machine, unlike the people-operated café, requires *takeout approval*: the end-user places an order while being somewhere in some other place then walks to the machine and pushes the “get the order” button in the app. We might, of course, require the user to stand up in front of the machine when placing an order, but that would contradict the entire product concept of users selecting and ordering beverages and then walking to the takeout point.
|
||||
Now let's try to imagine a real-world example that doesn't fit into our “three imperatives to rule them all” picture. That's quite easy as well: what if we're plugging in a vending machine via our API instead of a coffee house? On one hand, it means that the `modify` endpoint and all related stuff are simply meaningless: the contactless takeout requirement means nothing to a vending machine. On the other hand, the machine, unlike the people-operated café, requires *takeout approval*: the end-user places an order while being somewhere else and then walks to the machine and pushes the “get the order” button in the app. We might, of course, require the user to stand up in front of the machine when placing an order, but that would contradict the entire product concept of users selecting and ordering beverages and then walking to the takeout point.
|
||||
|
||||
Programmable takeout approval requires one more endpoint, let's say, `program_takeout_endpoint`. And so we've lost our way in a forest of five endpoints:
|
||||
* To have vending machines integrated a partner must implement the `program_takeout_endpoint`, but doesn't need the `program_modify_endpoint`.
|
||||
* To have regular coffee houses integrated a partner must implement the `program_modify_endpoint`, but doesn't need the `program_takeout_endpoint`.
|
||||
* To have vending machines integrated a partner must implement the `program_takeout_endpoint` but doesn't need the `program_modify_endpoint`.
|
||||
* To have regular coffee houses integrated a partner must implement the `program_modify_endpoint` but doesn't need the `program_takeout_endpoint`.
|
||||
|
||||
Furthermore, we have to describe both endpoints in the docs. It's quite natural that the `takeout` endpoint is very specific; unlike requesting contactless delivery, which we hid under the pretty general `modify` endpoint, operations like takeout approval will require introducing a new unique method every time. After several iterations, we would have a scrapyard, full of similarly looking methods, mostly optional — but developers would need to study the docs nonetheless to understand, which methods are needed in your specific situation, and which are not.
|
||||
Furthermore, we have to describe both endpoints in the documentation. It's quite natural that the `takeout` endpoint is very specific; unlike requesting contactless delivery, which we hid under the pretty general `modify` endpoint, operations like takeout approval will require introducing a new unique method every time. After several iterations, we would have a scrapyard full of similarly looking methods, mostly optional. However, developers would still need to study the documentation to understand which methods are needed in their specific situation and which are not.
|
||||
|
||||
**NB**: in this example, we assumed that passing `program_takeout_endpoint` parameter is the flag to the application to display the “get the order” button; it would be better to add something like a `supported_flow` field to the `PUT /api-types/` endpoint to provide an explicit flag instead of this implicit convention; however, this wouldn't change the problematics of stockpiling optional methods in the interface, so we skipped it to keep examples laconic.
|
||||
**NB**: in this example, we assumed that having the optional `program_takeout_endpoint` value filled serves as a flag to the application to display the “get the order” button. It would be better to add something like a `supported_flow` field to the `PUT /api-types/` endpoint to provide an explicit flag instead of relying on this implicit convention. However, this wouldn't change the problematic nature of stockpiling optional methods in the interface, so we skipped it to keep the examples concise.
|
||||
|
||||
We actually don't know, whether in the real world of coffee machine APIs this problem will occur or not. But we can say with all confidence regarding “bare metal” integrations that the processes we described *always* happen. The underlying technology shifts; an API that seemed clear and straightforward, becomes a trash bin full of legacy methods, half of which borrows no practical sense under any specific set of conditions. If we add technical progress to the situation, i.e., imagine that after a while all coffee houses have become automated, we will finally end up with the situation with half of the methods *aren't actually needed at all*, like requesting a contactless takeout one.
|
||||
We actually don't know whether in the real world of coffee machine APIs this problem will occur or not. But we can say with confidence that regarding “bare metal” integrations, the processes we described *always* happen. The underlying technology shifts; an API that seemed clear and straightforward becomes a trash bin full of legacy methods, half of which bear no practical sense under any specific set of conditions. If we add technical progress to the situation, i.e., imagine that after a while all coffee houses have become automated, we will finally end up in a situation where most methods *aren't actually needed at all*, such as requesting a contactless takeout.
|
||||
|
||||
It is also worth mentioning that we unwittingly violated the abstraction levels isolation principle. At the vending machine API level, there is no such thing as a “contactless takeout,” that's actually a product concept.
|
||||
It is also worth mentioning that we unwittingly violated the principle of isolating abstraction levels. At the vending machine API level, there is no such thing as “contactless takeout” as it is actually a product concept.
|
||||
|
||||
So, how would we tackle this issue? Using one of two possible approaches: either thoroughly study the entire subject area and its upcoming improvements for at least several years ahead, or abandon strong coupling in favor of a weak one. How would the *ideal* solution look to both parties? Something like this:
|
||||
* The higher-level program API level doesn't actually know how the execution of its commands works; it formulates the tasks at its own level of understanding: brew this recipe, send user's requests to a partner, allow the user to collect their order.
|
||||
* The underlying program execution API level doesn't care what other same-level implementations exist; it just interprets those parts of the task that make sense to it.
|
||||
So, how would we tackle this issue? We can use one of two possible approaches: either thoroughly study the entire subject area and its upcoming improvements for at least several years ahead or abandon strong coupling in favor of a weak one. How would the *ideal* solution look for both parties? Something like this:
|
||||
* The higher-level program API level doesn't actually know how the execution of its commands works. It formulates the tasks at its own level of understanding: brew this recipe, send the user's requests to a partner, allow the user to collect their order.
|
||||
* The underlying program execution API level doesn't care about what other same-level implementations exist. It just interprets those parts of the task that make sense to it.
|
||||
|
||||
If we take a look at the principles described in the previous chapter, we would find that this principle was already formulated: we need to describe *informational contexts* at every abstraction level and design a mechanism to translate them between levels. Furthermore, in a more general sense, we formulated it as early as in “The Data Flow” paragraph of the “[Separating Abstraction Levels](#api-design-separating-abstractions)” chapter.
|
||||
If we take a look at the principles described in the previous chapter, we would find that this principle was already formulated: we need to describe *informational contexts* at every abstraction level and design a mechanism to translate them between levels. Furthermore, in a more general sense, we formulated it as early as in the “Data Flow” paragraph of the “[Separating Abstraction Levels](#api-design-separating-abstractions)” chapter.
|
||||
|
||||
In our case we need to implement the following mechanisms:
|
||||
* Running a program creates a corresponding context comprising all the essential parameters.
|
||||
* There is the information stream regarding the state modifications: the execution level may read the context, learn about all the changes and report back the changes of its own.
|
||||
* There is an information stream regarding the state modifications: the execution level may read the context, learn about all the changes and report back its own changes.
|
||||
|
||||
There are different techniques to organize this data flow, but, basically, we always have two contexts and a two-way data pipe in between. If we were developing an SDK, we would express the idea with emitting and listening events, like this:
|
||||
There are different techniques to organize this data flow (see the [corresponding chapter](#api-patterns-push-vs-poll) of the “API Patterns” Section of this book). Basically, we always have two contexts and a two-way data pipe in between. If we were developing an SDK, we would express the idea with emitting and listening events, like this:
|
||||
|
||||
```
|
||||
/* Partner's implementation of the program
|
||||
@ -74,17 +74,17 @@ registerProgramRunHandler(
|
||||
apiType,
|
||||
(program) => {
|
||||
// Initiating an execution
|
||||
// on partner's side
|
||||
// on the partner's side
|
||||
let execution = initExecution(…);
|
||||
// Listen to parent context changes
|
||||
program.context.on(
|
||||
'takeout_requested',
|
||||
() => {
|
||||
// If a takeout is requested, initiate
|
||||
// corresponding procedures
|
||||
// required procedures
|
||||
await execution.prepareTakeout();
|
||||
// When the cup is ready for takeout,
|
||||
// emit corresponding event for
|
||||
// emit the corresponding event for
|
||||
// a higher-level entity to catch it
|
||||
execution.context.emit('takeout_ready');
|
||||
}
|
||||
@ -102,18 +102,18 @@ registerProgramRunHandler(
|
||||
);
|
||||
```
|
||||
|
||||
**NB**: In the case of HTTP API, a corresponding example would look rather bulky as it would require implementing several additional endpoints for the message exchange like `GET /program-run/events` and `GET /partner/{id}/execution/events`. We would leave this exercise to the reader. Also, it's worth mentioning that in real-world systems such event queues are usually organized using external event messaging systems like Apache Kafka or Amazon SNS/SQS.
|
||||
**NB**: In the case of an HTTP API, a corresponding example would look rather bulky as it would require implementing several additional endpoints for the message exchange like `GET /program-run/events` and `GET /partner/{id}/execution/events`. We would leave this exercise to the reader.
|
||||
|
||||
At this point, a mindful reader might begin protesting because if we take a look at the nomenclature of the new entities, we will find that nothing changed in the problem statement. It actually became even more complicated:
|
||||
* Instead of calling the `takeout` method, we're now generating a pair of `takeout_requested` / `takeout_ready` events
|
||||
* Instead of a long list of methods that shall be implemented to integrate the partner's API, we now have a long list of context entities and events they generate
|
||||
* And with regards to technological progress, we've changed nothing: now we have deprecated fields and events instead of deprecated methods.
|
||||
|
||||
And this remark is totally correct. Changing API formats doesn't solve any problems related to the evolution of functionality and underlying technology. Changing API formats serves another purpose: to make the code written by developers stay clean and maintainable. Why would strong-coupled integration (i.e., making entities interact via calling methods) render the code unreadable? Because both sides *are obliged* to implement the functionality which is meaningless in their corresponding subject areas. Code that integrates vending machines into the system *must* respond “ok” to the contactless delivery request — so after a while, these implementations would comprise a handful of methods that just always return `true` (or `false`).
|
||||
And this remark is totally correct. Changing API formats doesn't solve any problems related to the evolution of functionality and underlying technology. Changing API formats serves another purpose: to make the code written by developers stay clean and maintainable. Why would strong-coupled integration (i.e., making entities interact via calling methods) render the code unreadable? Because both sides *are obliged* to implement functionality that is meaningless in their corresponding subject areas. Code that integrates vending machines into the system *must* respond “ok” to the contactless delivery request — so after a while, these implementations would comprise a handful of methods that just always return `true` (or `false`).
|
||||
|
||||
The difference between strong coupling and weak coupling is that the field-event mechanism *isn't obligatory for both actors*. Let us remember what we sought to achieve:
|
||||
* A higher-level context doesn't know how low-level API works — and it really doesn't; it describes the changes that occur within the context itself, and reacts only to those events that mean something to it.
|
||||
* A low-level context doesn't know anything about alternative implementations — and it really doesn't; it handles only those events which mean something at its level and emits only those events that could happen under its specific conditions.
|
||||
* A higher-level context doesn't know how the low-level API works — and it really doesn't. It describes the changes that occur within the context itself and reacts only to those events that mean something to it.
|
||||
* A low-level context doesn't know anything about alternative implementations — and it really doesn't. It handles only those events which mean something at its level and emits only those events that could happen under its specific conditions.
|
||||
|
||||
It's ultimately possible that both sides would know nothing about each other and wouldn't interact at all, and this might happen with the evolution of underlying technologies.
|
||||
|
||||
@ -126,13 +126,13 @@ One more important feature of weak coupling is that it allows an entity to have
|
||||
It becomes obvious from what was said above that two-way weak coupling means a significant increase in code complexity on both levels, which is often redundant. In many cases, two-way event linking might be replaced with one-way linking without significant loss of design quality. That means allowing a low-level entity to call higher-level methods directly instead of generating events. Let's alter our example:
|
||||
|
||||
```
|
||||
/* Partner's implementation of program
|
||||
/* Partner's implementation of the program
|
||||
run procedure for a custom API type */
|
||||
registerProgramRunHandler(
|
||||
apiType,
|
||||
(program) => {
|
||||
// Initiating an execution
|
||||
// on partner's side
|
||||
// on the partner's side
|
||||
let execution = initExecution(…);
|
||||
// Listen to parent context changes
|
||||
program.context.on(
|
||||
@ -142,7 +142,7 @@ registerProgramRunHandler(
|
||||
// corresponding procedures
|
||||
await execution.prepareTakeout();
|
||||
/* When the order is ready
|
||||
for takeout, signalize about that
|
||||
for takeout, signalize that
|
||||
by calling the parent context
|
||||
method, not with event emitting */
|
||||
// execution.context
|
||||
@ -153,19 +153,20 @@ registerProgramRunHandler(
|
||||
// program.setTakeoutReady();
|
||||
}
|
||||
);
|
||||
/* Since we're modifying parent context
|
||||
instead of emitting events, we don't
|
||||
actually need to return anything */
|
||||
/* Since we're modifying the parent
|
||||
context instead of emitting events,
|
||||
we don't actually need
|
||||
to return anything */
|
||||
// return execution.context;
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
Again, this solution might look counter-intuitive, since we efficiently returned to strong coupling via strictly defined methods. But there is an important difference: we're bothering ourselves with weak coupling because we expect alternative implementations of the *lower* abstraction level to pop up. Situations with different realizations of *higher* abstraction levels emerging are, of course, possible, but quite rare. The tree of alternative implementations usually grows from root to leaves.
|
||||
Again, this solution might look counter-intuitive, since we efficiently returned to strong coupling via strictly defined methods. But there is an important difference: we're bothering ourselves with weak coupling because we expect alternative implementations of the *lower* abstraction level to pop up. Situations with different realizations of *higher* abstraction levels emerging are, of course, possible but quite rare. The tree of alternative implementations usually grows from root to leaves.
|
||||
|
||||
Another reason to justify this solution is that major changes occurring at different abstraction levels have different weights:
|
||||
* If the technical level is under change, that must not affect product qualities and the code written by partners.
|
||||
* If the product is changing, e.g., we start selling flight tickets instead of preparing coffee, there is literally no sense to preserve backward compatibility at technical abstraction levels. Ironically, we may actually make our API sell tickets instead of brewing coffee without breaking backward compatibility, but the partners' code will still become obsolete.
|
||||
* If the product is changing, e.g., we start selling flight tickets instead of preparing coffee, there is literally no sense in preserving backward compatibility at technical abstraction levels. Ironically, we may actually make our API sell tickets instead of brewing coffee without breaking backward compatibility, but the partners' code will still become obsolete.
|
||||
|
||||
In conclusion, as higher-level APIs are evolving more slowly and much more consistently than low-level APIs, reverse strong coupling might often be acceptable or even desirable, at least from the price-quality ratio point of view.
|
||||
|
||||
@ -186,7 +187,7 @@ program.context.on(
|
||||
);
|
||||
```
|
||||
|
||||
Let us note that this approach *in general* doesn't contradict the weak coupling principle, but violates another one — of abstraction levels isolation, and therefore isn't very well suited for writing branchy APIs with high hierarchy trees. In such systems, it's still possible to use a global or quasi-global state manager, but you need to implement event or method call propagation through the hierarchy, i.e., ensure that a low-level entity always interacts with its closest higher-level neighbors only, delegating the responsibility of calling high-level or global methods to them.
|
||||
Let us note that this approach *in general* doesn't contradict the weak coupling principle but violates another one — abstraction levels isolation — and therefore isn't very well suited for writing branchy APIs with high hierarchy trees. In such systems, it's still possible to use a global or quasi-global state manager, but you need to implement event or method call propagation through the hierarchy, i.e., ensure that a low-level entity always interacts with its closest higher-level neighbors only, delegating the responsibility of calling high-level or global methods to them.
|
||||
|
||||
```
|
||||
program.context.on(
|
||||
@ -219,8 +220,8 @@ ProgramContext.dispatch = (action) => {
|
||||
|
||||
#### Delegate!
|
||||
|
||||
From what was said, one more important conclusion follows: doing a real job, i.e., implementing some concrete actions (making coffee, in our case) should be delegated to the lower levels of the abstraction hierarchy. If the upper levels try to prescribe some specific implementation algorithms, then (as we have demonstrated on the `order_execution_endpoint` example) we will soon face a situation of inconsistent methods and interaction protocols nomenclature, most of which have no specific meaning when we talk about some specific hardware context.
|
||||
Based on what was said, one more important conclusion follows: doing a real job, i.e., implementing concrete actions (making coffee, in our case) should be delegated to the lower levels of the abstraction hierarchy. If the upper levels try to prescribe implementation algorithms, then (as demonstrated in the example of `order_execution_endpoint`) we will soon face a situation of inconsistent methods, most of which have no specific meaning when applied to a particular hardware context.
|
||||
|
||||
Contrariwise, applying the paradigm of concretizing the contexts at each new abstraction level, we will eventually fall into the bunny hole deep enough to have nothing to concretize: the context itself unambiguously matches the functionality we can programmatically control. And at that level, we must stop detailing contexts further, and just realize the algorithms needed. It's worth mentioning that the abstraction deepness for different underlying platforms might vary.
|
||||
On the other hand, by following the paradigm of concretizing the contexts at each new abstraction level, we will eventually fall into the bunny hole deep enough to have nothing more to concretize: the context itself unambiguously matches the functionality we can programmatically control. At that level, we should stop detailing contexts further and focus on implementing the necessary algorithms. It's worth mentioning that the depth of abstraction may vary for different underlying platforms.
|
||||
|
||||
**NB**. In the “[Separating Abstraction Levels](#api-design-separating-abstractions)” chapter we have illustrated exactly this: when we speak about the first coffee machine API type, there is no need to extend the tree of abstractions further than running programs, but with the second API type, we need one more intermediary abstraction level, namely the runtimes API.
|
||||
**NB**. In the “[Separating Abstraction Levels](#api-design-separating-abstractions)” chapter we illustrated exactly this: when we talk about the first coffee machine API type, there is no need to extend the tree of abstractions beyond running programs. However, with the second API type, we need one more intermediary abstraction level, namely the runtimes API.
|
@ -84,7 +84,7 @@ X-OurCoffeeAPI-Error-Kind:⮠
|
||||
"reason": "wrong_parameter_value",
|
||||
"localized_message":
|
||||
"Something is wrong.⮠
|
||||
Contact the developer of the app."
|
||||
Contact the developer of the app.",
|
||||
"details": {
|
||||
"checks_failed": [
|
||||
{
|
||||
|
@ -568,7 +568,7 @@ POST /v1/coffee-machines/search
|
||||
"reason": "wrong_parameter_value",
|
||||
"localized_message":
|
||||
"Что-то пошло не так.⮠
|
||||
Обратитесь к разработчику приложения."
|
||||
Обратитесь к разработчику приложения.",
|
||||
"details": {
|
||||
"checks_failed": [
|
||||
{
|
||||
|
@ -65,7 +65,7 @@ PUT /v1/api-types/{api_type}
|
||||
* запуск программы создаёт контекст её исполнения, содержащий все существенные параметры;
|
||||
* существует поток обмена информацией об изменении состояния: исполнитель может читать контекст, узнавать о всех его модификациях и сообщать обратно о изменениях своего состояния.
|
||||
|
||||
Организовать и то, и другое можно разными способами, однако по сути мы всегда имеем два контекста и поток событий между ними. В случае SDK эту идею можно мы бы выразили через генерацию событий:
|
||||
Организовать и то, и другое можно разными способами (см. [соответствующую главу](#api-patterns-push-vs-poll) раздела «Паттерны дизайна API»); по сути мы всегда имеем два контекста и поток событий между ними. В случае SDK эту идею можно было бы выразить через генерацию событий:
|
||||
|
||||
```
|
||||
/* Имплементация партнёром интерфейса
|
||||
@ -102,7 +102,7 @@ registerProgramRunHandler(
|
||||
);
|
||||
```
|
||||
|
||||
**NB**: в случае HTTP API соответствующий пример будет выглядеть более громоздко, поскольку потребует создания отдельных эндпойнтов для обмена сообщениями типа `GET /program-run/events` и `GET /partner/{id}/execution/events` — это упражнение мы оставляем читателю. Следует также отметить, что в реальных системах потоки событий часто направляют через внешнюю шину типа Apache Kafka или Amazon SNS/SQS.
|
||||
**NB**: в случае HTTP API соответствующий пример будет выглядеть более громоздко, поскольку потребует создания отдельных эндпойнтов для обмена сообщениями типа `GET /program-run/events` и `GET /partner/{id}/execution/events` — это упражнение мы оставляем читателю.
|
||||
|
||||
Внимательный читатель может возразить нам, что фактически, если мы посмотрим на номенклатуру возникающих сущностей, мы ничего не изменили в постановке задачи, и даже усложнили её:
|
||||
* вместо вызова метода `takeout` мы теперь генерируем пару событий `takeout_requested` / `takeout_ready`;
|
||||
|
@ -82,7 +82,7 @@ X-OurCoffeeAPI-Error-Kind:⮠
|
||||
"reason": "wrong_parameter_value",
|
||||
"localized_message":
|
||||
"Что-то пошло не так.⮠
|
||||
Обратитесь к разработчику приложения."
|
||||
Обратитесь к разработчику приложения.",
|
||||
"details": {
|
||||
"checks_failed": [
|
||||
{
|
||||
|
Loading…
x
Reference in New Issue
Block a user