mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-04-17 11:06:25 +02:00
style fixes and clarifications
This commit is contained in:
parent
a72ceeece0
commit
f40bc5c812
294
docs/API.en.html
294
docs/API.en.html
@ -2961,8 +2961,8 @@ PUT /v1/partners/{partnerId}/coffee-machines
|
||||
<p>Alas, this dilemma can't be easily resolved. On one hand, we want developers to write neat and laconic code, so we must provide useful helpers and defaults. On the other hand, we can't know in advance which sets of options will be the most useful after several years of the API expansion.</p>
|
||||
<p><strong>NB</strong>. We might mask this problem in the following manner: one day gather all these oddities and re-define all the defaults with one single parameter. For example, introduce a special method like <code>POST /use-defaults {"version": "v2"}</code> which would overwrite all the defaults with more suitable values. That will ease the learning curve, but your documentation will become even worse after that.</p>
|
||||
<p>In the real world, the only viable approach to somehow tackle the problem is the weak entity coupling, which we will discuss in the next chapter.</p><div class="page-break"></div><h3><a href="#back-compat-strong-coupling" class="anchor" id="back-compat-strong-coupling">Chapter 16. Strong Coupling and Related Problems</a><a href="#chapter-16" class="secondary-anchor" id="chapter-16"> </a></h3>
|
||||
<p>To demonstrate the strong coupling problematics let us move to <em>really interesting</em> things. Let's continue our “variation analysis”: what if the partners wish to offer not only the standard beverages but their own unique coffee recipes to end-users? There is a catch in this question: the partner API as we described it in the previous chapter does not expose the very existence of the partner network to the end-user, and thus describes a simple case. Once we start providing methods to alter the core functionality, not just API extensions, we will soon face next-level problems.</p>
|
||||
<p>So, let us add one more endpoint to register the partner's own recipe:</p>
|
||||
<p>To demonstrate the strong coupling problematics let us move to <em>really interesting</em> things. Let's continue our “variation analysis”: what if the partners wish to offer not only the standard beverages but their own unique coffee recipes to end-users? The catch is that the partner API as we described it in the previous chapter does not expose the very existence of the partner network to the end-user, and thus describes a simple case. Once we start providing methods to alter the core functionality, not just API extensions, we will soon face next-level problems.</p>
|
||||
<p>So, let us add one more endpoint for registering the partner's own recipe:</p>
|
||||
<pre><code>// Adds new recipe
|
||||
POST /v1/recipes
|
||||
{
|
||||
@ -2970,9 +2970,9 @@ POST /v1/recipes
|
||||
"product_properties": {
|
||||
"name",
|
||||
"description",
|
||||
"default_value"
|
||||
// Other properties, describing
|
||||
// a beverage to end-user
|
||||
"default_volume"
|
||||
// Other properties to describe
|
||||
// the beverage to end-user
|
||||
…
|
||||
}
|
||||
}
|
||||
@ -2997,36 +2997,39 @@ POST /v1/recipes
|
||||
</ul>
|
||||
<p>The flaw in the first option is that a partner might be willing to use the service in some new country or language — and will be unable to do so until the API supports them. The flaw in the second option is that it works with predefined volumes only, so you can't order an arbitrary beverage volume. So the very first step we've made effectively has us trapped.</p>
|
||||
<p>The localization flaws are not the only problem with this API. We should ask ourselves a question — <em>why</em> do we really need these <code>name</code> and <code>description</code>? They are simply non-machine-readable strings with no specific semantics. At first glance, we need them to return them back in the <code>/v1/search</code> method response, but that's not a proper answer: why do we really return these strings from <code>search</code>?</p>
|
||||
<p>The correct answer lies a way beyond this specific interface. We need them <em>because some representation exists</em>. There is a UI for choosing beverage type. Probably the <code>name</code> and <code>description</code> fields are simply two designations of the beverage for a user to read, a short one (to be displayed on the search results page) and a long one (to be displayed in the extended product specification block). It actually means that we are setting the requirements to the API based on some very specific design. But <em>what if</em> a partner is making their own UI for their own app? Not only they might not actually need two descriptions, but we are also <em>deceiving</em> them. The <code>name</code> is not “just a name” actually, it implies some restrictions: it has recommended length which is optimal to some specific UI, and it must look consistently on the search results page. Indeed, “our best quality™ coffee” or “Invigorating Morning Freshness®” designation would look very weird in-between “Cappuccino,” “Lungo,” and “Latte.”</p>
|
||||
<p>There is also another side to this story. As UIs (both ours and partners') tend to evolve, new visual elements will be eventually introduced. For example, a picture of a beverage, its energy value, allergen information, etc. <code>product_properties</code> will become a scrapyard for tons of optional fields, and learning how setting what field results in what effects in the UI will be an interesting quest, full of probes and mistakes.</p>
|
||||
<p>The correct answer lies a way beyond this specific interface. We need them <em>because some representation exists</em>. There is a UI for choosing beverage type. Probably the <code>name</code> and <code>description</code> fields are simply two designations of the beverage for a user to read, a short one (to be displayed on the search results page) and a long one (to be displayed in the extended product specification block). It actually means that we set the requirements to the API based on some specific design. But <em>what if</em> a partner is making their own UI for their own app? Not only they might not actually need two descriptions, but we are also <em>deceiving</em> them. The <code>name</code> is not “just a name”, it implies some restrictions: it has recommended length which is optimal to some specific UI, and it must look consistently on the search results page. Indeed, “our best quality™ coffee” or “Invigorating Morning Freshness®” designation would look very weird in-between “Cappuccino,” “Lungo,” and “Latte.”</p>
|
||||
<p>There is also another side to this story. As UIs (both ours' and partners') tend to evolve, new visual elements will be eventually introduced. For example, a picture of the beverage, its energy value, allergen information, etc. The <code>product_properties</code> entity will become a scrapyard for tons of optional fields, and learning how setting what field results in what effects in the UI will be an interesting quest, full of probes and mistakes.</p>
|
||||
<p>Problems we're facing are the problems of <em>strong coupling</em>. Each time we offer an interface like described above, we in fact prescript implementing one entity (recipe) based on implementations of other entities (UI layout, localization rules). This approach disrespects the very basic principle of the “top to bottom” API design because <strong>low-level entities must not define high-level ones</strong>.</p>
|
||||
<h4>The rule of contexts</h4>
|
||||
<p>To make things worse, let us state that the inverse principle is actually correct either: high-level entities must not define low-level ones as well, since that simply isn't their responsibility. The exit from this logical labyrinth is that high-level entities must <em>define a context</em>, which other objects are to interpret. To properly design the interfaces for adding a new recipe we shouldn't try to find a better data format; we need to understand what contexts, both explicit and implicit, exist in our subject area.</p>
|
||||
<p>We have already found a localization context. There is some set of languages and regions we support in our API, and there are the requirements — what exactly partners must provide to make our API work in a new region. More specifically, there must be some formatting function to represent beverage volume somewhere in our API code, either internally or within an SDK:</p>
|
||||
<pre><code>l10n.volume.format(
|
||||
<p>To make things worse, let us state that the inverse principle is also correct: high-level entities must not define low-level ones as well, since that simply isn't their responsibility. The exit from this logical labyrinth is that high-level entities must <em>define a context</em>, which other objects are to interpret. To properly design the interfaces for adding a new recipe we shouldn't try to find a better data format; we need to understand what contexts, both explicit and implicit, exist in our subject area.</p>
|
||||
<p>We have already noted a localization context. There is some set of languages and regions we support in our API, and there are the requirements — what exactly partners must provide to make our API work in a new region. More specifically, there must be some formatting function to represent beverage volume somewhere in our API code, either internally or within an SDK:</p>
|
||||
<pre><code>l10n.volume.format = function(
|
||||
value, language_code, country_code
|
||||
)
|
||||
// l10n.formatVolume(
|
||||
// '300ml', 'en', 'UK'
|
||||
// ) → '300 ml'
|
||||
// l10n.formatVolume(
|
||||
// '300ml', 'en', 'US'
|
||||
// ) → '10 fl oz'
|
||||
) { … }
|
||||
/*
|
||||
l10n.formatVolume(
|
||||
'300ml', 'en', 'UK'
|
||||
) → '300 ml'
|
||||
l10n.formatVolume(
|
||||
'300ml', 'en', 'US'
|
||||
) → '10 fl oz'
|
||||
*/
|
||||
</code></pre>
|
||||
<p>To make our API work correctly with a new language or region, the partner must either define this function or point which pre-existing implementation to use through the partner API. Like this:</p>
|
||||
<pre><code>// Add a general formatting rule
|
||||
// for Russian language
|
||||
// for the Russian language
|
||||
PUT /formatters/volume/ru
|
||||
{
|
||||
"template": "{volume} мл"
|
||||
}
|
||||
// Add a specific formatting rule
|
||||
// for Russian language in the “US” region
|
||||
// for the Russian language
|
||||
// in the “US” region
|
||||
PUT /formatters/volume/ru/US
|
||||
{
|
||||
// in the US, we need to recalculate
|
||||
// the number, then add a postfix
|
||||
"value_preparation": {
|
||||
"value_transform": {
|
||||
"action": "divide",
|
||||
"divisor": 30
|
||||
},
|
||||
@ -3073,22 +3076,14 @@ PUT /formatters/volume/ru/US
|
||||
"search_title", "search_description"
|
||||
}
|
||||
</code></pre>
|
||||
<p>or create a layout of their own and provide data fields it requires:</p>
|
||||
<pre><code>POST /v1/layouts
|
||||
{
|
||||
"properties"
|
||||
}
|
||||
→
|
||||
{ "id", "properties" }
|
||||
</code></pre>
|
||||
<p>or they may ultimately design their own UI and don't use this functionality at all, defining neither layouts nor corresponding data fields.</p>
|
||||
<p>or create a layout of their own and provide the data fields it requires, or they may ultimately design their own UI and don't use this functionality at all, defining neither layouts nor corresponding data fields.</p>
|
||||
<p>Then our interface would ultimately look like this:</p>
|
||||
<pre><code>POST /v1/recipes
|
||||
{ "id" }
|
||||
→
|
||||
{ "id" }
|
||||
</code></pre>
|
||||
<p>This conclusion might look highly counter-intuitive, but lacking any fields in a “Recipe” simply tells us that this entity possesses no specific semantics of its own, and is simply an identifier of a context; a method to point out where to look for the data needed by other entities. In the real world, we should implement a builder endpoint capable of creating all the related contexts with a single request:</p>
|
||||
<p>This conclusion might look highly counter-intuitive, but lacking any fields in a <code>Recipe</code> simply tells us that this entity possesses no specific semantics of its own, and is simply an identifier of a context; a method to point out where to look for the data needed by other entities. In the real world, we should implement a builder endpoint capable of creating all the related contexts with a single request:</p>
|
||||
<pre><code>POST /v1/recipe-builder
|
||||
{
|
||||
"id",
|
||||
@ -3119,7 +3114,7 @@ PUT /formatters/volume/ru/US
|
||||
…
|
||||
}
|
||||
</code></pre>
|
||||
<p>We should also note that providing a newly created entity identifier by the requesting side isn't exactly the best pattern. However, since we decided from the very beginning to keep recipe identifiers semantically meaningful, we have to live with this convention. Obviously, we're risking getting lots of collisions on recipe names used by different partners, so we actually need to modify this operation: either the partner must always use a pair of identifiers (i.e. recipe's one plus partner's own id), or we need to introduce composite identifiers, as we recommended earlier in the <a href="#api-design-describing-interfaces">“Describing Final Interfaces”</a> chapter.</p>
|
||||
<p>We should also note that providing a newly created entity identifier by the requesting side isn't exactly the best practice. However, since we decided from the very beginning to keep recipe identifiers semantically meaningful, we have to live on with this convention. Obviously, we're risking getting lots of collisions on recipe names used by different partners, so we actually need to modify this operation: either partners must always use a pair of identifiers (i.e. the recipe id plus partner's own id), or we need to introduce composite identifiers, as we recommended earlier in the <a href="#api-design-describing-interfaces">“Describing Final Interfaces”</a> chapter.</p>
|
||||
<pre><code>POST /v1/recipes/custom
|
||||
{
|
||||
// The first part of the composite
|
||||
@ -3135,8 +3130,8 @@ PUT /formatters/volume/ru/US
|
||||
"my-coffee-company:lungo-customato"
|
||||
}
|
||||
</code></pre>
|
||||
<p>Also note that this format allows us to maintain an important extensibility point: different partners might have totally isolated namespaces or conversely share them. Furthermore, we might introduce special namespaces (like <code>common</code>, for example) to allow editing standard recipes (and thus organizing our own recipes backoffice).</p><div class="page-break"></div><h3><a href="#back-compat-weak-coupling" class="anchor" id="back-compat-weak-coupling">Chapter 17. Weak Coupling</a><a href="#chapter-17" class="secondary-anchor" id="chapter-17"> </a></h3>
|
||||
<p>In the previous chapter we've demonstrated how breaking the strong coupling of components leads to decomposing entities and collapsing their public interfaces down to a reasonable minimum. A mindful reader might have noted that this technique was already used in our API study much earlier in the <a href="#api-design-separating-abstractions">“Separating Abstraction Levels”</a> chapter with regards to the “program” and “program run” entities. Indeed, we might do it without the <code>program-matcher</code> endpoint and make it this way:</p>
|
||||
<p>Also note that this format allows us to maintain an important extensibility point: different partners might have both shared and isolated namespaces. Furthermore, we might introduce special namespaces (like <code>common</code>, for example) to allow editing standard recipes (and thus organizing our own recipes backoffice).</p>
|
||||
<p><strong>NB</strong>: a mindful reader might have noted that this technique was already used in our API study much earlier in the <a href="#api-design-separating-abstractions">“Separating Abstraction Levels”</a> chapter with regards to the “program” and “program run” entities. Indeed, we might do it without the <code>program-matcher</code> endpoint and make it this way:</p>
|
||||
<pre><code>GET /v1/recipes/{id}/run-data/{api_type}
|
||||
→
|
||||
{ /* A description, how to
|
||||
@ -3149,66 +3144,62 @@ PUT /formatters/volume/ru/US
|
||||
<li>get the execution description, as stated above;</li>
|
||||
<li>depending on the API type, run some specific commands.</li>
|
||||
</ul>
|
||||
<p>Obviously, such an interface is absolutely unacceptable, simply because in the majority of use cases developers don't care at all, which API type the specific coffee machine runs. To avoid the necessity of introducing such bad interfaces we created a new “program” entity, which constitutes merely a context identifier, just like a “recipe” entity does. A <code>program_run_id</code> entity is also organized in this manner, it also possesses no specific properties, being <em>just</em> a program run identifier.</p>
|
||||
<p>But let us return to the question we have previously mentioned in the previous chapter: how should we parametrize the order preparation process implemented via third-party API. In other words, what's this <code>program_execution_endpoint</code> that we ask upon the API type registration?</p>
|
||||
<p>Obviously, such an interface is absolutely unacceptable, simply because in the majority of use cases developers don't care at all, which API type the specific coffee machine runs. To avoid the necessity of introducing such bad interfaces we created a new “program” entity, which constitutes merely a context identifier, just like a “recipe” entity does. A <code>program_run_id</code> entity is also organized in this manner, it also possesses no specific properties, being <em>just</em> a program run identifier.</p><div class="page-break"></div><h3><a href="#back-compat-weak-coupling" class="anchor" id="back-compat-weak-coupling">Chapter 17. Weak Coupling</a><a href="#chapter-17" class="secondary-anchor" id="chapter-17"> </a></h3>
|
||||
<p>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 <a href="#back-compat-abstracting-extending">“Extending through Abstracting”</a> chapter: how should we parametrize the order preparation process implemented via a third-party API? In other words, what <em>is</em> this <code>order_execution_endpoint</code> required in the API type registration endpoint?</p>
|
||||
<pre><code>PUT /v1/api-types/{api_type}
|
||||
{
|
||||
"order_execution_endpoint": {
|
||||
// ???
|
||||
}
|
||||
…
|
||||
"order_execution_endpoint": {
|
||||
// ???
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
<p>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:</p>
|
||||
<pre><code>// This is an endpoint for partners
|
||||
// to register their coffee machines
|
||||
// in the system
|
||||
PUT /partners/{id}/coffee-machines
|
||||
<pre><code>PUT /v1/api-types/{api_type}
|
||||
{
|
||||
"coffee-machines": [{
|
||||
"id",
|
||||
…
|
||||
"order_execution_endpoint": {
|
||||
"program_run_endpoint": {
|
||||
/* Some description of
|
||||
the remote function call */
|
||||
"type": "rpc",
|
||||
"endpoint": <URL>,
|
||||
"format"
|
||||
},
|
||||
"program_state_endpoint",
|
||||
"program_cancel_endpoint"
|
||||
}
|
||||
}, …]
|
||||
…
|
||||
"order_execution_endpoint": {
|
||||
"program_run_endpoint": {
|
||||
/* Some description of
|
||||
the remote function call */
|
||||
"type": "rpc",
|
||||
"endpoint": <URL>,
|
||||
"parameters"
|
||||
},
|
||||
"program_get_state_endpoint",
|
||||
"program_cancel_endpoint"
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
<p><strong>NB</strong>: doing so we're transferring the complexity of developing the API onto a plane of developing appropriate data formats, e.g. how exactly would we send order parameters to the <code>program_run_endpoint</code>, and what format the <code>program_state_endpoint</code> shall return, etc., but in this chapter, we're focusing on different questions.</p>
|
||||
<p>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.</p>
|
||||
<p><strong>NB</strong>: doing so we're transferring the complexity of developing the API onto a plane of developing appropriate data formats, e.g. how exactly would we send order parameters to the <code>program_run_endpoint</code>, and what format the <code>program_get_state_endpoint</code> shall return, etc., but in this chapter, we're focusing on different questions.</p>
|
||||
<p>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:</p>
|
||||
<ol>
|
||||
<li>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 in the approach. In fact, we simply described what we'd already learned, not even trying to look at a larger picture.</li>
|
||||
<li>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.</li>
|
||||
<li>This design is ultimately based on a single principle: every order preparation might be codified with these three imperative commands.</li>
|
||||
</ol>
|
||||
<p>We may easily disprove the #2 principle, 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, ask for a cinnamon sprinkling or contactless takeout. That would lead us to creating a new endpoint, let's say, <code>program_modify_endpoint</code>, and new difficulties in data format development (we need to understand in the real-time, could we actually sprinkle cinnamon on this specific cup of coffee or not). What <em>is</em> important is that both endpoint and new data fields would be optional because of backwards compatibility requirement.</p>
|
||||
<p>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 via our API not a coffee house, but a vending machine? From one side, it means that the <code>modify</code> endpoint and all related stuff are simply meaningless: a vending machine couldn't sprinkle cinnamon over a coffee cup, and the contactless takeout requirement means nothing to it. On the other side, the machine, unlike the people-operated café, requires <em>takeout approval</em>: the end-user places an order 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 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.</p>
|
||||
<p>Programmable takeout approval requires one more endpoint, let's say, <code>program_takeout_endpoint</code>. And so we've lost our way in a forest of three endpoints:</p>
|
||||
<p>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 creating a new endpoint, let's say, <code>program_modify_endpoint</code>, and new difficulties in data format development (as new fields for contactless delivery requested and satisfied flags need to be passed both directions). What <em>is</em> important is that both the endpoint and the new data fields would be optional because of backwards compatibility requirement.</p>
|
||||
<p>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 via our API not a coffee house, but a vending machine? From one side, it means that the <code>modify</code> 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 <em>takeout approval</em>: 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.</p>
|
||||
<p>Programmable takeout approval requires one more endpoint, let's say, <code>program_takeout_endpoint</code>. And so we've lost our way in a forest of five endpoints:</p>
|
||||
<ul>
|
||||
<li>to have vending machines integrated a partner must implement the <code>program_takeout_endpoint</code>, but doesn't actually need the <code>program_modify_endpoint</code>;</li>
|
||||
<li>to have regular coffee houses integrated a partner must implement the <code>program_modify_endpoint</code>, but doesn't actually need the <code>program_takeout_endpoint</code>.</li>
|
||||
<li>to have vending machines integrated a partner must implement the <code>program_takeout_endpoint</code>, but doesn't need the <code>program_modify_endpoint</code>;</li>
|
||||
<li>to have regular coffee houses integrated a partner must implement the <code>program_modify_endpoint</code>, but doesn't need the <code>program_takeout_endpoint</code>.</li>
|
||||
</ul>
|
||||
<p>Furthermore, we have to describe both endpoints in the docs. It's quite natural that the <code>takeout</code> endpoint is very specific; unlike cinnamon sprinkling, which we hid under the pretty general <code>modify</code> 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.</p>
|
||||
<p>We actually don't know, whether in the real world of coffee machine APIs this problem will really occur or not. But we can say with all confidence regarding “bare metal” integrations that the processes we described <em>always</em> 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 become automated, we will finally end up with the situation with half of the methods <em>isn't actually needed at all</em>, like the requesting a contactless takeout one.</p>
|
||||
<p>Furthermore, we have to describe both endpoints in the docs. It's quite natural that the <code>takeout</code> endpoint is very specific; unlike requesting contactless delivery, which we hid under the pretty general <code>modify</code> 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.</p>
|
||||
<p><strong>NB</strong>: in this example, we assumed that passing <code>program_takeout_endpoint</code> parameter is the flag to the application to display the “get the order” button; it would be better to add something like a <code>supported_flow</code> field to the <code>PUT /api-types/</code> 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.</p>
|
||||
<p>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 <em>always</em> 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 <em>aren't actually needed at all</em>, like the requesting a contactless takeout one.</p>
|
||||
<p>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.</p>
|
||||
<p>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 <em>ideal</em> solution look from both sides? Something like this:</p>
|
||||
<p>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 <em>ideal</em> solution look to both parties? Something like this:</p>
|
||||
<ul>
|
||||
<li>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, sprinkle with cinnamon, allow this user to take it;</li>
|
||||
<li>the underlying program execution API level doesn't care what other same-level implementations exist; it just interprets those parts of the task which make sense to it.</li>
|
||||
<li>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 user to collect their order;</li>
|
||||
<li>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.</li>
|
||||
</ul>
|
||||
<p>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 <em>informational contexts</em> 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 <a href="#api-design-separating-abstractions">“Separating Abstraction Levels”</a> chapter.</p>
|
||||
<p>In our case we need to implement the following mechanisms:</p>
|
||||
<ul>
|
||||
<li>running a program creates a corresponding context comprising all the essential parameters;</li>
|
||||
<li>there is a method to stream the information regarding the state modifications: the execution level may read the context, learn about all the changes and report back the changes of its own.</li>
|
||||
<li>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.</li>
|
||||
</ul>
|
||||
<p>There are different techniques to organize this data flow, but, basically, we always have two context descriptions and a two-way event stream in-between. If we were developing an SDK we would express the idea like this:</p>
|
||||
<p>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 listening events, like this:</p>
|
||||
<pre><code>/* Partner's implementation of the program
|
||||
run procedure for a custom API type */
|
||||
registerProgramRunHandler(
|
||||
@ -3223,36 +3214,41 @@ registerProgramRunHandler(
|
||||
() => {
|
||||
// If takeout is requested, initiate
|
||||
// corresponding procedures
|
||||
execution.prepareTakeout(() => {
|
||||
// When the cup is ready for takeout,
|
||||
// emit corresponding event for
|
||||
// a higher-level entity to catch it
|
||||
execution.context
|
||||
.emit('takeout_ready');
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
return execution.context;
|
||||
});
|
||||
await execution.prepareTakeout();
|
||||
// When the cup is ready for takeout,
|
||||
// emit corresponding event for
|
||||
// a higher-level entity to catch it
|
||||
execution.context.emit('takeout_ready');
|
||||
}
|
||||
);
|
||||
program.context.on(
|
||||
'order_canceled',
|
||||
() => {
|
||||
await execution.cancel();
|
||||
execution.context.emit('canceled');
|
||||
}
|
||||
);
|
||||
|
||||
return execution.context;
|
||||
}
|
||||
);
|
||||
</code></pre>
|
||||
<p><strong>NB</strong>: In the case of HTTP API corresponding example would look rather bulky as it involves implementing several additional endpoints for message queues like <code>GET /program-run/events</code> and <code>GET /partner/{id}/execution/events</code>. We would leave this exercise to the reader. Also worth mentioning that in real-world systems such event queues are usually organized using external event message systems like Apache Kafka or Amazon SNS/SQS.</p>
|
||||
<p><strong>NB</strong>: In the case of HTTP API corresponding example would look rather bulky as it involves implementing several additional endpoints for message exchange like <code>GET /program-run/events</code> and <code>GET /partner/{id}/execution/events</code>. 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.</p>
|
||||
<p>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:</p>
|
||||
<ul>
|
||||
<li>instead of calling the <code>takeout</code> method, we're now generating a pair of <code>takeout_requested</code>/<code>takeout_ready</code> events;</li>
|
||||
<li>instead of a long list of methods that shall be implemented to integrate the partner's API, we now have a long list of <code>context</code> objects fields and events they generate;</li>
|
||||
<li>instead of calling the <code>takeout</code> method, we're now generating a pair of <code>takeout_requested</code> / <code>takeout_ready</code> events;</li>
|
||||
<li>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;</li>
|
||||
<li>and with regards to technological progress, we've changed nothing: now we have deprecated fields and events instead of deprecated methods.</li>
|
||||
</ul>
|
||||
<p>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 solves another problem: how to make the code written by developers stay clean and maintainable. Why would strong-coupled integration (i.e. coupling entities via methods) render the code unreadable? Because both sides <em>are obliged</em> to implement the functionality which is meaningless in their corresponding subject areas. And these implementations would actually comprise a handful of methods to say that this functionality is either not supported at all, or supported always and unconditionally.</p>
|
||||
<p>The difference between strong coupling and weak coupling is that the field-event mechanism <em>isn't obligatory to both sides</em>. Let us remember what we sought to achieve:</p>
|
||||
<p>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 <em>are obliged</em> to implement the functionality which is meaningless in their corresponding subject areas. Code that integrates vending machines into the system <em>must</em> respond “ok” to the contactless delivery request — so after a while, these implementations would comprise a handful of methods that just always return <code>true</code> (or <code>false</code>).</p>
|
||||
<p>The difference between strong coupling and weak coupling is that the field-event mechanism <em>isn't obligatory to both actors</em>. Let us remember what we sought to achieve:</p>
|
||||
<ul>
|
||||
<li>a higher-level context doesn't actually 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;</li>
|
||||
<li>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 actually happen under its specific conditions.</li>
|
||||
<li>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;</li>
|
||||
<li>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.</li>
|
||||
</ul>
|
||||
<p>It's ultimately possible that both sides would know nothing about each other and wouldn't interact at all. This might actually happen at some point in the future with the evolution of underlying technologies.</p>
|
||||
<p>Worth mentioning that the number of entities (fields, events), though effectively doubled compared to strong-coupled API design, increased qualitatively, not quantitatively. The <code>program</code> context describes fields and events in its own terms (type of beverage, volume, cinnamon sprinkling), while the <code>execution</code> context must reformulate those terms according to its own subject area (omitting redundant ones, by the way). It is also important that the <code>execution</code> context might concretize these properties for underlying objects according to their own specifics, while the <code>program</code> context must keep its properties general enough to be applicable to any possible underlying technology.</p>
|
||||
<p>One more important feature of weak coupling is that it allows an entity to have several higher-level contexts. In typical subject areas, such a situation would look like an API design flaw, but in complex systems, with several system state-modifying agents present, such design patterns are not that rare. Specifically, you would likely face it while developing user-facing UI libraries. We will cover this issue in detail in the upcoming “SDK” section of this book.</p>
|
||||
<p>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.</p>
|
||||
<p><strong>NB</strong>: in the real world this might not be the case, e.g. we might <em>want</em> the application to know, whether the takeout request was successfully served or not, e.g. listen to the <code>takeout_ready</code> event and require the <code>takeout_ready</code> flag in the state of the execution context. Still, the general possibility of <em>not caring</em> about the implementation details is a very powerful technique that makes the application code much less complex — of course, unless this knowledge is important to the user.</p>
|
||||
<p>One more important feature of weak coupling is that it allows an entity to have several higher-level contexts. In typical subject areas, such a situation would look like an API design flaw, but in complex systems, with several system state-modifying agents present, such design patterns are not that rare. Specifically, you would likely face it while developing user-facing UI libraries. We will cover this issue in detail in the “SDK and UI Libraries” section of this book.</p>
|
||||
<h4>The Inversion of Responsibility</h4>
|
||||
<p>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:</p>
|
||||
<pre><code>/* Partner's implementation of program
|
||||
@ -3269,51 +3265,59 @@ registerProgramRunHandler(
|
||||
() => {
|
||||
// If takeout is requested, initiate
|
||||
// corresponding procedures
|
||||
execution.prepareTakeout(() => {
|
||||
/* When the order is ready
|
||||
for takeout, signalize about that
|
||||
by calling a method, not
|
||||
with event emitting */
|
||||
// execution.context
|
||||
// .emit('takeout_ready')
|
||||
program.context
|
||||
.set('takeout_ready');
|
||||
// Or even more rigidly
|
||||
// program.setTakeoutReady();
|
||||
}
|
||||
);
|
||||
});
|
||||
await execution.prepareTakeout();
|
||||
/* When the order is ready
|
||||
for takeout, signalize about that
|
||||
by calling the parent context
|
||||
method, not with event emitting */
|
||||
// execution.context
|
||||
// .emit('takeout_ready')
|
||||
program.context
|
||||
.set('takeout_ready');
|
||||
// Or even more rigidly
|
||||
// program.setTakeoutReady();
|
||||
}
|
||||
);
|
||||
/* Since we're modifying parent context
|
||||
instead of emitting events, we don't
|
||||
actually need to return anything */
|
||||
instead of emitting events, we don't
|
||||
actually need to return anything */
|
||||
// return execution.context;
|
||||
}
|
||||
);
|
||||
</code></pre>
|
||||
<p>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 making all this stuff up because we expect alternative implementations of the <em>lower</em> abstraction level. Situations with different realizations of <em>higher</em> abstraction levels emerging are, of course, possible, but quite rare. The tree of alternative implementations usually grows from top to bottom.</p>
|
||||
<p>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 <em>lower</em> abstraction level to pop up. Situations with different realizations of <em>higher</em> abstraction levels emerging are, of course, possible, but quite rare. The tree of alternative implementations usually grows from root to leaves.</p>
|
||||
<p>Another reason to justify this solution is that major changes occurring at different abstraction levels have different weights:</p>
|
||||
<ul>
|
||||
<li>if the technical level is under change, that must not affect product qualities and the code written by partners;</li>
|
||||
<li>if the product is changing, i.e. we start selling flight tickets instead of preparing coffee, there is literally no sense to preserve backwards compatibility at technical abstraction levels. Ironically, we may actually make our program run API sell tickets instead of brewing coffee without breaking backwards compatibility, but the partners' code will still become obsolete.</li>
|
||||
<li>if the product is changing, i.e. we start selling flight tickets instead of preparing coffee, there is literally no sense to preserve backwards compatibility at technical abstraction levels. Ironically, we may actually make our API sell tickets instead of brewing coffee without breaking backwards compatibility, but the partners' code will still become obsolete.</li>
|
||||
</ul>
|
||||
<p>In conclusion, because of the abovementioned reasons, higher-level APIs are evolving more slowly and much more consistently than low-level APIs, which means that reverse strong coupling might often be acceptable or even desirable, at least from the price-quality ratio point of view.</p>
|
||||
<p>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.</p>
|
||||
<p><strong>NB</strong>: many contemporary frameworks explore a shared state approach, Redux being probably the most notable example. In the Redux paradigm, the code above would look like this:</p>
|
||||
<pre><code>execution.prepareTakeout(() => {
|
||||
// Instead of generating events
|
||||
// or calling higher-level methods,
|
||||
// an `execution` entity calls
|
||||
// a global or quasi-global
|
||||
// callback to change a global state
|
||||
dispatch(takeoutReady());
|
||||
});
|
||||
<pre><code>program.context.on(
|
||||
'takeout_requested',
|
||||
() => {
|
||||
await execution.prepareTakeout();
|
||||
// Instead of generating events
|
||||
// or calling higher-level methods,
|
||||
// an `execution` entity calls
|
||||
// a global or quasi-global `dispatch`
|
||||
// callback to change a global state
|
||||
dispatch(takeoutReady());
|
||||
}
|
||||
);
|
||||
</code></pre>
|
||||
<p>Let us note that this approach <em>in general</em> doesn't contradict the weak coupling principle, but violates another one — of abstraction levels isolation, and therefore isn't suitable 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 interacting with its closest higher-level neighbors only, delegating the responsibility of calling high-level or global methods to them.</p>
|
||||
<pre><code>execution.prepareTakeout(() => {
|
||||
// Instead of initiating global actions
|
||||
// an `execution` entity invokes
|
||||
// its superior's dispatch functionality
|
||||
program.context.dispatch(takeoutReady());
|
||||
});
|
||||
<p>Let us note that this approach <em>in general</em> doesn't contradict the weak coupling principle, but violates another one — of abstraction levels isolation, and therefore isn't suitable 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.</p>
|
||||
<pre><code>program.context.on(
|
||||
'takeout_requested',
|
||||
() => {
|
||||
await execution.prepareTakeout();
|
||||
// Instead of calling the global
|
||||
// `dispatch` method, an `execution`
|
||||
// entity invokes its superior's
|
||||
// dispatch functionality
|
||||
program.context.dispatch(takeoutReady());
|
||||
}
|
||||
);
|
||||
</code></pre>
|
||||
<pre><code>// program.context.dispatch implementation
|
||||
ProgramContext.dispatch = (action) => {
|
||||
@ -3328,38 +3332,6 @@ ProgramContext.dispatch = (action) => {
|
||||
);
|
||||
}
|
||||
</code></pre>
|
||||
<h4>Test Yourself</h4>
|
||||
<p>So, we have designed the interaction with third-party APIs as described in the previous paragraph. And now we should (actually, must) check whether these interfaces are compatible with our own abstraction we had developed in the <a href="#api-design-separating-abstractions">“Separating Abstraction Levels”</a> chapter. In other words, could we start order execution if we operate the low-level API instead of the high-level one?</p>
|
||||
<p>Let us recall that we had proposed the following abstract interfaces to work with arbitrary coffee machine API types:</p>
|
||||
<ul>
|
||||
<li><code>POST /v1/program-matcher</code> returns the id of the program based on the coffee machine and recipe ids;</li>
|
||||
<li><code>POST /v1/programs/{id}/run</code> executes the program.</li>
|
||||
</ul>
|
||||
<p>As we can easily prove, it's quite simple to make these interfaces compatible: we only need to assign a <code>program_id</code> identifier to the (API type, recipe) pair, for example, through returning it in the <code>PUT /coffee-machines</code> method response:</p>
|
||||
<pre><code>PUT /v1/partners/{partnerId}/coffee-machines
|
||||
{
|
||||
"coffee_machines": [{
|
||||
"id",
|
||||
"api_type",
|
||||
"location",
|
||||
"supported_recipes"
|
||||
}, …]
|
||||
}
|
||||
→
|
||||
{
|
||||
"coffee_machines": [{
|
||||
"id",
|
||||
"recipes_programs": [
|
||||
{"recipe_id", "program_id"},
|
||||
…
|
||||
]
|
||||
}, …]
|
||||
}
|
||||
</code></pre>
|
||||
<p>So the method we'd developed:</p>
|
||||
<pre><code>POST /v1/programs/{id}/run
|
||||
</code></pre>
|
||||
<p>will work with the partner's coffee machines (like it's a third API type).</p>
|
||||
<h4>Delegate!</h4>
|
||||
<p>From what was said, one more important conclusion follows: doing a real job, e.g. 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 <code>order_execution_endpoint</code> 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.</p>
|
||||
<p>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. Worth mentioning that the abstraction deepness for different underlying platforms might vary.</p>
|
||||
|
@ -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* this `order_execution_endpoint` required in the API type registration endpoint?
|
||||
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?
|
||||
|
||||
```
|
||||
PUT /v1/api-types/{api_type}
|
||||
@ -32,7 +32,7 @@ PUT /v1/api-types/{api_type}
|
||||
}
|
||||
```
|
||||
|
||||
**NB**: doing so we're transferring the complexity of developing the API onto a plane of developing appropriate data formats, e.g. how exactly would we send 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, e.g. 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.
|
||||
|
||||
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:
|
||||
|
||||
@ -41,7 +41,7 @@ Though this API looks absolutely universal, it's quite easy to demonstrate how o
|
||||
|
||||
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 creating 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 backwards 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 via our API not a coffee house, but a vending machine? 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 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.
|
||||
|
||||
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`;
|
||||
@ -65,7 +65,7 @@ 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 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 listening events, like this:
|
||||
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:
|
||||
|
||||
```
|
||||
/* Partner's implementation of the program
|
||||
@ -76,11 +76,11 @@ registerProgramRunHandler(
|
||||
// Initiating an execution
|
||||
// on partner's side
|
||||
let execution = initExecution(…);
|
||||
// Listen to parent context's changes
|
||||
// Listen to parent context changes
|
||||
program.context.on(
|
||||
'takeout_requested',
|
||||
() => {
|
||||
// If takeout is requested, initiate
|
||||
// If a takeout is requested, initiate
|
||||
// corresponding procedures
|
||||
await execution.prepareTakeout();
|
||||
// When the cup is ready for takeout,
|
||||
@ -102,7 +102,7 @@ registerProgramRunHandler(
|
||||
);
|
||||
```
|
||||
|
||||
**NB**: In the case of HTTP API corresponding example would look rather bulky as it involves implementing several additional endpoints for 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 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.
|
||||
|
||||
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;
|
||||
@ -134,11 +134,11 @@ registerProgramRunHandler(
|
||||
// Initiating an execution
|
||||
// on partner's side
|
||||
let execution = initExecution(…);
|
||||
// Listen to parent context's changes
|
||||
// Listen to parent context changes
|
||||
program.context.on(
|
||||
'takeout_requested',
|
||||
() => {
|
||||
// If takeout is requested, initiate
|
||||
// If a takeout is requested, initiate
|
||||
// corresponding procedures
|
||||
await execution.prepareTakeout();
|
||||
/* When the order is ready
|
||||
@ -186,7 +186,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 suitable 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 — 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, e.g. 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(
|
||||
@ -221,6 +221,6 @@ ProgramContext.dispatch = (action) => {
|
||||
|
||||
From what was said, one more important conclusion follows: doing a real job, e.g. 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.
|
||||
|
||||
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. Worth mentioning that the abstraction deepness for different underlying platforms might vary.
|
||||
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.
|
||||
|
||||
**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.
|
@ -1,14 +1,14 @@
|
||||
### [Interfaces as a Universal Pattern][back-compat-universal-interfaces]
|
||||
|
||||
Let us summarize what we have written in the three previous chapters.
|
||||
Let us summarize what we have written in the three previous chapters:
|
||||
|
||||
1. Extending API functionality is realized through abstracting: the entity nomenclature is to be reinterpreted so that existing methods become partial (ideally — the most frequent) simplified cases to more general functionality.
|
||||
1. Extending API functionality is implemented through abstracting: the entity nomenclature is to be reinterpreted so that existing methods become partial (ideally — the most frequent) simplified cases to more general functionality.
|
||||
2. Higher-level entities are to be the informational contexts for low-level ones, e.g. don't prescribe any specific behavior but translate their state and expose functionality to modify it (directly through calling some methods or indirectly through firing events).
|
||||
3. Concrete functionality, e.g. working with “bare metal” hardware or underlying platform APIs, should be delegated to low-level entities.
|
||||
|
||||
**NB**. There is nothing novel about these rules: one might easily recognize them being the [SOLID](https://en.wikipedia.org/wiki/SOLID) architecture principles. There is no surprise in that either, because SOLID concentrates on contract-oriented development, and APIs are contracts by definition. We've just added “abstraction levels” and “informational contexts” concepts there.
|
||||
**NB**. There is nothing novel about these rules: one might easily recognize them being the [SOLID](https://en.wikipedia.org/wiki/SOLID) architecture principles. There is no surprise in that either, because SOLID concentrates on contract-oriented development, and APIs are contracts by definition. We've just added the “abstraction levels” and “informational contexts” concepts there.
|
||||
|
||||
However, there is an unanswered question: how should we design the entity nomenclature from the beginning so that extending the API won't make it a mess of different inconsistent methods of different ages. The answer is pretty obvious: to avoid clumsy situations while abstracting (as with the coffee machine's supported options), all the entities must be originally considered being a specific implementation of a more general interface, even if there are no planned alternative implementations for them.
|
||||
However, there is an unanswered question: how should we design the entity nomenclature from the beginning so that extending the API won't make it a mess of different inconsistent methods of different ages. The answer is pretty obvious: to avoid clumsy situations while abstracting (as with the recipe properties), all the entities must be originally considered being a specific implementation of a more general interface, even if there are no planned alternative implementations for them.
|
||||
|
||||
For example, we should have asked ourselves a question while designing the `POST /search` API: what is a “search result”? What abstract interface does it implement? To answer this question we must neatly decompose this entity to find which facet of it is used for interacting with which objects.
|
||||
|
||||
@ -19,12 +19,12 @@ Then we would have come to the understanding that a “search result” is actua
|
||||
|
||||
or we can encode this data in the single `offer_id`;
|
||||
|
||||
* to have this search result displayed in the app, we need a different data set: `name`, `description`, formatted and localized price.
|
||||
* to have this search result displayed in the app, we need a different data set: `name`, `description`, and formatted and localized prices.
|
||||
|
||||
So our interface (let us call it `ISearchResult`) is actually a composition of two other interfaces: `IOrderParameters` (an entity that allows for creating an order) and `ISearchItemViewParameters` (some abstract representation of the search result in the UI). This interface split should automatically lead us to additional questions.
|
||||
So our interface (let us call it `ISearchResult`) is actually a composition of two other interfaces: `IOrderParameters` (an entity that allows for creating an order) and `ISearchItemViewParameters` (some abstract representation of the search result in the UI). This interface split should automatically lead us to additional questions:
|
||||
|
||||
1. How will we couple the former and the latter? Obviously, these two sub-interfaces are related: the machine-readable price must match the human-readable one, for example. This will naturally lead us to the “formatter” concept described in the [“Strong Coupling and Related Problems”](#back-compat-strong-coupling) chapter.
|
||||
|
||||
2. And what is the “abstract representation of the search result in the UI”? Do we have other kinds of search, should the `ISearchItemViewParameters` interface be a subtype of some even more general interface, or maybe a composition of several such ones?
|
||||
|
||||
Replacing specific implementations with interfaces not only allows us to answer more clearly many questions which should have popped out in the API design phase but also helps us to outline many possible API evolution vectors, which should help in avoiding API inconsistency problems in the future.
|
||||
Replacing specific implementations with interfaces not only allows us to respond more clearly to many concerns that pop up during the API design phase but also helps us to outline many possible API evolution directions, which should help us in avoiding API inconsistency problems in the future.
|
@ -1,22 +1,22 @@
|
||||
### [The Serenity Notepad][back-compat-serenity-notepad]
|
||||
|
||||
Apart from the abovementioned abstract principles, let us give a list of concrete recommendations: how to make changes in the existing API to maintain the backwards compatibility.
|
||||
Apart from the abovementioned abstract principles, let us give a list of concrete recommendations: how to make changes in existing APIs to maintain backwards compatibility.
|
||||
|
||||
##### Remember the iceberg's waterline
|
||||
|
||||
If you haven't given any formal guarantee, it doesn't mean that you can violate informal once. Often, even just fixing bugs in APIs might make some developers' code inoperable. We might illustrate it with a real-life example that the author of this book has actually faced once:
|
||||
If you haven't given any formal guarantee, it doesn't mean that you can violate informal ones. Often, just fixing bugs in APIs might render some developers' code inoperable. We might illustrate it with a real-life example that the author of this book has actually faced once:
|
||||
* there was an API to place a button into a visual container; according to the docs, it was taking its position (offsets to the container's corner) as a mandatory argument;
|
||||
* in reality, there was a bug: if the position was not supplied, no exception was thrown; buttons were simply stacked in the corner one after another;
|
||||
* after the error was fixed, we got a bunch of complaints: clients did really use this flaw to stack the buttons in the container's corner.
|
||||
* after the error had been fixed, we got a bunch of complaints: clients did really use this flaw to stack the buttons in the container's corner.
|
||||
|
||||
If fixing the error might somehow affect real customers, you have no other choice but to emulate this erroneous behavior until the next major release. This situation is quite common if you develop a large API with a huge audience. For example, operating systems API developers literally have to transfer old bugs to new OS versions.
|
||||
If fixing an error might somehow affect real customers, you have no other choice but to emulate this erroneous behavior until the next major release. This situation is quite common if you develop a large API with a huge audience. For example, operating systems developers literally have to transfer old bugs to new OS versions.
|
||||
|
||||
##### Test the formal interface
|
||||
|
||||
Any software must be tested, and APIs ain't an exclusion. However, there are some subtleties there: as APIs provide formal interfaces, it's the formal interfaces that are needed to be tested. That leads to several kinds of mistakes:
|
||||
|
||||
1. Often the requirements like “the `getEntity` function returns the value previously being set by the `setEntity` function” appear to be too trivial to both developers and QA engineers to have a proper test. But it's quite possible to make a mistake there, and we have actually encountered such bugs several times.
|
||||
2. The interface abstraction principle must be tested either. In theory, you might have considered each entity as an implementation of some interface; in practice, it might happen that you have forgotten something, and alternative implementations aren't actually possible. For testing purposes, it's highly desirable to have an alternative realization, even a provisional one.
|
||||
2. The interface abstraction principle must be tested either. In theory, you might have considered each entity as an implementation of some interface; in practice, it might happen that you have forgotten something and alternative implementations aren't actually possible. For testing purposes, it's highly desirable to have an alternative realization, even a provisional one, for every interface.
|
||||
|
||||
##### Isolate the dependencies
|
||||
|
||||
@ -26,24 +26,24 @@ In the case of a gateway API that provides access to some underlying API or aggr
|
||||
|
||||
The best practice is quite the opposite: isolate the third-party API usage, e.g. develop an abstraction level that will allow for:
|
||||
* keeping backwards compatibility intact because of extension capabilities incorporated in the API design;
|
||||
* negating partner's problems by the technical means:
|
||||
* limiting the partner's API usage in case of an unpredicted surge in your API usage;
|
||||
* negating partner's problems by technical means:
|
||||
* limiting the partner's API usage in case of load surges;
|
||||
* implementing the retry policies or other methods of recovering after failures;
|
||||
* caching some data and states to have the ability to provide some (at least partial) functionality even if the partner's API is fully unreachable;
|
||||
* finally, configuring an automatical fallback to another partner or alternative API.
|
||||
* finally, configuring an automatic fallback to another partner or alternative API.
|
||||
|
||||
##### Implement your API functionality atop of public interfaces
|
||||
##### Implement your API functionality atop public interfaces
|
||||
|
||||
There is an antipattern that occurs frequently: API developers use some internal closed implementations of some methods which exist in the public API. It happens because of two reasons:
|
||||
* often the public API is just an addition to the existing specialized software, and the functionality, exposed via the API, isn't being ported back to the closed part of the project, or the public API developers simply don't know the corresponding internal functionality exists;
|
||||
* on a course of extending the API, some interfaces become abstract, but the existing functionality isn't affected; imagine that while implementing the `PUT /formatters` interface described in the the [“Strong Coupling and Related Problems”](#back-compat-strong-coupling) chapter developers have created a new, more general version of the volume formatter but hasn't changed the implementation of the existing one, so it continues working in case of pre-existing languages.
|
||||
* in the course of extending the API, some interfaces become abstract, but the existing functionality isn't affected; imagine that while implementing the `PUT /formatters` interface described in the [“Strong Coupling and Related Problems”](#back-compat-strong-coupling) chapter API developers have created a new, more general version of the volume formatter but hasn't changed the implementation of the existing one, so it continues working for pre-existing languages.
|
||||
|
||||
There are obvious local problems with this approach (like the inconsistency in functions' behavior, or the bugs which were not found while testing the code), but also a bigger one: your API might be simply unusable if a developer tries any non-mainstream approach, because of performance issues, bugs, instability, etc.
|
||||
There are obvious local problems with this approach (like the inconsistency in functions' behavior, or the bugs which were not found while testing the code), but also a bigger one: your API might be simply unusable if a developer tries any non-mainstream approach, because of performance issues, bugs, instability, etc., as the API developers themselves never tried to use this public interface for anything important.
|
||||
|
||||
**NB**. The perfect example of avoiding this anti-pattern is compiler development; usually, the next compiler's version is compiled with the previous compiler's version.
|
||||
**NB**. The perfect example of avoiding this anti-pattern is the development of compilers; usually, the next compiler's version is compiled with the previous compiler's version.
|
||||
|
||||
##### Keep a notepad
|
||||
|
||||
Whatever tips and tricks that were described in the previous chapters you use, it's often quite probable that you can't do *anything* to prevent the API inconsistencies start piling up. It's possible to reduce the speed of this stockpiling, foresee some problems, and have some interface durability reserved for future use. But one can't foresee *everything*. At this stage, many developers tend to make some rash decisions, e.g. releasing a backwards-incompatible minor version to fix some design flaws.
|
||||
Whatever tips and tricks described in the previous chapters you use, it's often quite probable that you can't do *anything* to prevent API inconsistencies from piling up. It's possible to reduce the speed of this stockpiling, foresee some problems, and have some interface durability reserved for future use. But one can't foresee *everything*. At this stage, many developers tend to make some rash decisions, e.g. releasing a backwards-incompatible minor version to fix some design flaws.
|
||||
|
||||
We highly recommend never doing that. Remember that the API is a multiplier of your mistakes either. What we recommend is to keep a serenity notepad — to fix the lessons learned, and not to forget to apply this knowledge when the major API version is released.
|
||||
We highly recommend never doing that. Remember that the API is also a multiplier of your mistakes. What we recommend is to keep a serenity notepad — to write down the lessons learned, and not to forget to apply this knowledge when a new major API version is released.
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
**NB**. В этих правилах нет ничего особенно нового: в них легко опознаются принципы архитектуры [SOLID](https://en.wikipedia.org/wiki/SOLID) — что неудивительно, поскольку SOLID концентрируется на контрактно-ориентированном подходе к разработке, а API по определению и есть контракт. Мы лишь добавляем в эти принципы понятие уровней абстракции и информационных контекстов.
|
||||
|
||||
Остаётся, однако, неотвеченным вопрос о том, как изначально выстроить номенклатуру сущностей таким образом, чтобы расширение API не превращало её в мешанину из различных неконсистентных методов разных эпох. Впрочем, ответ на него довольно очевиден: чтобы при абстрагировании не возникало неловких ситуаций, подобно рассмотренному нами примеру с поддерживаемыми кофемашиной опциями, все сущности необходимо *изначально* рассматривать как частную реализацию некоторого более общего интерфейса, даже если никаких альтернативных реализаций в настоящий момент не предвидится.
|
||||
Остаётся, однако, неотвеченным вопрос о том, как изначально выстроить номенклатуру сущностей таким образом, чтобы расширение API не превращало её в мешанину из различных неконсистентных методов разных эпох. Впрочем, ответ на него довольно очевиден: чтобы при абстрагировании не возникало неловких ситуаций, подобно рассмотренному нами примеру с полями рецептов, все сущности необходимо *изначально* рассматривать как частную реализацию некоторого более общего интерфейса, даже если никаких альтернативных реализаций в настоящий момент не предвидится.
|
||||
|
||||
Например, разрабатывая API эндпойнта `POST /search` мы должны были задать себе вопрос: а «результат поиска» — это абстракция над каким интерфейсом? Для этого нам нужно аккуратно декомпозировать эту сущность, чтобы понять, каким своим срезом она выступает во взаимодействии с какими объектами.
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user