diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..47c8086 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "grammarly.selectors": [ + { + "language": "markdown", + "scheme": "vscode-vfs" + } + ] +} \ No newline at end of file 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 e64b185..4ad1e75 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 @@ -112,4 +112,4 @@ The important (and undeniable) advantage of the *semver* system is that it provi Of course, preserving minor versions infinitely isn't possible (partly because of security and compliance issues that tend to pile up). However, providing such access for a reasonable period of time is rather a hygienic norm for popular APIs. -**NB**. Sometimes to defend the single accessible API version concept, the following argument is put forward: preserving the SDK or API application server code is not enough to maintain strict backwards compatibility, as it might be relying on some un-versioned services (for example, some data in the DB that are shared between all the API versions). We, however, consider this an additional reason to isolate such dependencies (see “The Serenity Notepad” chapter) as it means that changes to these subsystems might lead to the inoperability of the API. \ No newline at end of file +**NB**. Sometimes to defend the single accessible API version concept, the following argument is put forward: preserving the SDK or API application server code is not enough to maintain strict backwards compatibility as it might be relying on some un-versioned services (for example, some data in the DB that are shared between all the API versions). We, however, consider this an additional reason to isolate such dependencies (see “The Serenity Notepad” chapter) as it means that changes to these subsystems might lead to the inoperability of the API. \ No newline at end of file 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 c5c91b7..8345994 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,16 +10,16 @@ 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, and 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 until you really need to. It might be reformulated like that: every entity, every field, and every public API method is a *product decision*. There must be solid *product* reasons why some functionality is exposed. ##### Avoid gray zones and ambiguities -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. +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. However, API developers often legitimize such gray zones themselves, for example, by: - * returning undocumented fields in endpoints' responses; - * using private functionality in code examples — in the docs, responding to support messages, in conference talks, etc. + * returning undocumented fields in endpoints responses; + * using private functionality in code samples — in the docs, while 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 the slightest note such functionality exists. @@ -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 a 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 the order creation 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, 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. +What would be the result? The code above will stop working. A user 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, they would not expect this happen 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: @@ -61,7 +61,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 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. +If you failed to describe the eventual consistency in the first place, then you simply couldn't make these changes in the API. You will effectively break backwards compatibility, which will leads to huge problems with your customers' apps, intensified by the fact they can't be simply reproduced by QA engineers. **Example \#2**. Take a look at the following code: @@ -75,29 +75,29 @@ let promise = new Promise( resolve(); ``` -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. +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 call is executed. 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, the best practice is to avoid such conventions, since they are simply unobvious while reading the code. And of course, under no circumstances dare you change this behavior to an asynchronous one. **Example \#3**. Imagine you're providing animations API, which includes two independent functions: ``` // Animates object's width, -// beginning with first value, -// ending with second -// in a specified time period +// beginning with the first value, +// ending with the second +// in the specified time frame object.animateWidth( '100px', '500px', '1s' ); -// Observes object's width changes +// Observes the object's width changes 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 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. +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 have 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. +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 developers 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. 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. @@ -127,9 +127,9 @@ 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 even `"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; the 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 will have to cope with the error's cause; in the worst case, partners will operate wrong statistics for an indefinite period of time until they find the issue. 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. @@ -137,8 +137,8 @@ This example leads us to the last rule. ##### 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. +State transition graph, event order, possible causes of status changes — such critical things must be documented. However, not every piece of business logic might be defined in a form of a programmatical contract; some cannot be represented in a machine-readable form 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. -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 +A *technically* correct decision would be to add a “canceling via call center allowed” parameter to the order creation function. Conversely, call center operators might only cancel those orders which were created with this flag set. But that would be a bad decision from a *product* point of view as it's quite unobvious to a user we they can cancel some orders by phone and can't cancel others. 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