mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-02-22 18:42:09 +02:00
style fix
This commit is contained in:
parent
bcc85cd68d
commit
5b26fc1e38
@ -45,6 +45,24 @@ Let's consider the question: how exactly developers should determine whether the
|
||||
* add a reference beverage volume to the lungo recipe;
|
||||
* add the currently prepared volume of beverage to the order state.
|
||||
|
||||
```
|
||||
GET /v1/recipes/lungo
|
||||
→
|
||||
{
|
||||
…
|
||||
"volume": "100ml"
|
||||
}
|
||||
```
|
||||
```
|
||||
GET /v1/orders/{id}
|
||||
→
|
||||
{
|
||||
…
|
||||
"volume": "80ml"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
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.
|
||||
@ -53,22 +71,38 @@ This solution intuitively looks bad, and it really is: it violates all the above
|
||||
|
||||
**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.
|
||||
|
||||
Variant 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 the mass production of recipes, only different in volume, or to introduce some recipe “inheritance” to be able to specify the “base” recipe and just redefine the volume.
|
||||
|
||||
Variant 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 being just the default values. We allow requesting different cup volumes while placing an order:
|
||||
|
||||
```
|
||||
POST /v1/orders
|
||||
{
|
||||
"coffee_machine_id",
|
||||
"recipe":"lungo",
|
||||
"volume":"800ml"
|
||||
"recipe": "lungo",
|
||||
"volume": "800ml"
|
||||
}
|
||||
```
|
||||
|
||||
For those orders with an arbitrary volume requested, a developer will need to obtain the requested volume not from the `GET /v1/recipes` endpoint, but the `GET /v1/orders` one. Doing so we're getting a whole bunch of related problems:
|
||||
* there is a significant chance that developers will make mistakes in this functionality implementation if they add arbitrary volume support in the code working with the `POST /v1/orders` handler, but forget to make corresponding changes in the order readiness check code;
|
||||
* the same field (coffee volume) now means different things in different interfaces. In the `GET /v1/recipes` context the `volume` field means “a volume to be prepared if no arbitrary volume is specified in the `POST /v1/orders` request”; and it cannot be renamed to “default volume” easily, we now have to live with that.
|
||||
* the same field (coffee volume) now means different things in different interfaces. In the `GET /v1/recipes` context the `volume` field means “a volume to be prepared if no arbitrary volume is specified in the `POST /v1/orders` request”; and it cannot be renamed to “default volume” easily.
|
||||
|
||||
So we will get
|
||||
```
|
||||
GET /v1/orders/{id}
|
||||
→
|
||||
{
|
||||
…
|
||||
// this is a currently
|
||||
// prepared volume, bearing
|
||||
// the legacy name
|
||||
"volume": "80ml",
|
||||
// and this is the volume
|
||||
// requested by user
|
||||
"volume_requested": "800ml"
|
||||
}
|
||||
```
|
||||
|
||||
**Third**, the entire scheme becomes totally inoperable if different types of coffee machines produce different volumes of lungo. To introduce the “lungo volume depends on machine type” constraint we have to do quite a nasty thing: make recipes depend on coffee machine ids. By doing so we start actively “stir” abstraction levels: one part of our API (recipe endpoints) becomes unusable without explicit knowledge of another part (coffee machines listing). And what is even worse, developers will have to change the logic of their apps: previously it was possible to choose volume first, then a coffee machine; but now this step must be rebuilt from scratch.
|
||||
|
||||
@ -76,19 +110,19 @@ Okay, we understood how to make things naughty. But how to make them *nice*?
|
||||
|
||||
Abstraction levels separation should go in three directions:
|
||||
|
||||
1. From user scenarios to their internal representation: high-level entities and their method nomenclature must directly reflect API usage scenarios; low-level entities reflect the decomposition of scenarios into smaller parts.
|
||||
1. From user scenarios to their internal representation: high-level entities and their method nomenclatures must directly reflect the API usage scenarios; low-level entities reflect the decomposition of the scenarios into smaller parts.
|
||||
|
||||
2. From user subject field terms to “raw” data subject field terms — in our case from high-level terms like “order”, “recipe”, “café” to low-level terms like “beverage temperature”, “coffee machine geographical coordinates”, etc.
|
||||
2. From user to “raw” data subject field terms — in our case from high-level terms like “order”, “recipe”, “café” to low-level terms like “beverage temperature”, “coffee machine geographical coordinates”, etc.
|
||||
|
||||
3. Finally, from data structures suitable for end users to “raw” data structures — in our case, from “lungo recipe” and “"Chamomile" café chain” to the raw byte data stream from “Good Morning” coffee machine sensors.
|
||||
|
||||
The more is the distance between programmable contexts our API connects, the deeper is the hierarchy of the entities we are to develop.
|
||||
|
||||
In our example with coffee readiness detection we clearly face the situation when we need an interim abstraction level:
|
||||
In our example with coffee readiness detection, we clearly face the situation when we need an interim abstraction level:
|
||||
* from one side, an “order” should not store the data regarding coffee machine sensors;
|
||||
* on the other side, a coffee machine should not store the data regarding order properties (and its API probably doesn't provide such functionality).
|
||||
|
||||
A naïve approach to this situation is to design an interim abstraction level as a “connecting link,” which reformulates tasks from one abstraction level to another. For example, introduce a `task` entity like that:
|
||||
A naïve approach to this situation is to design an interim abstraction level as a “connecting link,” which reformulates tasks from one abstraction level into another. For example, introduce a `task` entity like that:
|
||||
|
||||
```
|
||||
{
|
||||
@ -111,7 +145,7 @@ A naïve approach to this situation is to design an interim abstraction level as
|
||||
|
||||
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 we really should determine 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.
|
||||
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.
|
||||
|
||||
In our example let's assume that we have studied coffee machines' API specs, and learned that two device types exist:
|
||||
* coffee machines capable of executing programs coded in the firmware; the only customizable options are some beverage parameters, like desired volume, a syrup flavor, and a kind of milk;
|
||||
@ -119,7 +153,7 @@ In our example let's assume that we have studied coffee machines' API specs, and
|
||||
|
||||
To be more specific, let's assume those two kinds of coffee machines provide the following physical API.
|
||||
|
||||
* Coffee machines with prebuilt programs:
|
||||
* Coffee machines with pre-built programs:
|
||||
```
|
||||
// Returns a list of programs
|
||||
GET /programs
|
||||
@ -161,7 +195,7 @@ To be more specific, let's assume those two kinds of coffee machines provide the
|
||||
GET /execution/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 would really get something like that from vendors, and this API is actually quite a sane one.
|
||||
**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:
|
||||
```
|
||||
@ -223,7 +257,7 @@ To be more specific, let's assume those two kinds of coffee machines provide the
|
||||
|
||||
**NB**. The example is intentionally factitious to model a 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 more intricate.
|
||||
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.
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user