1
0
mirror of https://github.com/twirl/The-API-Book.git synced 2025-07-12 22:50:21 +02:00

proofreading

This commit is contained in:
Sergey Konstantinov
2023-05-01 23:50:41 +03:00
parent 69fc906a8d
commit 25ffa2dbea
3 changed files with 101 additions and 94 deletions

View File

@ -1,34 +1,34 @@
### [Separating Abstraction Levels][api-design-separating-abstractions]
“Separate abstraction levels in your code” is possibly the most general advice to software developers. However, we don't think it would be a grave exaggeration to say that abstraction level separation is also the most difficult task for API developers.
“Separate abstraction levels in your code” is possibly the most general advice for software developers. However, we don't think it would be a grave exaggeration to say that separating abstraction levels is also the most challenging task for API developers.
Before proceeding to the theory, we should formulate clearly *why* abstraction levels are so important, and what goals we're trying to achieve by separating them.
Before proceeding to the theory, we should clearly formulate *why* abstraction levels are so important, and what goals we're trying to achieve by separating them.
Let us remember that software product is a medium connecting two outstanding contexts, thus transforming terms and operations belonging to one subject area into another area's concepts. The more these areas differ, the more interim connecting links we have to introduce.
Let us remember that a software product is a medium that connects two distinct contexts, thus transforming terms and operations belonging to one subject area into concepts from another area. The more these areas differ, the more interim connecting links we have to introduce.
Back to our coffee example. What entity abstraction levels do we see?
Returning to our coffee example, what entity abstraction levels do we see?
1. We're preparing an `order` via the API: one (or more) cups of coffee, and receive payments for this.
2. Each cup of coffee is prepared according to some `recipe`, which implies the presence of different ingredients and sequences of preparation steps.
3. Each beverage is being prepared on some physical `coffee machine`, occupying some position in space.
1. We're preparing an `order` via the API one (or more) cups of coffee and receiving payments for this.
2. Each cup of coffee is prepared according to some `recipe` implying the presence of various ingredients and sequences of preparation steps.
3. Each beverage is prepared on a physical `coffee machine`, occupying some position in space.
Every level presents a developer-facing “facet” in our API. While elaborating on the hierarchy of abstractions, we are first of all trying to reduce the interconnectivity of different entities. That would help us to reach several goals.
Each level presents a developer-facing “facet” in our API. While elaborating on the hierarchy of abstractions, we are primarily trying to reduce the interconnectivity of different entities. This would help us to achieve several goals:
1. Simplifying developers' work and the learning curve. At each moment of time, a developer is operating only those entities which are necessary for the task they're solving right now. And conversely, badly designed isolation leads to the situation when developers have to keep in mind lots of concepts mostly unrelated to the task being solved.
1. Simplifying developers' work and the learning curve. At each moment, a developer is operating only those entities that are necessary for the task they're solving right now. Conversely, poorly designed isolation leads to situations where developers have to keep in mind a lot of concepts mostly unrelated to the task being solved.
2. Preserving backward compatibility. Properly separated abstraction levels allow for adding new functionality while keeping interfaces intact.
3. Maintaining interoperability. Properly isolated low-level abstractions help us to adapt the API to different platforms and technologies without changing high-level entities.
Let's say we have the following interface:
Let's assume we have the following interface:
```
// Returns lungo recipe
// Returns the lungo recipe
GET /v1/recipes/lungo
```
```
// Posts an order to make a lungo
// using specified coffee-machine,
// using the specified coffee-machine,
// and returns an order identifier
POST /v1/orders
{
@ -37,13 +37,13 @@ POST /v1/orders
}
```
```
// Returns order state
// Returns the order
GET /v1/orders/{id}
```
Let's consider the question: how exactly developers should determine whether the order is ready or not? Let's say we do the following:
Let's consider a question: how exactly should developers determine whether the order is ready or not? Let's say we do the following:
* add a reference beverage volume to the lungo recipe;
* add the currently prepared volume of beverage to the order state.
* add the currently prepared volume of the beverage to the order state.
```
GET /v1/recipes/lungo
@ -64,15 +64,15 @@ GET /v1/orders/{id}
Then a developer just needs to compare two numbers to find out whether the order is ready.
This solution intuitively looks bad, and it really is: it violates all the abovementioned principles.
This solution intuitively looks bad, and it really is. It violates all the aforementioned principles.
**First**, to solve the task “order a lungo” a developer needs to refer to the “recipe” entity and learn that every recipe has an associated volume. Then they need to embrace the concept that an order is ready at that particular moment when the prepared beverage volume becomes equal to the reference one. This concept is simply unguessable, and knowing it is mostly useless.
**Second**, we will have automatically got problems if we need to vary the beverage size. For example, if one day we decide to offer a choice to a customer, how many milliliters of lungo they desire exactly, then we have to perform one of the following tricks.
**Second**, we will have automatically got problems if we need to vary the beverage size. For example, if one day we decide to offer customers a choice of how many milliliters of lungo they desire exactly, then we have to perform one of the following tricks.
Option I: we have a list of possible volumes fixed and introduce bogus recipes like `/recipes/small-lungo` or `recipes/large-lungo`. Why “bogus”? Because it's still the same lungo recipe, same ingredients, same preparation steps, only volumes differ. We will have to start the mass production of recipes, only different in volume, or introduce some recipe “inheritance” to be able to specify the “base” recipe and just redefine the volume.
Option I: we have a list of possible volumes fixed and introduce bogus recipes like `/recipes/small-lungo` or `recipes/large-lungo`. Why “bogus”? Because it's still the same lungo recipe, same ingredients, same preparation steps, only volumes differ. We will have to start mass-producing recipes, only different in volume, or introduce some recipe “inheritance” to be able to specify the “base” recipe and just redefine the volume.
Option II: we modify an interface, pronouncing volumes stated in recipes being just the default values. We allow requesting different cup volumes while placing an order:
Option II: we modify an interface, pronouncing volumes stated in recipes are just the default values. We allow requesting different cup volumes while placing an order:
```
POST /v1/orders
@ -144,7 +144,7 @@ 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:
So an `order` entity will keep links to the recipe and the task, thus not dealing with other abstraction layers directly:
```
GET /v1/orders/{id}
@ -169,11 +169,12 @@ To be more specific, let's assume those two kinds of coffee machines provide the
* Coffee machines with pre-built programs:
```
// Returns a list of programs
// Returns the list of
// available programs
GET /programs
{
// program identifier
// a program identifier
"program": 1,
// coffee type
"type": "lungo"
@ -181,8 +182,8 @@ To be more specific, let's assume those two kinds of coffee machines provide the
```
```
// Starts an execution
// of a specified program
// and returns execution status
// of the specified program
// and returns the execution status
POST /execute
{
"program": 1,
@ -190,43 +191,46 @@ To be more specific, let's assume those two kinds of coffee machines provide the
}
{
// Unique identifier of the execution
// A unique identifier
// of the execution
"execution_id": "01-01",
// Identifier of the program
// An identifier of the program
"program": 1,
// Beverage volume requested
// The requested beverage volume
"volume": "200ml"
}
```
```
// Cancels current program
// Cancels the current program
POST /cancel
```
```
// Returns execution status.
// The format is the same
// Returns the execution status.
// The response format is the same
// as in the `POST /execute` method
GET /execution/status
GET /execution/{id}/status
```
**NB**. Just in case: this API violates a number of design principles, starting with a lack of versioning; it's described in such a manner because of two reasons: (1) to demonstrate how to design a more convenient API, (2) in the real life, you will really get something like that from vendors, and this API is actually quite a sane one.
* Coffee machines with built-in functions:
```
// Returns a list of functions available
// Returns the list of
// available functions
GET /functions
{
"functions": [
{
// Operation type:
// One of the available
// operation types:
// * set_cup
// * grind_coffee
// * pour_water
// * discard_cup
"type": "set_cup",
// Arguments available
// to each operation.
// Arguments for the
// operation.
// To keep it simple,
// let's limit these to one:
// * volume
@ -251,13 +255,13 @@ To be more specific, let's assume those two kinds of coffee machines provide the
}
```
```
// Returns sensors' state
// Returns the state of the sensors
GET /sensors
{
"sensors": [
{
// Values allowed:
// Possible values:
// * cup_volume
// * ground_coffee_volume
// * cup_filled_volume
@ -271,20 +275,20 @@ To be more specific, let's assume those two kinds of coffee machines provide the
**NB**. The example is intentionally fictitious to model the situation described above: to determine beverage readiness you have to compare the requested volume with volume sensor readings.
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.
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 the 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 learn what problems they face 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)?
2. Developers must be able to learn the execution state: is the order ready? If not, when can they expect it to be ready (and is there any sense to wait in case of execution errors)?
3. Developers need to address the order's location in space and time — to explain to users where and when they should pick the order up.
4. Finally, developers need to run atomic operations, like canceling orders.
Note, that the first-kind API is much closer to developers' needs than the second-kind API. An indivisible “program” is a way more convenient concept than working with raw commands and sensor data. There are only two problems we see in the first-kind API:
* 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.
* absence of an 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, or to what order it relates.
But with the second-kind API, it's much worse. The main problem we foresee is the absence of “memory” for actions being executed. The functions and sensors API is totally stateless, which means we don't even understand who called a function being currently executed, when, or to what order it relates.
So we need to introduce two abstraction levels.
@ -317,7 +321,7 @@ POST /v1/program-matcher
{ "program_id" }
```
Now, after obtaining a correct `program` identifier, the handler runs a program:
Now, after obtaining the correct `program` identifier, the handler runs the program:
```
POST /v1/programs/{id}/run
@ -335,12 +339,12 @@ POST /v1/programs/{id}/run
{ "program_run_id" }
```
Please note that knowing the coffee machine API kind isn't required at all; that's why we're making abstractions! We could possibly make interfaces more specific, implementing different `run` and `match` endpoints for different coffee machines:
Please note that knowing the coffee machine API kind isn't required at all; that's why we're making abstractions! We could possibly make the interfaces more specific by implementing different `run` and `match` endpoints for different coffee machines:
* `POST /v1/program-matcher/{api_type}`
* `POST /v1/{api_type}/programs/{id}/run`
This approach has some benefits, like the possibility to provide different sets of parameters, specific to the API kind. But we see no need for such fragmentation. `run` method handler is capable of extracting all the program metadata and performing one of two actions:
* call `POST /execute` physical API method, passing internal program identifier for the first API kind;
This approach has some benefits, like the possibility to provide different sets of parameters, specific to the API kind. But we see no need for such fragmentation. The `run` method handler is capable of extracting all the program metadata and performing one of two actions:
* call the `POST /execute` physical API method, passing the internal program identifier for the first API kind;
* initiate runtime creation to proceed with the second API kind.
Out of general considerations, the 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” (i.e., a stateful execution context) to run a program and control its state.
@ -375,11 +379,14 @@ And the `state` like that:
```
{
// Runtime status:
// The `runtime` status:
// * "pending" — awaiting execution
// * "executing" — performing some command
// * "ready_waiting" — beverage is ready
// * "finished" — all operations done
// * "executing" — performing
// some command
// * "ready_waiting" — the beverage
// is ready
// * "finished" — all operations
// are done
"status": "ready_waiting",
// Command being currently executed.
// Similar to line numbers
@ -387,75 +394,75 @@ And the `state` like that:
"command_sequence_id",
// How the execution concluded:
// * "success"
// — beverage prepared and taken
// — the beverage prepared and taken
// * "terminated"
// — execution aborted
// — the execution aborted
// * "technical_error"
// — preparation error
// — a preparation error
// * "waiting_time_exceeded"
// — beverage prepared,
// but not taken;
// timed out then disposed
"resolution": "success",
// All variables values,
// including sensors state
// The values of all variables,
// including the state of the sensors
"variables"
}
```
**NB**: while implementing the `orders` → `match` → `run` → `runtimes` call sequence we have two options:
* either `POST /orders` handler requests the data regarding the recipe, the coffee machine model, and the program on its own behalf, and forms a stateless request which contains all the necessary data (the API kind, command sequence, etc.);
**NB**: when implementing the `orders` → `match` → `run` → `runtimes` call sequence, we have two options:
* either `POST /orders` handler requests the data regarding the recipe, the coffee machine model, and the program on its own, and forms a stateless request that contains all necessary data (API kind, command sequence, etc.);
* or the request contains only data identifiers, and the next handler in the chain will request pieces of data it needs via some internal APIs.
Both variants are plausible, selecting one of them depends on implementation details.
Both variants are plausible and the selection between them depends on implementation details.
#### Abstraction Levels Isolation
A crucial quality of properly separated abstraction levels (and therefore a requirement to their design) is a level isolation restriction: **only adjacent levels may interact**. If “jumping over” is needed in the API design, then clearly mistakes were made.
Get back to our example. How retrieving order status would work? To obtain a status the following call chain is to be performed:
* a user initiates a call to the `GET /v1/orders` method;
* the `orders` handler completes operations on its level of responsibility (for example, checks user authorization), finds `program_run_id` identifier and performs a call to the `runs/{program_run_id}` endpoint;
* the `runs` endpoint in its turn completes operations corresponding to its level (for example, checks the coffee machine API kind) and, depending on the API kind, proceeds with one of two possible execution branches:
* either calls the `GET /execution/status` method of a physical coffee machine API, gets the coffee volume, and compares it to the reference value;
* or invokes the `GET /v1/runtimes/{runtime_id}` method to obtain the `state.status` and converts it to the order status;
* in the case of the second-kind API, the call chain continues: the `GET /runtimes` handler invokes the `GET /sensors` method of a physical coffee machine API and performs some manipulations with the data, like comparing the cup / ground coffee / shed water volumes with the reference ones, and changing the state and the status if needed.
Returning to our example, how would retrieving the order status work? To obtain a status the following call chain is to be performed:
* A user initiates a call to the `GET /v1/orders` method.
* The `orders` handler completes operations on its level of responsibility (e.g., checks user authorization), finds the `program_run_id` identifier and performs a call to the `runs/{program_run_id}` endpoint.
* The `runs` endpoint completes operations corresponding to its level (e.g., checks the coffee machine API kind) and, depending on the API kind, proceeds with one of two possible execution branches:
* either calls the `GET /execution/status` method of the physical coffee machine API, gets the coffee volume, and compares it to the reference value
* or invokes the `GET /v1/runtimes/{runtime_id}` method to obtain the `state.status` and converts it to the order status.
* In the case of the second-kind API, the call chain continues: the `GET /runtimes` handler invokes the `GET /sensors` method of the physical coffee machine API and performs some manipulations with the data, like comparing the cup / ground coffee / shed water volumes with the reference ones, and changing the state and the status if needed.
**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, which implies renewing its state in the background, and not waiting for an explicit call.
**NB**: The term “call chain” shouldn't be taken literally. Each abstraction level may be organized differently in a technical sense. For example:
* there might be explicit proxying of calls down the hierarchy
* there might be a cache at each level, which is 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 (i.e., order, runtime, and sensors status respectively), 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.
Note what happens here: each abstraction level wields its own status (i.e., order, runtime, and sensors status respectively) formulated in subject area terms corresponding to this level. Forbidding “jumping over” results in the necessity to spawn statuses at each level independently.
Let's now look at how the order cancel operation flows through our abstraction levels. In this case, the call chain will look like that:
* a user initiates a call to the `POST /v1/orders/{id}/cancel` method;
* the method handler completes operations on its level of responsibility:
* checks the authorization;
* solves money issues, i.e., whether a refund is needed;
* finds the `program_run_id` identifier and calls the `runs/{program_run_id}/cancel` method;
* 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 the second case, the call chain continues as the `terminate` handler operates its internal state:
* changes the `resolution` to `"terminated"`;
Now let's examine how the order cancel operation flows through our abstraction levels. In this case, the call chain will look like this:
* A user initiates a call to the `POST /v1/orders/{id}/cancel` method.
* The method handler completes operations on its level of responsibility:
* checks the authorization
* resolves money issues (e.g., whether a refund is needed)
* finds the `program_run_id` identifier and calls the `runs/{program_run_id}/cancel` method.
* 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:
* calls the `POST /execution/cancel` method of a physical coffee machine API, or
* invokes the `POST /v1/runtimes/{id}/terminate` method.
* 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.
Handling state-modifying operations like the `cancel` one requires more advanced abstraction levels juggling skills compared to non-modifying calls like the `GET /status` one. There are two important moments:
Handling state-modifying operations like the `cancel` operation requires more advanced abstraction-level juggling skills compared to non-modifying calls like the `GET /status` method. There are two important moments to consider:
1. At each abstraction level the idea of “order canceling” is reformulated:
* at the `orders` level, this action in fact splits into several “cancels” of other levels: you need to cancel money holding and to cancel an order execution;
* at the second API kind, physical level the “cancel” operation itself doesn't exist: “cancel” means “executing the `discard_cup` command,” which is quite the same as any other command.
* at the `orders` level, this action splits into several “cancels” of other levels: you need to cancel money holding and cancel order execution
* at the second API kind, physical level the “cancel” operation itself doesn't exist; “cancel” means “executing the `discard_cup` command,” which is quite the same as any other command.
The interim API level is needed to make this transition between different level “cancels” smooth and rational without jumping over canyons.
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 (i.e., 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).
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, processing continues until the cup is discarded, and then the machine is to be unlocked (i.e., new runtimes creation allowed). It's an execution control level's task to couple those two states, outer (the order is canceled) and inner (the execution continues).
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.
It might seem like forcing the abstraction levels isolation is redundant and makes interfaces more complicated. In fact, it is. It's essential 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 to sort 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.
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 to sort this gap into levels *explicitly*. The more implicitly abstraction levels are separated (or worse — blended into each other), the more complicated your API's learning curve is, and the worse the code that uses it will be.
#### The Data Flow
One useful exercise allowing us to examine the entire abstraction hierarchy is excluding all the particulars and constructing (on paper or just in your head) a data flow chart: what data is flowing through your API entities, and how it's being altered at each step.
One useful exercise that allows us to examine the entire abstraction hierarchy is to exclude all the particulars and construct a data flow chart, either on paper or in our head. This chart shows what data is flowing through your API entities, and how it's being altered at each step.
This exercise doesn't just help but also allows us design really large APIs with huge entity nomenclatures. Human memory isn't boundless; any project which grows extensively will eventually become too big to keep the entire entity hierarchy in mind. But it's usually possible to keep in mind the data flow chart, or at least keep a much larger portion of the hierarchy.
@ -472,14 +479,14 @@ Each API abstraction level, therefore corresponds to some data flow generalizati
We may also traverse the tree backward.
1. At the order level, we set its logical parameters: recipe, volume, execution place and possible statuses set.
1. At the order level, we set its logical parameters: recipe, volume, execution place and possible status set.
2. At the execution level, we read the order level data and create a lower level execution context: the program as a sequence of steps, their parameters, transition rules, and initial state.
2. At the execution level, we read the order-level data and create a lower-level execution context: the program as a sequence of steps, their parameters, transition rules, and initial state.
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:
* on one hand, in the order context “leaked” physical data (beverage volume prepared) is injected, therefore stirring abstraction levels irreversibly;
Also, if we take a deeper look at the “bad” decision (forcing developers to determine the actual order status on their own), being discussed at the beginning of this chapter, we could notice a data flow collision there:
* on the one hand, in the order context “leaked” physical data (beverage volume prepared) is injected, 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 helps us to separate abstraction levels properly and to check if our theoretical concepts 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 separate abstraction levels properly and check if our theoretical concepts work as intended.

View File

@ -78,7 +78,7 @@ In the case of the API-first approach, the backward compatibility problem gets o
The question of whether two specification versions are backward-compatible or not rather belongs to a gray zone, as specification standards themselves do not define this. Generally speaking, the “specification change is backward-compatible” statement is equivalent to “any client code written or generated based on the previous version of the spec continues working correctly after the API vendor releases the new API version implementing the new version of the spec.” Practically speaking, following this definition seems quite unrealistic for two reasons: it's impossible to learn the behavior of every piece of code-generating software there (for instance, it's rather hard to say whether code generated based on a specification that includes the parameter `additionalProperties: false` will still function properly if the server starts returning additional fields).
Thus, using IDLs to describe APIs with all advantages it undeniably brings to the field, leads to having one more side to the technology drift problem: the IDL version and, more importantly, versions of helper software based on it, are constantly and sometimes unpredictably evolving.
Thus, using IDLs to describe APIs with all advantages it undeniably brings to the field, leads to having one more side to the technology drift problem: the IDL version and, more importantly, versions of helper software based on it, are constantly and sometimes unpredictably evolving. If an API vendor employs the “code-first” approach, i.e., generates spec based on the actual API code, the occurrence of backward-incompatible changes in the server code — spec — code-generated SDK — client app chain is but a matter of time.
**NB**: we incline to recommend sticking to reasonable practices, i.e., don't use the functionality that is controversial from the backward compatibility point of view (including the above-mentioned `additionalProperties: false`) and, while evaluating the safety of changes, consider spec-generated code behave just like a manually written one. If you still get into the situation of unresolvable doubts, your only option is to manually check every code generator with regards to whether its output continues working with the new version of the API.

View File

@ -78,7 +78,7 @@
Вообще вопрос того, являются ли две версии спецификации обратно совместимыми — относится скорее к серой зоне, поскольку в самих стандартах спецификаций такое понятие не определено. Из общих соображений, утверждение «изменение спецификации является обратно-совместимым» тождественно утверждению «любой клиентский код, написанный или сгенерированный по этой спецификации, продолжит работать функционально корректно после релиза сервера, соответствующего обновлённой версии спецификации», однако в практическом смысле следовать этому определению достаточно тяжело. Изучить поведение всех мыслимых генераторов кода по спецификациям крайне трудоёмко (в частности, очень сложно предсказать, переживёт ли код, сгенерированный по спецификации с `additionaProperties`: `false` появление дополнительных полей в ответе).
Таким образом, использование IDL для описания API при всех плюсах этого подхода приводит к ещё одной существенной проблеме дрифта технологий: версии IDL и, что важнее, основанного на нём программного обеспечения, тоже постоянно обновляются, и далеко не всегда предсказуемым образом.
Таким образом, использование IDL для описания API при всех плюсах этого подхода приводит к ещё одной существенной проблеме дрифта технологий: версии IDL и, что важнее, основанного на нём программного обеспечения, тоже постоянно обновляются, и далеко не всегда предсказуемым образом. Если же разработчик API придерживается подхода «code-first», т.е. генерирует спецификацию из актуального кода API, то появление обратно-несовместимых изменений в цепочке код сервера — спецификация — кодогенерированный SDK — клиентское приложение можно считать делом времени.
**NB**: мы здесь склонны советовать придерживаться разумного подхода, а именно — не использовать потенциально проблемные с точки зрения обратной совместимости возможности (включая упомянутый `additionalProperties: false`) и при оценке совместимости изменений исходить из соображения, что сгенерированный по спецификации код ведёт себя так же, как и написанный вручную. В случае же неразрешимых сомнений вам не остаётся ничего другого, кроме как перебрать все имеющиеся кодогенераторы и проверить работоспособность их выдачи.