From 60c3ac6d88b4ab98b8447cef2a552032ab25f9e8 Mon Sep 17 00:00:00 2001 From: Sergey Konstantinov Date: Wed, 18 May 2022 00:30:46 +0300 Subject: [PATCH] grammar --- .../02-Section I. The API Design/03.md | 14 +++---- .../02-Section I. The API Design/04.md | 2 +- .../01.md | 16 ++++---- .../02.md | 28 ++++++------- .../03.md | 6 +-- .../04.md | 10 ++--- .../05.md | 40 +++++++++---------- .../06.md | 4 +- .../07.md | 8 ++-- .../04-Section III. The API Product/01.md | 4 +- .../04-Section III. The API Product/02.md | 14 +++---- 11 files changed, 73 insertions(+), 73 deletions(-) diff --git a/src/en/clean-copy/02-Section I. The API Design/03.md b/src/en/clean-copy/02-Section I. The API Design/03.md index 5c642df..3236c78 100644 --- a/src/en/clean-copy/02-Section I. The API Design/03.md +++ b/src/en/clean-copy/02-Section I. The API Design/03.md @@ -9,7 +9,7 @@ Let us remember that software product is a medium connecting two outstanding con Back to our coffee example. What entity abstraction levels do we see? 1. We're preparing an `order` via the API: one (or more) cups of coffee, and receive payments for this. - 2. Each cup of coffee is being prepared according to some `recipe`, which implies the presence of different ingredients and sequences of preparation steps. + 2. Each cup of coffee is prepared according to some `recipe`, which implies the presence of different ingredients and sequences of preparation steps. 3. Each beverage is being prepared on some physical `coffee machine`, occupying some position in space. 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. @@ -55,7 +55,7 @@ This solution intuitively looks bad, and it really is: it violates all the above Variant I: we have a list of possible volumes fixed and introduce bogus recipes like `/recipes/small-lungo` or `recipes/large-lungo`. Why ‘bogus’? Because it's still the same lungo recipe, same ingredients, same preparation steps, only volumes differ. We will have to start the mass production of recipes, only different in volume, or introduce some recipe ‘inheritance’ to be able to specify the ‘base’ recipe and just redefine the volume. -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: +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: ``` POST /v1/orders @@ -158,7 +158,7 @@ To be more specific, let's assume those two kinds of coffee machines provide the GET /execution/status ``` - **NB**. Just in case: this API violates a number of design principles, starting with a lack of versioning; it's described in such a manner because of two reasons: (1) to demonstrate how to design a more convenient API, (2) in the real life, you would really get something like that from vendors, and this API is quite a sane one, actually. + **NB**. Just in case: this API violates a number of design principles, starting with a lack of versioning; it's described in such a manner because of two reasons: (1) to demonstrate how to design a more convenient API, (2) in the real life, you would really get something like that from vendors, and this API is actually quite a sane one. * Coffee machines with built-in functions: ``` @@ -370,13 +370,13 @@ Let's now look at how the order cancel operation flows through our abstraction l Handling state-modifying operations like the `cancel` one requires more advanced abstraction levels juggling skills compared to non-modifying calls like the `GET /status` one. There are two important moments: 1. At each abstraction level the idea of ‘order canceling’ is reformulated: - * at the `orders` level this action in fact splits into several ‘cancels’ of other levels: you need to cancel money holding and to cancel an order execution; - * at the second API kind physical level the ‘cancel’ operation itself doesn't exist: ‘cancel’ means ‘executing the `discard_cup` command’, which is quite the same as any other command. + * at the `orders` level, this action in fact splits into several ‘cancels’ of other levels: you need to cancel money holding and to cancel an order execution; + * at the second API kind, physical level the ‘cancel’ operation itself doesn't exist: ‘cancel’ means ‘executing the `discard_cup` command’, which is quite the same as any other command. The interim API level is needed to make this transition between different level ‘cancels’ smooth and rational without jumping over canyons. 2. From a high-level point of view, canceling an order is a terminal action, since no further operations are possible. From a low-level point of view, the processing continues until the cup is discarded, and then the machine is to be unlocked (e.g. new runtimes creation allowed). It's a task to the execution control level to couple those two states, outer (the order is canceled) and inner (the execution continues). -It might look that forcing the abstraction levels isolation is redundant and makes interfaces more complicated. In fact, it is: it's very important to understand that flexibility, consistency, readability, and extensibility come with a price. One may construct an API with zero overhead, essentially just 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. +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. Separating abstraction levels is first of all a logical procedure: how we explain to ourselves and developers what our API consists of. **The abstraction gap between entities exists objectively**, no matter what interfaces we design. Our task is just separate this gap into levels *explicitly*. The more implicitly abstraction levels are separated (or worse — blended into each other), the more complicated is your API's learning curve, and the worse is the code that uses it. @@ -403,7 +403,7 @@ We may also traverse the tree backward. 2. At the execution level, we read the order level data and create a lower level execution context: the program as a sequence of steps, their parameters, transition rules, and initial state. - 3. At the runtime level, we read the target parameters (which operation to execute, what the target volume is) and translate them into coffee machine API microcommands and statuses for each command. + 3. At the runtime level, we read the target parameters (which operation to execute, and what the target volume is) and translate them into coffee machine API microcommands and statuses for each command. Also, if we take a deeper look into the ‘bad’ decision (forcing developers to determine actual order status on their own), being discussed at the beginning of this chapter, we could notice a data flow collision there: * from one side, in the order context ‘leaked’ physical data (beverage volume prepared) is injected, therefore stirring abstraction levels irreversibly; diff --git a/src/en/clean-copy/02-Section I. The API Design/04.md b/src/en/clean-copy/02-Section I. The API Design/04.md index 7de8972..f8ace35 100644 --- a/src/en/clean-copy/02-Section I. The API Design/04.md +++ b/src/en/clean-copy/02-Section I. The API Design/04.md @@ -19,7 +19,7 @@ As for our fictional example, it would look as follows. * A `recipe` describes an ‘ideal model’ of some coffee beverage type, e.g. its customer properties. A `recipe` is an immutable entity for us, which means we could only read it. * A `coffee-machine` is a model of a real-world device. We must be able to retrieve the coffee machine's geographical location and the options it supports from this model (which will be discussed below). 2. Program execution control level entities. - * A `program` describes some general execution plan for a coffee machine. Programs could only be read. + * A `program` describes a general execution plan for a coffee machine. Programs could only be read. * The `programs/matcher` entity is capable of coupling a `recipe` and a `program`, which in fact means ‘to retrieve a dataset needed to prepare a specific recipe on a specific coffee machine’. * A `programs/run` entity describes a single fact of running a program on a coffee machine. A `run` might be: * initialized (created); diff --git a/src/en/clean-copy/03-Section II. The Backwards Compatibility/01.md b/src/en/clean-copy/03-Section II. The Backwards Compatibility/01.md index a162dc1..19cbd88 100644 --- a/src/en/clean-copy/03-Section II. The Backwards Compatibility/01.md +++ b/src/en/clean-copy/03-Section II. The Backwards Compatibility/01.md @@ -2,7 +2,7 @@ As usual, let's conceptually define ‘backwards compatibility’ before we start. -Backwards compatibility is a feature of the entire API system to be stable in time. It means the following: **the code which developers have written using your API, continues working functionally correctly for a long period of time**. There are two important questions to this definition and two explanations: +Backwards compatibility is a feature of the entire API system to be stable in time. It means the following: **the code that developers have written using your API continues working functionally correctly for a long period of time**. There are two important questions to this definition and two explanations: 1. What does ‘functionally correctly’ mean? @@ -30,7 +30,7 @@ These arguments could be summarized frankly as ‘the API developers don't want Let us remind that [an API is a bridge]((https://twirl.github.io/The-API-Book/docs/API.en.html#chapter-2)), 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 *our own* code won't change, so at some point, we will have to ask the clients to change *their* code. -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. +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. #### Consumer applications fragmentation @@ -38,7 +38,7 @@ When you shipped the very first API version, and the first clients started to us 1. 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 *the client library* backwards-compatible. As for client-server interaction, you're free. - 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 *for weeks* 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. + 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 *for weeks* 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. 2. 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 *some clients will never be up to date*, because of one of the three reasons: @@ -46,7 +46,7 @@ When you shipped the very first API version, and the first clients started to us * users don't want to get updates (sometimes because users think that developers ‘spoiled’ the app in new versions); * users can't get updates because their devices are no longer supported. - 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. + 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. 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. @@ -66,11 +66,11 @@ Let us also stress that low-level API vendors are not always as resolute regardi #### Platform drift -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. +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. 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. -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. +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. #### Backwards compatibility policy @@ -87,7 +87,7 @@ Let's briefly describe these decisions and the key factors for making them. 2. How many *major* versions should be supported at a time? - As for major versions, we gave *theoretical* 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. *Practically* speaking you should look at the size of auditory which continues using older versions. + As for major versions, we gave *theoretical* 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. *Practically* speaking you should look at the size of the auditory which continues using older versions. 3. How many *minor* versions (within one major version) should be supported at a time? @@ -96,4 +96,4 @@ Let's briefly describe these decisions and the key factors for making them. * if you provide server-side APIs and compiled SDKs only, you may basically do not expose minor versions at all, just the actual one: the server-side API is totally within your control, and you may fix any problem efficiently; * if you provide code-on-demand SDKs, it is considered a good form to provide an access to previous minor versions of SDK for a period of time sufficient enough for developers to test their application and fix some issues if necessary. Since full rewriting isn't necessary, it's fine to align with apps release cycle duration in your industry, which is usually several months in worst cases. -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. +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. diff --git a/src/en/clean-copy/03-Section II. The Backwards Compatibility/02.md b/src/en/clean-copy/03-Section II. The Backwards Compatibility/02.md index 2daaaa2..c003db8 100644 --- a/src/en/clean-copy/03-Section II. The Backwards Compatibility/02.md +++ b/src/en/clean-copy/03-Section II. The Backwards Compatibility/02.md @@ -10,7 +10,7 @@ At any moment in its lifetime, your API is like an iceberg: it comprises an obse * 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. -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 *product solution*. There must be solid *product* reasons why some functionality is exposed. +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 *product solution*. There must be solid *product* reasons why some functionality is exposed. ##### Avoid gray zones and ambiguities @@ -21,7 +21,7 @@ However, API developers often legitimize such gray zones themselves, for example * returning undocumented fields in endpoints' responses; * using private functionality in code examples — in the docs, responding to support messages, in conference talks, etc. -One cannot make a partial commitment. Either you guarantee this code will always work or do not slip a slightest note such functionality exists. +One cannot make a partial commitment. Either you guarantee this code will always work or do not slip the slightest note such functionality exists. ##### Codify implicit agreements @@ -35,9 +35,9 @@ let order = api.createOrder(); let status = api.getStatus(order.id); ``` -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 `404` if an asynchronous replica hasn't got the update yet. In fact, thus we abandon strict [consistency policy](https://en.wikipedia.org/wiki/Consistency_model) in a favor of an eventual one. +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 `404` if an asynchronous replica hasn't got the update yet. In fact, thus we abandon a strict [consistency policy](https://en.wikipedia.org/wiki/Consistency_model) in a favor of an eventual one. -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. +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. 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 `createOrder` docs, and all your SDK examples look like: @@ -58,7 +58,7 @@ if (status) { } ``` -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 `createOrder` operation must be asynchronous and return the result when all replicas are synchronized, or the retry policy must be hidden inside `getStatus` operation implementation. +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 `createOrder` operation must be asynchronous and return the result when all replicas are synchronized, or the retry policy must be hidden inside the `getStatus` operation implementation. 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. @@ -74,9 +74,9 @@ let promise = new Promise( resolve(); ``` -This code presumes that the callback function passed to `new Promise` will be executed synchronously, and the `resolve` variable will be initialized before the `resolve()` function is called. But this assumption is based on nothing: there are no clues indicating that `new Promise` constructor executes the callback function synchronously. +This code presumes that the callback function passed to a `new Promise` will be executed synchronously, and the `resolve` variable will be initialized before the `resolve()` function is called. But this assumption is based on nothing: there are no clues indicating the `new Promise` constructor executes the callback function synchronously. -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. +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. **Example \#3**. Imagine you're providing animations API, which includes two independent functions: @@ -89,7 +89,7 @@ object.animateWidth('100px', '500px', '1s'); object.observe('widthchange', observerFunction); ``` -A question arises: how frequently and at what time fractions the `observerFunction` will be called? Let's assume in the first SDK version we emulated step-by-step animation at 10 frames per second: then `observerFunction` 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 `observerFunction` will be called. +A question arises: how frequently and at what time fractions the `observerFunction` will be called? Let's assume in the first SDK version we emulated step-by-step animation at 10 frames per second: then the `observerFunction` 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 `observerFunction` will be called. 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 `observerFunction` ceases to be called when exactly '500px' is reached because of some system algorithms specifics, some code will be broken beyond any doubt. @@ -122,18 +122,18 @@ GET /v1/orders/{id}/events/history } ``` -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 *is* backwards compatible since you've never really promised any specific event order being maintained, but it is not. +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 *is* backwards-compatible since you've never really promised any specific event order being maintained, but it is not. -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. +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. -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. +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. This example leads us to the last rule. -##### Product logic must be backwards compatible as well +##### Product logic must be backwards-compatible as well 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. -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 *technically* backwards compatible, introducing new fields to the ‘order’ entity. But the end-user might simply *know* 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. +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 *technically* backwards-compatible, introducing new fields to the ‘order’ entity. But the end-user might simply *know* 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. -*Technically* 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 *product* 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. \ No newline at end of file +A *technically* 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 *product* 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. \ No newline at end of file diff --git a/src/en/clean-copy/03-Section II. The Backwards Compatibility/03.md b/src/en/clean-copy/03-Section II. The Backwards Compatibility/03.md index 71dc73d..1f19e3a 100644 --- a/src/en/clean-copy/03-Section II. The Backwards Compatibility/03.md +++ b/src/en/clean-copy/03-Section II. The Backwards Compatibility/03.md @@ -44,14 +44,14 @@ So the mechanics is like that: Now the partners might dynamically plug their coffee machines in and get the orders. But we now will do the following exercise: * enumerate all the implicit assumptions we have made; - * enumerate all the implicit coupling mechanisms we need to haven the platform functioning properly. + * enumerate all the implicit coupling mechanisms we need to have the platform functioning properly. 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. 1. It is implied that every coffee machine supports every order option like varying the beverage volume. 2. There is no need to display some additional data to the end-user regarding coffee being brewed on these new coffee machines. 3. The price of the beverage doesn't depend on the selected partner or coffee machine type. -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? +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? 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. @@ -89,7 +89,7 @@ Usually, just adding a new optional parameter to the existing interface is enoug 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’ `PUT /coffee-machines` 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. -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. +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. **NB**. 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 `POST /use-defaults {"version": "v2"}` 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. diff --git a/src/en/clean-copy/03-Section II. The Backwards Compatibility/04.md b/src/en/clean-copy/03-Section II. The Backwards Compatibility/04.md index 2f6d0f7..412dc4f 100644 --- a/src/en/clean-copy/03-Section II. The Backwards Compatibility/04.md +++ b/src/en/clean-copy/03-Section II. The Backwards Compatibility/04.md @@ -44,9 +44,9 @@ And here the first big question arises: what should we do with the `default_volu 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. -The localization flaws are not the only problem of this API. We should ask ourselves a question — *why* do we really need these `name` and `description`? They are simply non-machine-readable strings with no specific semantics. At first glance, we need them to return them back in the `/v1/search` method response, but that's not a proper answer: why do we really return these strings from `search`? +The localization flaws are not the only problem with this API. We should ask ourselves a question — *why* do we really need these `name` and `description`? They are simply non-machine-readable strings with no specific semantics. At first glance, we need them to return them back in the `/v1/search` method response, but that's not a proper answer: why do we really return these strings from `search`? -The correct answer lies a way beyond this specific interface. We need them *because some representation exists*. There is a UI for choosing beverage type. Probably the `name` and `description` 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 *what if* a partner is making their own UI for their own app? Not only they might not actually need two descriptions, but we are also *deceiving* them. The `name` 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’. +The correct answer lies a way beyond this specific interface. We need them *because some representation exists*. There is a UI for choosing beverage type. Probably the `name` and `description` 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 *what if* a partner is making their own UI for their own app? Not only they might not actually need two descriptions, but we are also *deceiving* them. The `name` 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’. 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. `product_properties` 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. @@ -54,7 +54,7 @@ Problems we're facing are the problems of *strong coupling*. Each time we offer #### The rule of contexts -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 *define a context*, 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. +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 *define a context*, 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. 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: @@ -87,7 +87,7 @@ PUT /formatters/volume/ru/US } ``` -**NB**: 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. +**NB**: 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. Let us deal with the `name` and `description` problem then. To lower the coupling level there we need to formalize (probably just to ourselves) a ‘layout’ concept. We are asking for providing `name` and `description` 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. @@ -146,7 +146,7 @@ POST /v1/layouts or they may ultimately design their own UI and don't use this functionality at all, defining neither layouts nor data fields. -Then our interface would ultimately look like: +Then our interface would ultimately look like this: ``` POST /v1/recipes diff --git a/src/en/clean-copy/03-Section II. The Backwards Compatibility/05.md b/src/en/clean-copy/03-Section II. The Backwards Compatibility/05.md index b38cf82..4fb960c 100644 --- a/src/en/clean-copy/03-Section II. The Backwards Compatibility/05.md +++ b/src/en/clean-copy/03-Section II. The Backwards Compatibility/05.md @@ -1,6 +1,6 @@ ### 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. A mindful reader might have noted that this technique was already used in our API study much earlier in [Chapter 9](#chapter-9) with regards to ‘program’ and ‘program run’ entities. Indeed, we might do it without `program-matcher` endpoint and make it this way: +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 [Chapter 9](#chapter-9) with regards to the ‘program’ and ‘program run’ entities. Indeed, we might do it without the `program-matcher` endpoint and make it this way: ``` GET /v1/recipes/{id}/run-data/{api_type} @@ -28,7 +28,7 @@ PUT /v1/api-types/{api_type} } ``` -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: +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: ``` // This is an endpoint for partners @@ -63,19 +63,19 @@ Though this API looks absolutely universal, it's quite easy to demonstrate how o 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, `program_modify_endpoint`, 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 *is* important is that both endpoint and 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 either: what if we're plugging via our API not a coffee house, but a vending machine? From one side, it means that `modify` 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 *takeout approval*: 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. +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: 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 *takeout approval*: 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. Programmable takeout approval requires one more endpoint, let's say, `program_takeout_endpoint`. And so we've lost our way in a forest of three endpoints: * to have vending machines integrated a partner must implement the `program_takeout_endpoint`, but doesn't actually need the `program_modify_endpoint`; * to have regular coffee houses integrated a partner must implement the `program_modify_endpoint`, but doesn't actually need the `program_takeout_endpoint`. -Furthermore, we have to describe both endpoints in the docs. It's quite natural that `takeout` endpoint is very specific; unlike cinnamon sprinkling, which we hid under the pretty general `modify` 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. +Furthermore, we have to describe both endpoints in the docs. It's quite natural that the `takeout` endpoint is very specific; unlike cinnamon sprinkling, which we hid under the pretty general `modify` 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. -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 *always* 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 *isn't actually needed at all*, like requesting contactless takeout method. +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 *always* 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 *isn't actually needed at all*, like the requesting a contactless takeout one. 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. -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 *ideal* solution look from both sides? Something like this: +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 *ideal* solution look from both sides? Something like this: * 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; * 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. @@ -86,7 +86,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 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. -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: +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: ``` /* Partner's implementation of the program @@ -115,26 +115,26 @@ registerProgramRunHandler(apiType, (program) => { 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; - * instead of a long list of methods that shall be implemented to integrate partner's API, we now have a long list of `context` objects fields and events they generate; - * and with regards to technological progress we've changed nothing: now we have deprecated fields and events instead of deprecated methods. + * instead of calling the `takeout` method, we're now generating a pair of `takeout_requested`/`takeout_ready` events; + * 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` objects fields and events they generate; + * and with regards to technological progress, we've changed nothing: now we have deprecated fields and events instead of deprecated methods. 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 *are obliged* 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. -The difference between strong coupling and weak coupling is that field-event mechanism *isn't obligatory to both sides*. Let us remember what we sought to achieve: +The difference between strong coupling and weak coupling is that the field-event mechanism *isn't obligatory to both sides*. Let us remember what we sought to achieve: - * 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; - * 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. + * 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; + * 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. 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. -Worth mentioning that the number of entities (fields, events), though effectively doubled compared to strong-coupled API design, raised qualitatively, not quantitatively. The `program` context describes fields and events in its own terms (type of beverage, volume, cinnamon sprinkling), while the `execution` context must reformulate those terms according to its own subject area (omitting redundant ones, by the way). It is also important that the `execution` context might concretize these properties for underlying objects according to its own specifics, while the `program` context must keep its properties general enough to be applicable to any possible underlying technology. +Worth mentioning that the number of entities (fields, events), though effectively doubled compared to strong-coupled API design, increased qualitatively, not quantitatively. The `program` context describes fields and events in its own terms (type of beverage, volume, cinnamon sprinkling), while the `execution` context must reformulate those terms according to its own subject area (omitting redundant ones, by the way). It is also important that the `execution` context might concretize these properties for underlying objects according to their own specifics, while the `program` context must keep its properties general enough to be applicable to any possible underlying technology. -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. +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. #### The Inversion of Responsibility -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: +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: ``` /* Partner's implementation of program @@ -165,7 +165,7 @@ registerProgramRunHandler(apiType, (program) => { } ``` -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 *lower* abstraction level. Situations with different realizations of *higher* abstraction levels emerging are, of course, possible, but quite rare. The tree of alternative implementations usually grows top to bottom. +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 *lower* abstraction level. Situations with different realizations of *higher* abstraction levels emerging are, of course, possible, but quite rare. The tree of alternative implementations usually grows from top to bottom. Another reason to justify this solution is that major changes occurring at different abstraction levels have different weights: @@ -174,7 +174,7 @@ Another reason to justify this solution is that major changes occurring at diffe 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. -**NB**: 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: +**NB**: 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: ``` execution.prepareTakeout(() => { @@ -187,7 +187,7 @@ execution.prepareTakeout(() => { }); ``` -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 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. +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 interacting with its closest higher-level neighbors only, delegating the responsibility of calling high-level or global methods to them. ``` execution.prepareTakeout(() => { @@ -255,7 +255,7 @@ will work with the partner's coffee machines (like it's a third API type). #### Delegate! -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 has no specific meaning when we talk about some specific hardware context. +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. diff --git a/src/en/clean-copy/03-Section II. The Backwards Compatibility/06.md b/src/en/clean-copy/03-Section II. The Backwards Compatibility/06.md index 367808e..47d7c2e 100644 --- a/src/en/clean-copy/03-Section II. The Backwards Compatibility/06.md +++ b/src/en/clean-copy/03-Section II. The Backwards Compatibility/06.md @@ -4,7 +4,7 @@ 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. 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, underlying platform APIs, should be delegated to low-level entities. + 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. @@ -27,4 +27,4 @@ So our interface (let us call it `ISearchResult`) is actually a composition of t 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 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. \ No newline at end of file +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. \ No newline at end of file diff --git a/src/en/clean-copy/03-Section II. The Backwards Compatibility/07.md b/src/en/clean-copy/03-Section II. The Backwards Compatibility/07.md index f0f8ceb..844dea7 100644 --- a/src/en/clean-copy/03-Section II. The Backwards Compatibility/07.md +++ b/src/en/clean-copy/03-Section II. The Backwards Compatibility/07.md @@ -4,10 +4,10 @@ Apart from the abovementioned abstract principles, let us give a list of concret ##### 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 which 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 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: * 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; - * 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. + * 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. 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. @@ -22,7 +22,7 @@ Any software must be tested, and APIs ain't an exclusion. However, there are som 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 [Chapter 16](#chapter-16) 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. + * 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 [Chapter 16](#chapter-16) 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. 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. @@ -30,6 +30,6 @@ There are obvious local problems with this approach (like the inconsistency in f ##### 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, 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. to release a backwards incompatible minor version to fix some design flaws. +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. 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. \ No newline at end of file diff --git a/src/en/drafts/04-Section III. The API Product/01.md b/src/en/drafts/04-Section III. The API Product/01.md index 38ce5cb..2f322c5 100644 --- a/src/en/drafts/04-Section III. The API Product/01.md +++ b/src/en/drafts/04-Section III. The API Product/01.md @@ -18,10 +18,10 @@ The obvious conclusion of this model is that you must advertise the advantages o 'Stop!', the mindful reader must yell at this moment. The actual order of things is exactly the opposite: * developers are normally a hired workforce implementing the tasks set by business owners (and even if a developer implements a project of his own, they still choose an API which fits the project best, thus being an employer of themselves); * business leaders don't set tasks out of their sense of style or code elegance; they need some functionality being implemented — one that is needed to solve their customers' problems; - * finally, customers don't care about the technical aspects of the solution; they choose the product they need and ask for some specific functionality being implemented. + * finally, customers don't care about the technical aspects of the solution; they choose the product they need and ask for some specific functionality implemented. So it turns out that customers are at the apex of the pyramid: it is customers you need to convince they need not *any* cup of coffee, but a cup of coffee brewed using our API (interesting question: how will we convey the knowledge which API works under the hood, and why customers should pay their money for it!); then business owners will set the task to integrate the API, and developers will have no other choice but to implement it (which, by the way, means that investing into API's readability and consistency is not *that* important). The truth, of course, lies somewhere in between. In some markets and subject areas, it is developers who make decisions (i.e. which framework to choose); in other markets and areas, it might be business owners or customers. It also depends on the competitiveness of the market: introducing a new frontend framework does not meet any resistance while developing, let's say, a new mobile operating system requires million-dollar investments into promotions and strategic partnerships. -Here and after, we will describe some ‘averaged’ situation, meaning that all three pyramid levels are important: customers choosing the product which fits their needs best, business owners seeking quality guarantees and lower development costs, as well as software engineers caring about the API capabilities and the convenience of working with it. \ No newline at end of file +Here and after, we will describe some ‘averaged’ situations, meaning that all three pyramid levels are important: customers choosing the product which fits their needs best, business owners seeking quality guarantees and lower development costs, as well as software engineers caring about the API capabilities and the convenience of working with it. \ No newline at end of file diff --git a/src/en/drafts/04-Section III. The API Product/02.md b/src/en/drafts/04-Section III. The API Product/02.md index 215c8d2..5fe58b6 100644 --- a/src/en/drafts/04-Section III. The API Product/02.md +++ b/src/en/drafts/04-Section III. The API Product/02.md @@ -4,7 +4,7 @@ Before we proceed to the API product management principles description, let us d #### Developers = end users -The easiest and the most understandable case is that of providing a service for developers, with no end users involved. First of all, we talk about different software engineering tools: APIs of programming languages, frameworks, operating systems, UI libraries, game engines, etc; in other words, general-purpose interfaces. [In our coffee API case it means the following: we've developed a library for ordering a cup of coffee, possibly furnished with UI components, and now selling it to coffeeshop chains owners whoever willing to buy it to ease the development of their own applications.] In this case, the answer to the ‘why have an API’ question is self-evident. +The easiest and the most understandable case is that of providing a service for developers, with no end users involved. First of all, we talk about different software engineering tools: APIs of programming languages, frameworks, operating systems, UI libraries, game engines, etc; in other words, general-purpose interfaces. [In our coffee API case, it means the following: we've developed a library for ordering a cup of coffee, possibly furnished with UI components, and now selling it to coffeeshop chains owners whoever willing to buy it to ease the development of their own applications.] In this case, the answer to the ‘why have an API’ question is self-evident. There is also a plethora of monetizing techniques; in fact, we're just talking about monetizing software for developers. @@ -14,7 +14,7 @@ There is also a plethora of monetizing techniques; in fact, we're just talking a 3. The API itself might be free, but the developer company might provide additional paid services (for example, consulting or integrating ones), or just sell the extended technical support. - 4. The API development might be sponsored (explicitly or implicitly) by the platform or operating system owners [in our coffee case — by the smart coffee machines vendors] who are interested in providing a wide range of convenient tools for developers to work with the platform. + 4. The API development might be sponsored (explicitly or implicitly) by the platform or operating system owners [in our coffee case — by the vendors of smart coffee machines] who are interested in providing a wide range of convenient tools for developers to work with the platform. 5. Finally, the API developer company might be attracting attention to other related programming tools and hoping to increase sales by publishing the API under a free license. @@ -42,26 +42,26 @@ B2B services are a special case. As B2B Service providers benefit from offering ##### API = an advertisment site -In this case, we talk mostly about widgets and search engines; to display commercials direct access to end users is a must. The most typical examples of such APIs are advertisement networks APIs. However, mixed approaches do exist either — meaning that some API, usually a searching one, goes with commercial incuts. [In our cofee example, it means that offers searching function will start promoting paid results on the search results page.] +In this case, we talk mostly about widgets and search engines; to display commercials direct access to end users is a must. The most typical examples of such APIs are advertisement networks APIs. However, mixed approaches do exist either — meaning that some API, usually a searching one, goes with commercial incuts. [In our coffee example, it means that the offer searching function will start promoting paid results on the search results page.] ##### API = self-advertisement and self-PR -If an API has neither explicit nor implicit monetization, it might still generate some income, increasing the company's brand awareness through displaying logos and other recognizable elements while working with the API, either native (if the API goes with UI elements) or agreed-upon ones (if partners are obliged to embed specifical branding in those places where the API functionality is used or the data acquired through API is displayed). The API provider company's goals in this case are either attracting users to the company's services or just increasing brand awareness in general. [In case of our coffee API — let's imagine that we're providing some totally unrelated service, let's say, selling tyres, and through providing the API we hope to increase the brand recognition and get a reputation as an IT-company.] +If an API has neither explicit nor implicit monetization, it might still generate some income, increasing the company's brand awareness through displaying logos and other recognizable elements while working with the API, either native (if the API goes with UI elements) or agreed-upon ones (if partners are obliged to embed specifical branding in those places where the API functionality is used or the data acquired through API is displayed). The API provider company's goals in this case are either attracting users to the company's services or just increasing brand awareness in general. [In the case of our coffee API, let's imagine that we're providing some totally unrelated service, like selling tires, and by providing the API we hope to increase brand recognition and get a reputation as an IT company.] The target audiences of such self-promotion might also differ: * you might seek to increase the brand awareness among end users (by embedding logos and links to your services on partner's websites and applications), and even build the brand exclusively through such integrations [for example, if our coffee API provides coffeeshop ratings, and we're working hard on making consumers demand the coffeeshops to publish the ratings]; - * you might concentrate efforts on increasing awareness among business owners [for example, for partners integrating coffee ordering widget on their websites to also pay attention to your tyres catalogue]; + * you might concentrate efforts on increasing awareness among business owners [for example, for partners integrating coffee ordering widget on their websites to also pay attention to your tires catalogue]; * finally, you might provide APIs only to make developers know your company's name to increase their knowledge of your other products or just to improve your reputation as an employer (this activity is sometimes called ‘tech-PR’). Additionally, we might talk about forming a community, e.g. the network of developers (or customers, or business owners) who are loyal to the product. The benefits of having such a community might be substantial: lowering the technical support costs, getting a convenient channel for publishing announcements regarding new services and new releases, and obtaining beta users for upcoming products. ##### API = a feedback and UGC tool -If a company possesses some big data, it might be useful to provide a public API for users to make corrections in the data or otherwise get involved in working with it. For example, cartographical API providers usually allow to post feedback or correct a mistake right on partners' websites and applications. [In case of our coffee API we might be collecting feedback to improve the service, both passively through building coffeeshops ratings or actively through contacting business owners to convey users' requests or through finding new coffeeshops that are still not integrated with the platform.] +If a company possesses some big data, it might be useful to provide a public API for users to make corrections in the data or otherwise get involved in working with it. For example, cartographical API providers usually allow to post feedback or correct a mistake right on partners' websites and applications. [In the case of our coffee API, we might be collecting feedback to improve the service, both passively through building coffeeshops ratings or actively through contacting business owners to convey users' requests or through finding new coffee shops that are still not integrated with the platform.] ##### Terraforming -Finally, the most altruistic approach to API product development is providing it free of charge (or as an open source and open data project) just to change the landscape. If today nobody's willing to pay for the API, we might invest in popularizing the functionality hoping to find commercial niches later (in any of the abovementioned formats) or to increase the significance and usefulness of the API integrations for end users (and therefore the readiness of the partners to pay for the API). [In case of our cofee example — imagine a coffee machines maker starts providing APIs for free aiming to make having an API a ‘must’ for every coffee machine vendor thus allowing for developing commerical API-based services in the future.] +Finally, the most altruistic approach to API product development is providing it free of charge (or as an open source and open data project) just to change the landscape. If today nobody's willing to pay for the API, we might invest in popularizing the functionality hoping to find commercial niches later (in any of the abovementioned formats) or to increase the significance and usefulness of the API integrations for end users (and therefore the readiness of the partners to pay for the API). [In the case of our coffee example, imagine a coffee machine maker that starts providing APIs for free aiming to make having an API a ‘must’ for every coffee machine vendor thus allowing for the development of commercial API-based services in the future.] ##### Gray zones