You've already forked The-API-Book
mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-06-12 22:17:33 +02:00
style fix
This commit is contained in:
committed by
GitHub
parent
1274f6e148
commit
c7a5b1aedb
@ -130,6 +130,7 @@ A naïve approach to this situation is to design an interim abstraction level as
|
||||
"volume_prepared": "200ml",
|
||||
"readiness_policy": "check_volume",
|
||||
"ready": false,
|
||||
"coffee_machine_id",
|
||||
"operation_state": {
|
||||
"status": "executing",
|
||||
"operations": [
|
||||
@ -142,6 +143,19 @@ A naïve approach to this situation is to design an interim abstraction level as
|
||||
}
|
||||
```
|
||||
|
||||
So an `order` entity will keep links to a recipe and a task, thus not dealing with other abstraction layers directly:
|
||||
|
||||
```
|
||||
GET /v1/orders/{id}
|
||||
→
|
||||
{
|
||||
"recipe": "lungo",
|
||||
"task": {
|
||||
"id": <task id>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
We call this approach “naïve” not because it's wrong; on the contrary, that's quite a logical “default” solution if you don't know yet (or don't understand yet) how your API will look like. The problem with this approach lies in its speculativeness: it doesn't reflect the subject area's organization.
|
||||
|
||||
An experienced developer in this case must ask: what options do exist? how should we really determine the beverage readiness? If it turns out that comparing volumes *is* the only working method to tell whether the beverage is ready, then all the speculations above are wrong. You may safely include readiness-by-volume detection into your interfaces since no other methods exist. Before abstracting something we need to learn what exactly we're abstracting.
|
||||
@ -258,7 +272,7 @@ To be more specific, let's assume those two kinds of coffee machines provide the
|
||||
|
||||
Now the picture becomes more apparent: we need to abstract coffee machine API calls so that the “execution level” in our API provides general functions (like beverage readiness detection) in a unified form. We should also note that these two coffee machine API kinds belong to different abstraction levels themselves: the first one provides a higher-level API than the second one. Therefore, a “branch” of our API working with second-kind machines will be deeper.
|
||||
|
||||
The next step in abstraction level separating is determining what functionality we're abstracting. To do so we need to understand the tasks developers solve at the “order” level, and to learn what problems they get if our interim level is missing.
|
||||
The next step in abstraction level separating is determining what functionality we're abstracting. To do so, we need to understand the tasks developers solve at the “order” level, and to learn what problems they get if our interim level is missing.
|
||||
|
||||
1. Obviously, the developers desire to create an order uniformly: list high-level order properties (beverage kind, volume, and special options like syrup or milk type), and don't think about how the specific coffee machine executes it.
|
||||
2. Developers must be able to learn the execution state: is the order ready? if not — when to expect it's ready (and is there any sense to wait in case of execution errors).
|
||||
@ -269,13 +283,13 @@ Note, that the first-kind API is much closer to developers' needs than the secon
|
||||
* absence of explicit “programs” to “recipes” relation; program identifier is of no use to developers since there is a “recipe” concept;
|
||||
* absence of explicit “ready” status.
|
||||
|
||||
But with the second-kind API, it's much worse. The main problem we foresee is an absence of “memory” for actions being executed. Functions and sensors API is totally stateless, which means we don't even understand who called a function being currently executed, when, and which order it relates.
|
||||
But with the second-kind API, it's much worse. The main problem we foresee is an absence of “memory” for actions being executed. Functions and sensors API is totally stateless, which means we don't even understand who called a function being currently executed, or when, or which order it relates.
|
||||
|
||||
So we need to introduce two abstraction levels.
|
||||
|
||||
1. Execution control level, which provides the uniform interface to indivisible programs. “Uniform interface” means here that, regardless of a coffee machine's kind, developers may expect:
|
||||
|
||||
* statuses and other high-level execution parameters nomenclature (for example, estimated preparation time or possible execution error) being the same;
|
||||
* statuses and other high-level execution parameters nomenclature (for example, estimated preparation time or possible execution errors) being the same;
|
||||
* methods nomenclature (for example, order cancellation method) and their behavior being the same.
|
||||
|
||||
2. Program runtime level. For the first-kind API, it will provide just a wrapper for existing programs API; for the second-kind API, the entire “runtime” concept is to be developed from scratch by us.
|
||||
@ -294,13 +308,16 @@ POST /v1/orders
|
||||
```
|
||||
|
||||
The `POST /orders` handler checks all order parameters, puts a hold of the corresponding sum on the user's credit card, forms a request to run, and calls the execution level. First, a correct execution program needs to be fetched:
|
||||
|
||||
```
|
||||
POST /v1/program-matcher
|
||||
{ "recipe", "coffee-machine" }
|
||||
→
|
||||
{ "program_id" }
|
||||
```
|
||||
|
||||
Now, after obtaining a correct `program` identifier, the handler runs a program:
|
||||
|
||||
```
|
||||
POST /v1/programs/{id}/run
|
||||
{
|
||||
@ -325,7 +342,7 @@ This approach has some benefits, like the possibility to provide different sets
|
||||
* call `POST /execute` physical API method, passing internal program identifier — for the first API kind;
|
||||
* initiate runtime creation to proceed with the second API kind.
|
||||
|
||||
Out of general concerns runtime level for the second-kind API will be private, so we are more or less free in implementing it. The easiest solution would be to develop a virtual state machine that creates a “runtime” (e.g. a stateful execution context) to run a program and control its state.
|
||||
Out of general considerations, runtime level for the second-kind API will be private, so we are more or less free in implementing it. The easiest solution would be to develop a virtual state machine that creates a “runtime” (e.g. a stateful execution context) to run a program and control its state.
|
||||
|
||||
```
|
||||
POST /v1/runtimes
|
||||
@ -352,7 +369,9 @@ The `program` here would look like that:
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
And the `state` like that:
|
||||
|
||||
```
|
||||
{
|
||||
// Runtime status:
|
||||
@ -403,7 +422,7 @@ Get back to our example. How retrieving order status would work? To obtain a sta
|
||||
|
||||
**NB**: The “call chain” wording shouldn't be treated literally. Each abstraction level might be organized differently in a technical sense:
|
||||
* there might be explicit proxying of calls down the hierarchy;
|
||||
* there might be a cache at each level, being updated upon receiving a callback call or an event. In particular, a low-level runtime execution cycle obviously must be independent of upper levels, renew its state in the background, and not wait for an explicit call.
|
||||
* there might be a cache at each level, being updated upon receiving a callback call or an event. In particular, a low-level runtime execution cycle obviously must be independent of upper levels, which implies renewing its state in the background, and not waiting for an explicit call.
|
||||
|
||||
Note what happens here: each abstraction level wields its own status (e.g. order, runtime, sensors status), being formulated in subject area terms corresponding to this level. Forbidding the “jumping over” results in the necessity to spawn statuses at each level independently.
|
||||
|
||||
@ -416,7 +435,7 @@ Let's now look at how the order cancel operation flows through our abstraction l
|
||||
* the `runs/cancel` handler completes operations on its level of responsibility and, depending on the coffee machine API kind, proceeds with one of two possible execution branches:
|
||||
* either calls the `POST /execution/cancel` method of a physical coffee machine API;
|
||||
* or invokes the `POST /v1/runtimes/{id}/terminate` method;
|
||||
* in a second case the call chain continues as the `terminate` handler operates its internal state:
|
||||
* in the second case the call chain continues as the `terminate` handler operates its internal state:
|
||||
* changes the `resolution` to `"terminated"`;
|
||||
* runs the `"discard_cup"` command.
|
||||
|
||||
@ -429,7 +448,7 @@ Handling state-modifying operations like the `cancel` one requires more advanced
|
||||
|
||||
2. From a high-level point of view, canceling an order is a terminal action, since no further operations are possible. From a low-level point of view, the processing continues until the cup is discarded, and then the machine is to be unlocked (e.g. new runtimes creation allowed). It's a task to the execution control level to couple those two states, outer (the order is canceled) and inner (the execution continues).
|
||||
|
||||
It might look that forcing the abstraction levels isolation is redundant and makes interfaces more complicated. In fact, it is: it's very important to understand that flexibility, consistency, readability, and extensibility come with a price. One may construct an API with zero overhead, essentially just providing access to the coffee machine's microcontrollers. However using such an API would be a disaster for a developer, not to mention the inability to extend it.
|
||||
It might look like forcing the abstraction levels isolation is redundant and makes interfaces more complicated. In fact, it is: it's very important to understand that flexibility, consistency, readability, and extensibility come with a price. One may construct an API with zero overhead, essentially just providing access to the coffee machine's microcontrollers. However using such an API would be a disaster for a developer, not to mention the inability to extend it.
|
||||
|
||||
Separating abstraction levels is first of all a logical procedure: how we explain to ourselves and developers what our API consists of. **The abstraction gap between entities exists objectively**, no matter what interfaces we design. Our task is just separate this gap into levels *explicitly*. The more implicitly abstraction levels are separated (or worse — blended into each other), the more complicated is your API's learning curve, and the worse is the code that uses it.
|
||||
|
||||
@ -443,8 +462,8 @@ What data flow do we have in our coffee API?
|
||||
|
||||
1. It starts with the sensors data, i.e. volumes of coffee / water / cups. This is the lowest data level we have, and here we can't change anything.
|
||||
|
||||
2. A continuous sensors data stream is being transformed into discrete command execution statuses, injecting new concepts which don't exist within the subject area. A coffee machine API doesn't provide a “coffee is being shed” or a “cup is being set” notion. It's our software that treats incoming sensors data and introduces new terms: if the volume of coffee or water is less than the target one, then the process isn't over yet. If the target value is reached, then this synthetic status is to be switched, and the next command to be executed.
|
||||
It is important to note that we don't calculate new variables out from sensors data: we need to create a new dataset first, a context, an “execution program” comprising a sequence of steps and conditions, and to fill it with initial values. If this context is missing, it's impossible to understand what's happening with the machine.
|
||||
2. A continuous sensors data stream is being transformed into discrete command execution statuses, injecting new concepts which don't exist within the subject area. A coffee machine API doesn't provide a “coffee is being poured” or a “cup is being set” notion. It's our software that treats incoming sensors data and introduces new terms: if the volume of coffee or water is less than the target one, then the process isn't over yet. If the target value is reached, then this synthetic status is to be switched, and the next command to be executed.
|
||||
It is important to note that we don't calculate new variables out of sensors data: we need to create a new dataset first, a context, an “execution program” comprising a sequence of steps and conditions, and to fill it with initial values. If this context is missing, it's impossible to understand what's happening with the machine.
|
||||
|
||||
3. Having logical data about the program execution state, we can (again via creating a new high-level data context) merge two different data streams from two different kinds of APIs into a single stream, which provides in a unified form the data regarding executing a beverage preparation program with logical variables like the recipe, volume, and readiness status.
|
||||
|
||||
@ -459,7 +478,7 @@ We may also traverse the tree backward.
|
||||
3. At the runtime level, we read the target parameters (which operation to execute, and what the target volume is) and translate them into coffee machine API microcommands and statuses for each command.
|
||||
|
||||
Also, if we take a deeper look into the “bad” decision (forcing developers to determine actual order status on their own), being discussed at the beginning of this chapter, we could notice a data flow collision there:
|
||||
* from one side, in the order context “leaked” physical data (beverage volume prepared) is injected, therefore stirring abstraction levels irreversibly;
|
||||
* from the other side, the order context itself is deficient: it doesn't provide new meta-variables, non-existent at the lower levels (the order status, in particular), doesn't initialize them, and doesn't set the game rules.
|
||||
* on one hand, in the order context “leaked” physical data (beverage volume prepared) is injected, therefore stirring abstraction levels irreversibly;
|
||||
* on the other hand, the order context itself is deficient: it doesn't provide new meta-variables, non-existent at the lower levels (the order status, in particular), doesn't initialize them, and doesn't set the game rules.
|
||||
|
||||
We will discuss data contexts in more detail in Section II. Here we will just state that data flows and their transformations might be and must be examined as a specific API facet, which, from one side, helps us to separate abstraction levels properly, and, from the other side, to check if our theoretical structures work as intended.
|
||||
We will discuss data contexts in more detail in Section II. Here we will just state that data flows and their transformations might be and must be examined as a specific API facet, which helps us to separate abstraction levels properly and to check if our theoretical concepts work as intended.
|
||||
|
@ -120,6 +120,7 @@ GET /v1/orders/{id}
|
||||
"volume_prepared": "200ml",
|
||||
"readiness_policy": "check_volume",
|
||||
"ready": false,
|
||||
"coffee_machine_id",
|
||||
"operation_state": {
|
||||
"status": "executing",
|
||||
"operations": [
|
||||
@ -131,6 +132,19 @@ GET /v1/orders/{id}
|
||||
}
|
||||
```
|
||||
|
||||
Таким образом, сущность «заказ» будет только хранить ссылки на рецепт и исполняемую задачу и не вторгаться в «чужие» уровни абстракции:
|
||||
|
||||
```
|
||||
GET /v1/orders/{id}
|
||||
→
|
||||
{
|
||||
"recipe": "lungo",
|
||||
"task": {
|
||||
"id": <task id>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Мы называем этот подход «наивным» не потому, что он неправильный; напротив, это вполне логичное решение «по умолчанию», если вы на данном этапе ещё не знаете или не понимаете, как будет выглядеть ваш API. Проблема его в том, что он умозрительный: он не добавляет понимания того, как устроена предметная область.
|
||||
|
||||
Хороший разработчик в нашем примере должен спросить: хорошо, а какие вообще говоря существуют варианты? Как можно определять готовность напитка? Если вдруг окажется, что сравнение объёмов — единственный способ определения готовности во всех без исключения кофемашинах, то почти все рассуждения выше — неверны: можно совершенно спокойно включать в интерфейсы определение готовности кофе по объёму, т.к. никакого другого и не существует. Прежде, чем что-то абстрагировать — надо представлять, *что* мы, собственно, абстрагируем.
|
||||
|
Reference in New Issue
Block a user