mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-01-05 10:20:22 +02:00
fresh build
This commit is contained in:
parent
60c3ac6d88
commit
33005ae102
BIN
docs/API.en.epub
BIN
docs/API.en.epub
Binary file not shown.
128
docs/API.en.html
128
docs/API.en.html
@ -522,7 +522,7 @@ Cache-Control: no-cache
|
||||
<p>Back to our coffee example. What entity abstraction levels do we see?</p>
|
||||
<ol>
|
||||
<li>We're preparing an <code>order</code> via the API: one (or more) cups of coffee, and receive payments for this.</li>
|
||||
<li>Each cup of coffee is being prepared according to some <code>recipe</code>, which implies the presence of different ingredients and sequences of preparation steps.</li>
|
||||
<li>Each cup of coffee is prepared according to some <code>recipe</code>, which implies the presence of different ingredients and sequences of preparation steps.</li>
|
||||
<li>Each beverage is being prepared on some physical <code>coffee machine</code>, occupying some position in space.</li>
|
||||
</ol>
|
||||
<p>Every level presents a developer-facing ‘facet’ in our API. While elaborating on the hierarchy of abstractions, we are first of all trying to reduce the interconnectivity of different entities. That would help us to reach several goals.</p>
|
||||
@ -563,7 +563,7 @@ GET /v1/orders/{id}
|
||||
<p><strong>First</strong>, to solve the task ‘order a lungo’ a developer needs to refer to the ‘recipe’ entity and learn that every recipe has an associated volume. Then they need to embrace the concept that an order is ready at that particular moment when the prepared beverage volume becomes equal to the reference one. This concept is simply unguessable, and knowing it is mostly useless.</p>
|
||||
<p><strong>Second</strong>, 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.</p>
|
||||
<p>Variant I: we have a list of possible volumes fixed and introduce bogus recipes like <code>/recipes/small-lungo</code> or <code>recipes/large-lungo</code>. 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.</p>
|
||||
<p>Variant II: we modify an interface, pronouncing volumes stated in recipes being just the default values. We allow to request different cup volumes while placing an order:</p>
|
||||
<p>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:</p>
|
||||
<pre><code>POST /v1/orders
|
||||
{
|
||||
"coffee_machine_id",
|
||||
@ -658,7 +658,7 @@ POST /cancel
|
||||
// The format is the same as in `POST /execute`
|
||||
GET /execution/status
|
||||
</code></pre>
|
||||
<p><strong>NB</strong>. 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 quite a sane one, actually.</p>
|
||||
<p><strong>NB</strong>. 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.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Coffee machines with built-in functions:</p>
|
||||
@ -876,8 +876,8 @@ GET /sensors
|
||||
<li>
|
||||
<p>At each abstraction level the idea of ‘order canceling’ is reformulated:</p>
|
||||
<ul>
|
||||
<li>at the <code>orders</code> level this action in fact splits into several ‘cancels’ of other levels: you need to cancel money holding and to cancel an order execution;</li>
|
||||
<li>at the second API kind physical level the ‘cancel’ operation itself doesn't exist: ‘cancel’ means ‘executing the <code>discard_cup</code> command’, which is quite the same as any other command.
|
||||
<li>at the <code>orders</code> level, this action in fact splits into several ‘cancels’ of other levels: you need to cancel money holding and to cancel an order execution;</li>
|
||||
<li>at the second API kind, physical level the ‘cancel’ operation itself doesn't exist: ‘cancel’ means ‘executing the <code>discard_cup</code> command’, which is quite the same as any other command.
|
||||
The interim API level is needed to make this transition between different level ‘cancels’ smooth and rational without jumping over canyons.</li>
|
||||
</ul>
|
||||
</li>
|
||||
@ -885,7 +885,7 @@ The interim API level is needed to make this transition between different level
|
||||
<p>From a high-level point of view, canceling an order is a terminal action, since no further operations are possible. From a low-level point of view, the processing continues until the cup is discarded, and then the machine is to be unlocked (e.g. new runtimes creation allowed). It's a task to the execution control level to couple those two states, outer (the order is canceled) and inner (the execution continues).</p>
|
||||
</li>
|
||||
</ol>
|
||||
<p>It might look that forcing the abstraction levels isolation is redundant and makes interfaces more complicated. In fact, it is: it's very important to understand that flexibility, consistency, readability, and extensibility come with a price. One may construct an API with zero overhead, essentially just provide access to the coffee machine's microcontrollers. However using such an API would be a disaster for a developer, not to mention the inability to extend it.</p>
|
||||
<p>It might look that forcing the abstraction levels isolation is redundant and makes interfaces more complicated. In fact, it is: it's very important to understand that flexibility, consistency, readability, and extensibility come with a price. One may construct an API with zero overhead, essentially just providing access to the coffee machine's microcontrollers. However using such an API would be a disaster for a developer, not to mention the inability to extend it.</p>
|
||||
<p>Separating abstraction levels is first of all a logical procedure: how we explain to ourselves and developers what our API consists of. <strong>The abstraction gap between entities exists objectively</strong>, no matter what interfaces we design. Our task is just separate this gap into levels <em>explicitly</em>. The more implicitly abstraction levels are separated (or worse — blended into each other), the more complicated is your API's learning curve, and the worse is the code that uses it.</p>
|
||||
<h4>The Data Flow</h4>
|
||||
<p>One useful exercise allowing us to examine the entire abstraction hierarchy is excluding all the particulars and constructing (on a paper or just in your head) a data flow chart: what data is flowing through your API entities, and how it's being altered at each step.</p>
|
||||
@ -913,7 +913,7 @@ It is important to note that we don't calculate new variables out from sensors d
|
||||
<p>At the execution level, we read the order level data and create a lower level execution context: the program as a sequence of steps, their parameters, transition rules, and initial state.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>At the runtime level, we read the target parameters (which operation to execute, what the target volume is) and translate them into coffee machine API microcommands and statuses for each command.</p>
|
||||
<p>At the runtime level, we read the target parameters (which operation to execute, and what the target volume is) and translate them into coffee machine API microcommands and statuses for each command.</p>
|
||||
</li>
|
||||
</ol>
|
||||
<p>Also, if we take a deeper look into the ‘bad’ decision (forcing developers to determine actual order status on their own), being discussed at the beginning of this chapter, we could notice a data flow collision there:</p>
|
||||
@ -948,7 +948,7 @@ It is important to note that we don't calculate new variables out from sensors d
|
||||
</li>
|
||||
<li>Program execution control level entities.
|
||||
<ul>
|
||||
<li>A <code>program</code> describes some general execution plan for a coffee machine. Programs could only be read.</li>
|
||||
<li>A <code>program</code> describes a general execution plan for a coffee machine. Programs could only be read.</li>
|
||||
<li>The <code>programs/matcher</code> entity is capable of coupling a <code>recipe</code> and a <code>program</code>, which in fact means ‘to retrieve a dataset needed to prepare a specific recipe on a specific coffee machine’.</li>
|
||||
<li>A <code>programs/run</code> entity describes a single fact of running a program on a coffee machine. A <code>run</code> might be:
|
||||
<ul>
|
||||
@ -2148,7 +2148,7 @@ GET /v1/runtimes/{runtime_id}/state
|
||||
POST /v1/runtimes/{id}/terminate
|
||||
</code></pre><div class="page-break"></div><h2><a href="#section-3" class="anchor" id="section-3">Section II. The Backwards Compatibility</a></h2><h3><a href="#chapter-13" class="anchor" id="chapter-13">Chapter 13. The Backwards Compatibility Problem Statement</a></h3>
|
||||
<p>As usual, let's conceptually define ‘backwards compatibility’ before we start.</p>
|
||||
<p>Backwards compatibility is a feature of the entire API system to be stable in time. It means the following: <strong>the code which developers have written using your API, continues working functionally correctly for a long period of time</strong>. There are two important questions to this definition and two explanations:</p>
|
||||
<p>Backwards compatibility is a feature of the entire API system to be stable in time. It means the following: <strong>the code that developers have written using your API continues working functionally correctly for a long period of time</strong>. There are two important questions to this definition and two explanations:</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p>What does ‘functionally correctly’ mean?</p>
|
||||
@ -2176,13 +2176,13 @@ POST /v1/runtimes/{id}/terminate
|
||||
<p>These arguments could be summarized frankly as ‘the API developers don't want to support the old code’. But this explanation is still incomplete: even if you're not going to rewrite the API code to add new functionality, or you're not going to add it at all, you still have to ship new API versions, minor and major alike.</p>
|
||||
<p><strong>NB</strong>: in this chapter, we don't make any difference between minor versions and patches: ‘minor version’ means any backwards-compatible API release.</p>
|
||||
<p>Let us remind that <a href="(https://twirl.github.io/The-API-Book/docs/API.en.html#chapter-2)">an API is a bridge</a>, a meaning of connecting different programmable contexts. No matter how strong our desire to keep the bridge intact is, our capabilities are limited: we could lock the bridge, but we cannot command the rifts and the canyon itself. That's the source of the problems: we can't guarantee that <em>our own</em> code won't change, so at some point, we will have to ask the clients to change <em>their</em> code.</p>
|
||||
<p>Apart from our aspirations to change the API architecture, three other tectonic processes are happening at the same time: user agents, subject areas, and underlying platforms erosion.</p>
|
||||
<p>Apart from our aspirations to change the API architecture, three other tectonic processes are happening at the same time: user agents, subject areas, and underlying platforms' erosion.</p>
|
||||
<h4>Consumer applications fragmentation</h4>
|
||||
<p>When you shipped the very first API version, and the first clients started to use it, the situation was perfect. There was only one version, and all clients were using just it. When this perfection ends, two scenarios are possible.</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p>If the platform allows for fetching code on-demand as the good old Web does, and you weren't too lazy to implement that code-on-demand feature (in a form of a platform SDK — for example, JS API), then the evolution of your API is more or less under your control. Maintaining backwards compatibility effectively means keeping <em>the client library</em> backwards-compatible. As for client-server interaction, you're free.</p>
|
||||
<p>It doesn't mean that you can't break backwards compatibility. You still can make a mess with cache-control headers or just overlook a bug in the code. Besides, even code-on-demand systems don't get updated instantly. The author of this book faced the situation, when users were deliberately keeping a browser tab open <em>for weeks</em> to get rid of updates. But still, you usually don't have to support more than two API versions — the last one and the penultimate one. Furthermore, you may try to rewrite the previous major version of the library, implementing it on top of the actual API version.</p>
|
||||
<p>It doesn't mean that you can't break backwards compatibility. You still can make a mess with cache-control headers or just overlook a bug in the code. Besides, even code-on-demand systems don't get updated instantly. The author of this book faced the situation when users were deliberately keeping a browser tab open <em>for weeks</em> to get rid of updates. But still, you usually don't have to support more than two API versions — the last one and the penultimate one. Furthermore, you may try to rewrite the previous major version of the library, implementing it on top of the actual API version.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>If the code-on-demand feature isn't supported or is prohibited by the platform, as in modern mobile operating systems, then the situation becomes more severe. Each client effectively borrows a snapshot of the code, working with your API, frozen at the moment of compilation. Client application updates are scattered over time at much more extent than Web application updates. The most painful thing is that <em>some clients will never be up to date</em>, because of one of the three reasons:</p>
|
||||
@ -2191,7 +2191,7 @@ POST /v1/runtimes/{id}/terminate
|
||||
<li>users don't want to get updates (sometimes because users think that developers ‘spoiled’ the app in new versions);</li>
|
||||
<li>users can't get updates because their devices are no longer supported.</li>
|
||||
</ul>
|
||||
<p>In modern times these three categories combined could easily constitute tens of percent of auditory. It implies that cutting the support of any API version might be remarkable — especially if developers' apps continue supporting a more broad spectrum of platforms than the API does.</p>
|
||||
<p>In modern times these three categories combined could easily constitute tens of per cent of auditory. It implies that cutting the support of any API version might be remarkable — especially if developers' apps continue supporting a more broad spectrum of platforms than the API does.</p>
|
||||
<p>You could have never issued any SDK, providing just the server-side API, for example in a form of HTTP endpoints. You might think, given your API is less competitive on the market because of a lack of SDKs, that the backwards compatibility problem is mitigated. That's not true: if you don't provide an SDK, then developers will either adopt an unofficial one (if someone bothers to make it) or just write a framework themselves — independently. ‘Your framework — your problems’ strategy, fortunately or not, works badly: if developers write poor quality code upon your API, then your API is of poor quality itself. Definitely in the view of developers, possibly in the view of end-users, if the API performance within the app is visible to them.</p>
|
||||
</li>
|
||||
</ol>
|
||||
@ -2206,9 +2206,9 @@ POST /v1/runtimes/{id}/terminate
|
||||
<p>As usual, the API provides an abstraction to a much more granular subject area. In the case of our <a href="https://twirl.github.io/The-API-Book/docs/API.en.html#chapter-7">coffee machine API example</a> one might reasonably expect new models to pop up, which are to be supported by the platform. New models tend to provide new APIs, and it's hard to guarantee they might be adopted while preserving the same high-level API. And anyway, the code needs to be altered, which might lead to incompatibility, albeit unintentional.</p>
|
||||
<p>Let us also stress that low-level API vendors are not always as resolute regarding maintaining backwards compatibility for their APIs (actually, any software they provide) as (we hope so) you are. You should be warned that keeping your API in an operational state, e.g. writing and supporting facades to the shifting subject area landscape, will be your problem, and rather a sudden one.</p>
|
||||
<h4>Platform drift</h4>
|
||||
<p>Finally, there is a third side to a story — the ‘canyon’ you're crossing over with a bridge of your API. Developers write code that is executed in some environment you can't control, and it's evolving. New versions of operating systems, browsers, protocols, programming language SDKs emerge. New standards are being developed, new arrangements made, some of them being backwards-incompatible, and nothing could be done about that.</p>
|
||||
<p>Finally, there is a third side to a story — the ‘canyon’ you're crossing over with a bridge of your API. Developers write code that is executed in some environment you can't control, and it's evolving. New versions of operating systems, browsers, protocols, and programming language SDKs emerge. New standards are being developed, new arrangements made, some of them being backwards-incompatible, and nothing could be done about that.</p>
|
||||
<p>Older platform versions lead to fragmentation just like older app versions do, because developers (including the API developers) are struggling with supporting older platforms, and users are struggling with platform updates — and often can't update at all, since newer platform versions require newer devices.</p>
|
||||
<p>The nastiest thing here is that not only does incremental progress in a form of new platforms and protocols demand changing the API, but also does a vulgar fashion. Several years ago realistic 3d icons were popular, but since then the public taste changed in a favor of flat and abstract ones. UI components developers had to follow the fashion, rebuilding their libraries, either shipping new icons or replacing old ones. Similarly, right now ‘night mode’ support is introduced everywhere, demanding changes in a broad range of APIs.</p>
|
||||
<p>The nastiest thing here is that not only does incremental progress in a form of new platforms and protocols demand changing the API, but also does vulgar fashion. Several years ago realistic 3d icons were popular, but since then the public taste changed in a favor of flat and abstract ones. UI components developers had to follow the fashion, rebuilding their libraries, either shipping new icons or replacing old ones. Similarly, right now ‘night mode’ support is introduced everywhere, demanding changes in a broad range of APIs.</p>
|
||||
<h4>Backwards compatibility policy</h4>
|
||||
<p>To summarize the above:</p>
|
||||
<ul>
|
||||
@ -2224,7 +2224,7 @@ POST /v1/runtimes/{id}/terminate
|
||||
</li>
|
||||
<li>
|
||||
<p>How many <em>major</em> versions should be supported at a time?</p>
|
||||
<p>As for major versions, we gave <em>theoretical</em> advice earlier: ideally, the major API version lifecycle should be a bit longer than the platform's one. In stable niches like desktop operating systems, it constitutes 5 to 10 years. In new and emerging ones, it is less but still measured in years. <em>Practically</em> speaking you should look at the size of auditory which continues using older versions.</p>
|
||||
<p>As for major versions, we gave <em>theoretical</em> advice earlier: ideally, the major API version lifecycle should be a bit longer than the platform's one. In stable niches like desktop operating systems, it constitutes 5 to 10 years. In new and emerging ones, it is less but still measured in years. <em>Practically</em> speaking you should look at the size of the auditory which continues using older versions.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>How many <em>minor</em> versions (within one major version) should be supported at a time?</p>
|
||||
@ -2235,7 +2235,7 @@ POST /v1/runtimes/{id}/terminate
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
<p>We will address these questions in more detail in the next chapters. Additionally, in Section III we will also discuss, how to communicate to customers about new releases and older versions support discontinuance, and how to stimulate them to adopt new API versions.</p><div class="page-break"></div><h3><a href="#chapter-14" class="anchor" id="chapter-14">Chapter 14. On the Iceberg's Waterline</a></h3>
|
||||
<p>We will address these questions in more detail in the next chapters. Additionally, in Section III we will also discuss, how to communicate to customers about new releases and discontinued supporting of older versions, and how to stimulate them to adopt new API versions.</p><div class="page-break"></div><h3><a href="#chapter-14" class="anchor" id="chapter-14">Chapter 14. On the Iceberg's Waterline</a></h3>
|
||||
<p>Before we start talking about the extensible API design, we should discuss the hygienic minimum. A huge number of problems would have never happened if API vendors had paid more attention to marking their area of responsibility.</p>
|
||||
<h5><a href="#chapter-14-paragraph-1" id="chapter-14-paragraph-1" class="anchor">1. Provide a minimal amount of functionality</a></h5>
|
||||
<p>At any moment in its lifetime, your API is like an iceberg: it comprises an observable (e.g. documented) part and a hidden one, undocumented. If the API is designed properly, these two parts correspond to each other just like the above-water and under-water parts of a real iceberg do, i.e. one to ten. Why so? Because of two obvious reasons.</p>
|
||||
@ -2247,7 +2247,7 @@ POST /v1/runtimes/{id}/terminate
|
||||
<p>Revoking the API functionality causes losses. If you've promised to provide some functionality, you will have to do so ‘forever’ (until this API version's maintenance period is over). Pronouncing some functionality deprecated is a tricky thing, potentially alienating your customers.</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p>Rule #1 is the simplest: if some functionality might be withheld — then never expose it. It might be reformulated like: every entity, every field, every public API method is a <em>product solution</em>. There must be solid <em>product</em> reasons why some functionality is exposed.</p>
|
||||
<p>Rule #1 is the simplest: if some functionality might be withheld — then never expose it. It might be reformulated like: every entity, every field, and every public API method is a <em>product solution</em>. There must be solid <em>product</em> reasons why some functionality is exposed.</p>
|
||||
<h5><a href="#chapter-14-paragraph-2" id="chapter-14-paragraph-2" class="anchor">2. Avoid gray zones and ambiguities</a></h5>
|
||||
<p>Your obligations to maintain some functionality must be stated as clearly as possible. Especially regarding those environments and platforms where no native capability to restrict access to undocumented functionality exists. Unfortunately, developers tend to consider some private features they found to be eligible for use, thus presuming the API vendor shall maintain them intact. Policy on such ‘findings’ must be articulated explicitly. At the very least, in case of such non-authorized usage of undocumented functionality, you might refer to the docs, and be in your own rights in the eyes of the community.</p>
|
||||
<p>However, API developers often legitimize such gray zones themselves, for example, by:</p>
|
||||
@ -2255,7 +2255,7 @@ POST /v1/runtimes/{id}/terminate
|
||||
<li>returning undocumented fields in endpoints' responses;</li>
|
||||
<li>using private functionality in code examples — in the docs, responding to support messages, in conference talks, etc.</li>
|
||||
</ul>
|
||||
<p>One cannot make a partial commitment. Either you guarantee this code will always work or do not slip a slightest note such functionality exists.</p>
|
||||
<p>One cannot make a partial commitment. Either you guarantee this code will always work or do not slip the slightest note such functionality exists.</p>
|
||||
<h5><a href="#chapter-14-paragraph-3" id="chapter-14-paragraph-3" class="anchor">3. Codify implicit agreements</a></h5>
|
||||
<p>The third principle is much less obvious. Pay close attention to the code which you're suggesting developers to develop: are there any conventions that you consider evident, but never wrote them down?</p>
|
||||
<p><strong>Example #1</strong>. Let's take a look at this order processing SDK example:</p>
|
||||
@ -2264,8 +2264,8 @@ let order = api.createOrder();
|
||||
// Returns the order status
|
||||
let status = api.getStatus(order.id);
|
||||
</code></pre>
|
||||
<p>Let's imagine that you're struggling with scaling your service, and at some point moved to the asynchronous replication of the database. This would lead to the situation when querying for the order status right after order creating might return <code>404</code> if an asynchronous replica hasn't got the update yet. In fact, thus we abandon strict <a href="https://en.wikipedia.org/wiki/Consistency_model">consistency policy</a> in a favor of an eventual one.</p>
|
||||
<p>What would be the result? The code above will stop working. A developer creates an order, tries to get its status — but gets the error. It's very hard to predict what approach developers would implement to tackle this error. Probably, none at all.</p>
|
||||
<p>Let's imagine that you're struggling with scaling your service, and at some point moved to the asynchronous replication of the database. This would lead to the situation when querying for the order status right after order creating might return <code>404</code> if an asynchronous replica hasn't got the update yet. In fact, thus we abandon a strict <a href="https://en.wikipedia.org/wiki/Consistency_model">consistency policy</a> in a favor of an eventual one.</p>
|
||||
<p>What would be the result? The code above will stop working. A developer creates an order, then tries to get its status — but gets the error. It's very hard to predict what approach developers would implement to tackle this error. Probably, none at all.</p>
|
||||
<p>You may say something like, ‘But we've never promised the strict consistency in the first place’ — and that is obviously not true. You may say that if, and only if, you have really described the eventual consistency in the <code>createOrder</code> docs, and all your SDK examples look like:</p>
|
||||
<pre><code>let order = api.createOrder();
|
||||
let status;
|
||||
@ -2282,7 +2282,7 @@ if (status) {
|
||||
…
|
||||
}
|
||||
</code></pre>
|
||||
<p>We presume we may skip the explanations why such code must never be written under any circumstances. If you're really providing a non-strictly consistent API, then either <code>createOrder</code> operation must be asynchronous and return the result when all replicas are synchronized, or the retry policy must be hidden inside <code>getStatus</code> operation implementation.</p>
|
||||
<p>We presume we may skip the explanations why such code must never be written under any circumstances. If you're really providing a non-strictly consistent API, then either the <code>createOrder</code> operation must be asynchronous and return the result when all replicas are synchronized, or the retry policy must be hidden inside the <code>getStatus</code> operation implementation.</p>
|
||||
<p>If you failed to describe the eventual consistency in the first place, then you simply can't make these changes in the API. You will effectively break backwards compatibility, which will lead to huge problems with your customers' apps, intensified by the fact they can't be simply reproduced.</p>
|
||||
<p><strong>Example #2</strong>. Take a look at the following code:</p>
|
||||
<pre><code>let resolve;
|
||||
@ -2293,8 +2293,8 @@ let promise = new Promise(
|
||||
);
|
||||
resolve();
|
||||
</code></pre>
|
||||
<p>This code presumes that the callback function passed to <code>new Promise</code> will be executed synchronously, and the <code>resolve</code> variable will be initialized before the <code>resolve()</code> function is called. But this assumption is based on nothing: there are no clues indicating that <code>new Promise</code> constructor executes the callback function synchronously.</p>
|
||||
<p>Of course, the developers of the language standard can afford such tricks; but you as an API developer cannot. You must at least document this behavior and make the signatures point to it; actually, good advice is to avoid such conventions, since they are simply unobvious while reading the code. And of course, under no circumstances you can actually change this behavior to an asynchronous one.</p>
|
||||
<p>This code presumes that the callback function passed to a <code>new Promise</code> will be executed synchronously, and the <code>resolve</code> variable will be initialized before the <code>resolve()</code> function is called. But this assumption is based on nothing: there are no clues indicating the <code>new Promise</code> constructor executes the callback function synchronously.</p>
|
||||
<p>Of course, the developers of the language standard can afford such tricks; but you as an API developer cannot. You must at least document this behavior and make the signatures point to it; actually, good advice is to avoid such conventions, since they are simply unobvious while reading the code. And of course, under no circumstances, you can actually change this behavior to an asynchronous one.</p>
|
||||
<p><strong>Example #3</strong>. Imagine you're providing animations API, which includes two independent functions:</p>
|
||||
<pre><code>// Animates object's width,
|
||||
// beginning with first value, ending with second
|
||||
@ -2303,7 +2303,7 @@ object.animateWidth('100px', '500px', '1s');
|
||||
// Observes object's width changes
|
||||
object.observe('widthchange', observerFunction);
|
||||
</code></pre>
|
||||
<p>A question arises: how frequently and at what time fractions the <code>observerFunction</code> will be called? Let's assume in the first SDK version we emulated step-by-step animation at 10 frames per second: then <code>observerFunction</code> will be called 10 times, getting values '140px', '180px', etc., up to '500px'. But then in a new API version, we switched to implementing both functions atop of a system's native functionality — and so you simply don't know, when and how frequently the <code>observerFunction</code> will be called.</p>
|
||||
<p>A question arises: how frequently and at what time fractions the <code>observerFunction</code> will be called? Let's assume in the first SDK version we emulated step-by-step animation at 10 frames per second: then the <code>observerFunction</code> will be called 10 times, getting values '140px', '180px', etc., up to '500px'. But then in a new API version, we switched to implementing both functions atop of a system's native functionality — and so you simply don't know, when and how frequently the <code>observerFunction</code> will be called.</p>
|
||||
<p>Just changing call frequency might result in making some code dysfunctional — for example, if the callback function makes some complex calculations, and no throttling is implemented since the developer just relied on your SDK's built-in throttling. And if the <code>observerFunction</code> ceases to be called when exactly '500px' is reached because of some system algorithms specifics, some code will be broken beyond any doubt.</p>
|
||||
<p>In this example, you should document the concrete contract (how often the observer function is called) and stick to it even if the underlying technology is changed.</p>
|
||||
<p><strong>Example #4</strong>. Imagine that customer orders are passing through a specific pipeline:</p>
|
||||
@ -2330,14 +2330,14 @@ object.observe('widthchange', observerFunction);
|
||||
]
|
||||
}
|
||||
</code></pre>
|
||||
<p>Suppose at some moment we decided to allow trustworthy clients to get their coffee in advance before the payment is confirmed. So an order will jump straight to "preparing_started", or event "ready", without a "payment_approved" event being emitted. It might appear to you that this modification <em>is</em> backwards compatible since you've never really promised any specific event order being maintained, but it is not.</p>
|
||||
<p>Let's assume that a developer (probably, your company's business partner) wrote some code implementing some valuable business procedure, for example, gathering income and expenses analytics. It's quite logical to expect this code operates a state machine, which switches from one state to another depending on getting (or getting not) specific events. This analytical code will be broken if the event order changes. In the best-case scenario, a developer will get some exceptions and have to cope with the error's cause; worst-case, partners will operate wrong statistics for an indefinite period of time until they find a mistake.</p>
|
||||
<p>A proper decision would be, in first, documenting the event order and allowed states; in second, continuing generating "payment_approved" event before "preparing_started" (since you're making a decision to prepare that order, so you're in fact approving the payment) and add extended payment information.</p>
|
||||
<p>Suppose at some moment we decided to allow trustworthy clients to get their coffee in advance before the payment is confirmed. So an order will jump straight to "preparing_started", or event "ready", without a "payment_approved" event being emitted. It might appear to you that this modification <em>is</em> backwards-compatible since you've never really promised any specific event order being maintained, but it is not.</p>
|
||||
<p>Let's assume that a developer (probably, your company's business partner) wrote some code implementing some valuable business procedure, for example, gathering income and expenses analytics. It's quite logical to expect this code operates a state machine, which switches from one state to another depending on getting (or getting not) specific events. This analytical code will be broken if the event order changes. In the best-case scenario, a developer will get some exceptions and have to cope with the error's cause; the worst-case, partners will operate wrong statistics for an indefinite period of time until they find a mistake.</p>
|
||||
<p>A proper decision would be, first, documenting the event order and the allowed states; second, continuing generating the "payment_approved" event before the "preparing_started" one (since you're making a decision to prepare that order, so you're in fact approving the payment) and add extended payment information.</p>
|
||||
<p>This example leads us to the last rule.</p>
|
||||
<h5><a href="#chapter-14-paragraph-4" id="chapter-14-paragraph-4" class="anchor">4. Product logic must be backwards compatible as well</a></h5>
|
||||
<h5><a href="#chapter-14-paragraph-4" id="chapter-14-paragraph-4" class="anchor">4. Product logic must be backwards-compatible as well</a></h5>
|
||||
<p>State transition graph, event order, possible causes of status changes — such critical things must be documented. Not every piece of business logic might be defined in a form of a programmatical contract; some cannot be represented at all.</p>
|
||||
<p>Imagine that one day you start to take phone calls. A client may contact the call center to cancel an order. You might even make this functionality <em>technically</em> backwards compatible, introducing new fields to the ‘order’ entity. But the end-user might simply <em>know</em> the number, and call it even if the app wasn't suggesting anything like that. Partner's business analytical code might be broken likewise, or start displaying weather on Mars since it was written knowing nothing about the possibility of canceling orders somehow in circumvention of the partner's systems.</p>
|
||||
<p><em>Technically</em> correct decision would be adding ‘canceling via call center allowed’ parameter to the order creation function. Conversely, call center operators may only cancel those orders which were created with this flag set. But that would be a bad decision from a <em>product</em> point of view. The only ‘good’ decision in this situation is to foresee the possibility of external order cancels in the first place. If you haven't foreseen it, your only option is the ‘Serenity Notepad’ to be discussed in the last chapter of this Section.</p><div class="page-break"></div><h3><a href="#chapter-15" class="anchor" id="chapter-15">Chapter 15. Extending through Abstracting</a></h3>
|
||||
<p>Imagine that one day you start to take phone calls. A client may contact the call center to cancel an order. You might even make this functionality <em>technically</em> backwards-compatible, introducing new fields to the ‘order’ entity. But the end-user might simply <em>know</em> the number, and call it even if the app wasn't suggesting anything like that. Partner's business analytical code might be broken likewise, or start displaying weather on Mars since it was written knowing nothing about the possibility of canceling orders somehow in circumvention of the partner's systems.</p>
|
||||
<p>A <em>technically</em> correct decision would be to add a ‘canceling via call center allowed’ parameter to the order creation function. Conversely, call center operators may only cancel those orders which were created with this flag set. But that would be a bad decision from a <em>product</em> point of view. The only ‘good’ decision in this situation is to foresee the possibility of external order cancellations in the first place. If you haven't foreseen it, your only option is the ‘Serenity Notepad’ to be discussed in the last chapter of this Section.</p><div class="page-break"></div><h3><a href="#chapter-15" class="anchor" id="chapter-15">Chapter 15. Extending through Abstracting</a></h3>
|
||||
<p>In previous chapters, we have tried to outline theoretical rules and illustrate them with practical examples. However, understanding the principles of change-proof API design requires practice above all things. An ability to anticipate future growth problems comes from a handful of grave mistakes once made. One cannot foresee everything but can develop certain technical intuition.</p>
|
||||
<p>So in the following chapters, we will try to probe <a href="#chapter-12">our study API</a> from the previous Section, testing its robustness from every possible viewpoint, thus carrying out some ‘variational analysis’ of our interfaces. More specifically, we will apply a ‘What If?’ question to every entity, as if we are to provide a possibility to write an alternate implementation of every piece of logic.</p>
|
||||
<p><strong>NB</strong>. In our examples, the interfaces will be constructed in a manner allowing for dynamic real-time linking of different entities. In practice, such integrations usually imply writing an ad hoc server-side code in accordance with specific agreements made with specific partners. But for educational purposes, we will pursue more abstract and complicated ways. Dynamic real-time linking is more typical in complex program constructs like operating system APIs or embeddable libraries; giving educational examples based on such sophisticated systems would be too inconvenient.</p>
|
||||
@ -2375,7 +2375,7 @@ PUT /v1/partners/{partnerId}/coffee-machines
|
||||
<p>Now the partners might dynamically plug their coffee machines in and get the orders. But we now will do the following exercise:</p>
|
||||
<ul>
|
||||
<li>enumerate all the implicit assumptions we have made;</li>
|
||||
<li>enumerate all the implicit coupling mechanisms we need to haven the platform functioning properly.</li>
|
||||
<li>enumerate all the implicit coupling mechanisms we need to have the platform functioning properly.</li>
|
||||
</ul>
|
||||
<p>It may look like there are no such things in our API since it's quite simple and basically just describes making some HTTP call — but that's not true.</p>
|
||||
<ol>
|
||||
@ -2383,7 +2383,7 @@ PUT /v1/partners/{partnerId}/coffee-machines
|
||||
<li>There is no need to display some additional data to the end-user regarding coffee being brewed on these new coffee machines.</li>
|
||||
<li>The price of the beverage doesn't depend on the selected partner or coffee machine type.</li>
|
||||
</ol>
|
||||
<p>We have written down this list having one purpose in mind: we need to understand, how exactly will we make these implicit arrangements explicit if we need it. For example, if different coffee machines provide different functionality — for example, some of them provide fixed beverage volumes only — what would change in our API?</p>
|
||||
<p>We have written down this list having one purpose in mind: we need to understand, how exactly will we make these implicit arrangements explicit if we need that. For example, if different coffee machines provide different functionality — let's say, some of them are capable of brewing fixed beverage volumes only — what would change in our API?</p>
|
||||
<p>The universal approach to making such amendments is: to consider the existing interface as a reduction of some more general one like if some parameters were set to defaults and therefore omitted. So making a change is always a three-step process.</p>
|
||||
<ol>
|
||||
<li>Explicitly define the programmatical contract <em>as it works right now</em>.</li>
|
||||
@ -2419,7 +2419,7 @@ PUT /v1/partners/{partnerId}/coffee-machines
|
||||
<p><strong>NB</strong>. When we talk about defining the contract as it works right now, we're talking about <em>internal</em> agreements. We must have asked partners to support those three options while negotiating the interaction format. If we had failed to do so from the very beginning, and now are defining these in a course of expanding the public API, it's a very strong claim to break backwards compatibility, and we should never do that (see <a href="#chapter-14">Chapter 14</a>).</p>
|
||||
<h4>Limits of Applicability</h4>
|
||||
<p>Though this exercise looks very simple and universal, its consistent usage is possible only if the hierarchy of entities is well designed from the very beginning and, which is more important, the vector of the further API expansion is clear. Imagine that after some time passed, the options list got new items; let's say, adding syrup or a second espresso shot. We are totally capable of expanding the list — but not the defaults. So the ‘default’ <code>PUT /coffee-machines</code> interface will eventually become totally useless because the default set of three options will not only be any longer of use but will also look ridiculously: why these three options, what are the selection criteria? In fact, the defaults and the method list will be reflecting the historical stages of our API development, and that's totally not what you'd expect from the helpers and defaults nomenclature.</p>
|
||||
<p>Alas, this dilemma can't be easily resolved. From one side, we want developers to write neat and laconic code, so we must provide useful helpers and defaults. From the other side, we can't know in advance which options sets will be the most frequent after several years of the API expansion.</p>
|
||||
<p>Alas, this dilemma can't be easily resolved. From one side, we want developers to write neat and laconic code, so we must provide useful helpers and defaults. On the other side, we can't know in advance which sets of options will be the most frequent 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="#chapter-16" class="anchor" id="chapter-16">Chapter 16. Strong Coupling and Related Problems</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>
|
||||
@ -2457,12 +2457,12 @@ POST /v1/recipes
|
||||
<li>or the partner provides both the number and all of its localized representations.</li>
|
||||
</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 of 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>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>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, since that simply isn't their responsibility. The exit from this logical labyrinth is: high-level entities must <em>define a context</em>, which other objects are to interpret. To properly design adding new recipe interface 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>To make things worse, let us state that the inverse principle is actually correct either: high-level entities must not define low-level ones, 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 adding a new recipe interface 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 requirements — what exactly the partner 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:</p>
|
||||
<pre><code>l10n.volume.format(value, language_code, country_code)
|
||||
// l10n.formatVolume('300ml', 'en', 'UK') → '300 ml'
|
||||
@ -2488,7 +2488,7 @@ PUT /formatters/volume/ru/US
|
||||
"template": "{volume} ун."
|
||||
}
|
||||
</code></pre>
|
||||
<p><strong>NB</strong>: we are more than aware that such a simple format isn't enough to cover real-world localization use-cases, and one either rely on existing libraries, or design a sophisticated format for such templating, which takes into account such things as grammatical cases and rules of rounding numbers up, or allow defining formatting rules in a form of function code. The example above is simplified for purely educational purposes.</p>
|
||||
<p><strong>NB</strong>: we are more than aware that such a simple format isn't enough to cover real-world localization use-cases, and one either relies on existing libraries or designs a sophisticated format for such templating, which takes into account such things as grammatical cases and rules of rounding numbers up or allow defining formatting rules in a form of function code. The example above is simplified for purely educational purposes.</p>
|
||||
<p>Let us deal with the <code>name</code> and <code>description</code> problem then. To lower the coupling level there we need to formalize (probably just to ourselves) a ‘layout’ concept. We are asking for providing <code>name</code> and <code>description</code> not because we just need them, but for representing them in some specific user interface. This specific UI might have an identifier or a semantic name.</p>
|
||||
<pre><code>GET /v1/layouts/{layout_id}
|
||||
{
|
||||
@ -2536,7 +2536,7 @@ PUT /formatters/volume/ru/US
|
||||
{ "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 data fields.</p>
|
||||
<p>Then our interface would ultimately look like:</p>
|
||||
<p>Then our interface would ultimately look like this:</p>
|
||||
<pre><code>POST /v1/recipes
|
||||
{ "id" }
|
||||
→
|
||||
@ -2583,7 +2583,7 @@ PUT /formatters/volume/ru/US
|
||||
}
|
||||
</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 ‘common’, for example) to allow for publishing new recipes for everyone (and that, by the way, would allow us to organize our own backoffice to edit recipes).</p><div class="page-break"></div><h3><a href="#chapter-17" class="anchor" id="chapter-17">Chapter 17. Weak Coupling</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. A mindful reader might have noted that this technique was already used in our API study much earlier in <a href="#chapter-9">Chapter 9</a> with regards to ‘program’ and ‘program run’ entities. Indeed, we might do it without <code>program-matcher</code> endpoint and make it this way:</p>
|
||||
<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 <a href="#chapter-9">Chapter 9</a> 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
|
||||
@ -2605,7 +2605,7 @@ PUT /formatters/volume/ru/US
|
||||
}
|
||||
}
|
||||
</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 current execution status, and finish (cancel) the order. An obvious way to provide the common interface is to require these three functions being executed via a remote call, for example, like this:</p>
|
||||
<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
|
||||
@ -2635,16 +2635,16 @@ PUT /partners/{id}/coffee-machines
|
||||
<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 either: what if we're plugging via our API not a coffee house, but a vending machine? From one side, it means that <code>modify</code> endpoint and all related stuff are simply meaningless: vending machine couldn't sprinkle cinnamon over a coffee cup, and contactless takeout requirement means nothing to it. From 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>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>
|
||||
<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>
|
||||
</ul>
|
||||
<p>Furthermore, we have to describe both endpoints in the docs. It's quite natural that <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 when half of the methods <em>isn't actually needed at all</em>, like requesting contactless takeout method.</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 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>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 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 from both sides? 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>
|
||||
@ -2655,7 +2655,7 @@ PUT /partners/{id}/coffee-machines
|
||||
<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>
|
||||
</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 context descriptions and a two-way event stream in-between. If we were developing an SDK we would express the idea like this:</p>
|
||||
<pre><code>/* Partner's implementation of the program
|
||||
run procedure for a custom API type */
|
||||
registerProgramRunHandler(apiType, (program) => {
|
||||
@ -2680,21 +2680,21 @@ registerProgramRunHandler(apiType, (program) => {
|
||||
<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>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 partner's API, we now have a long list of <code>context</code> objects fields 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>
|
||||
<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>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 field-event mechanism <em>isn't obligatory to both sides</em>. Let us remember what we sought to achieve:</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>
|
||||
<ul>
|
||||
<li>higher-level context doesn't actually know how low-level API works — and it really doesn't; it describes the changes which occurs within the context itself, and reacts only to those events which mean something to it;</li>
|
||||
<li>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 which could actually happen under its specific conditions.</li>
|
||||
<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>
|
||||
</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, raised 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 its 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>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>
|
||||
<h4>The Inversion of Responsibility</h4>
|
||||
<p>It becomes obvious from what was said above that two-way weak coupling means a significant increase of 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>
|
||||
<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
|
||||
run procedure for a custom API type */
|
||||
registerProgramRunHandler(apiType, (program) => {
|
||||
@ -2722,14 +2722,14 @@ registerProgramRunHandler(apiType, (program) => {
|
||||
});
|
||||
}
|
||||
</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 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 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>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>
|
||||
</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><strong>NB</strong>: many contemporary frameworks explore a shared state approach, Redux being probably the most notable example. In Redux paradigm the code above would look like this:</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,
|
||||
@ -2739,7 +2739,7 @@ registerProgramRunHandler(apiType, (program) => {
|
||||
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 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>
|
||||
<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
|
||||
@ -2793,14 +2793,14 @@ ProgramContext.dispatch = (action) => {
|
||||
</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 has no specific meaning when we talk about some specific hardware context.</p>
|
||||
<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>
|
||||
<p><strong>NB</strong>. In the <a href="#chapter-9">Chapter 9</a> 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.</p><div class="page-break"></div><h3><a href="#chapter-18" class="anchor" id="chapter-18">Chapter 18. Interfaces as a Universal Pattern</a></h3>
|
||||
<p>Let us summarize what we have written in the three previous chapters.</p>
|
||||
<ol>
|
||||
<li>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.</li>
|
||||
<li>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).</li>
|
||||
<li>Concrete functionality, e.g. working with ‘bare metal’ hardware, underlying platform APIs, should be delegated to low-level entities.</li>
|
||||
<li>Concrete functionality, e.g. working with ‘bare metal’ hardware or underlying platform APIs, should be delegated to low-level entities.</li>
|
||||
</ol>
|
||||
<p><strong>NB</strong>. There is nothing novel about these rules: one might easily recognize them being the <a href="https://en.wikipedia.org/wiki/SOLID">SOLID</a> 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.</p>
|
||||
<p>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.</p>
|
||||
@ -2825,14 +2825,14 @@ ProgramContext.dispatch = (action) => {
|
||||
<p>And what is the ‘abstract representation of the search result in the UI’? Do we have other kinds of search, should the <code>ISearchItemViewParameters</code> interface be a subtype of some even more general interface, or maybe a composition of several such ones?</p>
|
||||
</li>
|
||||
</ol>
|
||||
<p>Replacing specific implementations with interfaces not only allows us to answer more clearly many questions which should have been appeared 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.</p><div class="page-break"></div><h3><a href="#chapter-19" class="anchor" id="chapter-19">Chapter 19. The Serenity Notepad</a></h3>
|
||||
<p>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.</p><div class="page-break"></div><h3><a href="#chapter-19" class="anchor" id="chapter-19">Chapter 19. The Serenity Notepad</a></h3>
|
||||
<p>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.</p>
|
||||
<h5><a href="#chapter-19-paragraph-1" id="chapter-19-paragraph-1" class="anchor">1. Remember the iceberg's waterline</a></h5>
|
||||
<p>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 which the author of this book has actually faced once:</p>
|
||||
<p>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:</p>
|
||||
<ul>
|
||||
<li>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;</li>
|
||||
<li>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;</li>
|
||||
<li>when the error was fixed, we've got a bunch of complaints: clients did really use this flaw to stack the buttons in the container's corner.</li>
|
||||
<li>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.</li>
|
||||
</ul>
|
||||
<p>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.</p>
|
||||
<h5><a href="#chapter-19-paragraph-2" id="chapter-19-paragraph-2" class="anchor">2. Test the formal interface</a></h5>
|
||||
@ -2845,12 +2845,12 @@ ProgramContext.dispatch = (action) => {
|
||||
<p>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:</p>
|
||||
<ul>
|
||||
<li>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;</li>
|
||||
<li>on a course of extending the API some interfaces become abstract, but the existing functionality isn't affected; imagine that while implementing the <code>PUT /formatters</code> interface described in the <a href="#chapter-16">Chapter 16</a> 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.</li>
|
||||
<li>on a course of extending the API, some interfaces become abstract, but the existing functionality isn't affected; imagine that while implementing the <code>PUT /formatters</code> interface described in the <a href="#chapter-16">Chapter 16</a> 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.</li>
|
||||
</ul>
|
||||
<p>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.</p>
|
||||
<p><strong>NB</strong>. 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.</p>
|
||||
<h5><a href="#chapter-19-paragraph-4" id="chapter-19-paragraph-4" class="anchor">4. Keep a notepad</a></h5>
|
||||
<p>Whatever tips and tricks that were described in the previous chapters you use, it's often quite probable that you can't do <em>anything</em> to prevent the API inconsistencies start piling up. It's possible to reduce the speed of this stockpiling, foresee some problems, have some interface durability reserved for future use. But one can't foresee <em>everything</em>. At this stage, many developers tend to make some rash decisions, e.g. to release a backwards incompatible minor version to fix some design flaws.</p>
|
||||
<p>Whatever tips and tricks that were described in the previous chapters you use, it's often quite probable that you can't do <em>anything</em> 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 <em>everything</em>. At this stage, many developers tend to make some rash decisions, e.g. releasing a backwards-incompatible minor version to fix some design flaws.</p>
|
||||
<p>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.</p><div class="page-break"></div>
|
||||
</div></article>
|
||||
</body></html>
|
BIN
docs/API.en.pdf
BIN
docs/API.en.pdf
Binary file not shown.
BIN
docs/API.ru.epub
BIN
docs/API.ru.epub
Binary file not shown.
BIN
docs/API.ru.pdf
BIN
docs/API.ru.pdf
Binary file not shown.
Loading…
Reference in New Issue
Block a user