§
@@ -4678,7 +4678,7 @@ X-Idempotency-Token: <token>
If standard JSON deserializers are used, the overhead compared to binary standards might indeed be significant. However, if this overhead is a real problem, it makes sense to consider alternative JSON serializers such as simdjson 15 . Due to their low-level and highly optimized code, simdjson demonstrates impressive throughput which would be suitable for all APIs except some corner cases.
-The combination of gzip/brotli + simdjson largely renders the use of optimized JSON derivatives, such as BSON, unnecessary in client-server communication.16
+The combination of gzip/brotli + simdjson largely renders the use of optimized JSON derivatives, such as BSON,16 unnecessary in client-server communication.
@@ -4783,7 +4783,7 @@ X-Idempotency-Token: <token>
4 HATEOAShttps://en.wikipedia.org/wiki/HATEOAS
5 Gupta, L. What is RESThttps://restfulapi.net/
The important exercise we must conduct is to describe the format of an HTTP request and response and explain the basic concepts. Many of these may seem obvious to the reader. However, the situation is that even the basic knowledge we require to move further is scattered across vast and fragmented documentation, causing even experienced developers to struggle with some nuances. Below, we will try to compile a structured overview that is sufficient to design HTTP APIs.
-To describe the semantics and formats, we will refer to the brand-new RFC 91101 , which replaces no fewer than nine previous specifications dealing with different aspects of the technology. However, a significant volume of additional functionality is still covered by separate standards. In particular, the HTTP caching principles are described in the standalone RFC 91112 , while the popular PATCH
method is omitted in the main RFC and is regulated by RFC 57893 .
+To describe the semantics and formats, we will refer to the brand-new RFC 91101 , which replaces no fewer than nine previous specifications dealing with different aspects of the technology. However, a significant volume of additional functionality is still covered by separate standards. In particular, the HTTP caching principles are described in the standalone RFC 91112 , while the popular PATCH
method is omitted in the main RFC and is regulated by RFC 57893 .
An HTTP request consists of (1) applying a specific verb to a URL, stating (2) the protocol version, (3) additional meta-information in headers, and (4) optionally, some content (request body):
POST /v1/orders HTTP/1.1
Host: our-api-host.tld
@@ -4807,10 +4807,10 @@ Content-Type: application/json
"id" : 123
}
-NB : In HTTP/2 (and future HTTP/3), separate binary frames are used for headers and data instead of the holistic text format.4 However, this doesn't affect the architectural concepts we will describe below. To avoid ambiguity, we will provide examples in the HTTP/1.1 format.
+NB : In HTTP/2 (and future HTTP/3), separate binary frames are used for headers and data instead of the holistic text format.4 However, this doesn't affect the architectural concepts we will describe below. To avoid ambiguity, we will provide examples in the HTTP/1.1 format.
A Uniform Resource Locator (URL) is an addressing unit in an HTTP API. Some evangelists of the technology even use the term “URL space” as a synonym for “The World Wide Web.” It is expected that a proper HTTP API should employ an addressing system that is as granular as the subject area itself; in other words, each entity that the API can manipulate should have its own URL.
-The URL format is governed by a separate standard5 developed by an independent body known as the Web Hypertext Application Technology Working Group (WHATWG). The concepts of URLs and Uniform Resource Names (URNs) together constitute a more general entity called Uniform Resource Identifiers (URIs). (The difference between the two is that a URL allows for locating a resource within the framework of some protocol whereas a URN is an “internal” entity name that does not provide information on how to find the resource.)
+The URL format is governed by a separate standard5 developed by an independent body known as the Web Hypertext Application Technology Working Group (WHATWG). The concepts of URLs and Uniform Resource Names (URNs) together constitute a more general entity called Uniform Resource Identifiers (URIs). (The difference between the two is that a URL allows for locating a resource within the framework of some protocol whereas a URN is an “internal” entity name that does not provide information on how to find the resource.)
URLs can be decomposed into sub-components, each of which is optional. While the standard enumerates a number of legacy practices, such as passing logins and passwords in URLs or using non-UTF encodings, we will skip discussing those. Instead, we will focus on the following components that are relevant to the topic of HTTP API design:
A scheme: a protocol to access the resource (in our case it is always https:
)
@@ -4842,11 +4842,11 @@ Content-Type: application/json
Headers contain metadata associated with a request or a response. They might describe properties of entities being passed (e.g., Content-Length
), provide additional information regarding a client or a server (e.g., User-Agent
, Date
, etc.) or simply contain additional fields that are not directly related to the request/response semantics (such as Authorization
).
The important feature of headers is the possibility to read them before the message body is fully transmitted. This allows for altering request or response handling depending on the headers, and it is perfectly fine to manipulate headers while proxying requests. Many network agents actually do this, i.e., add, remove, or modify headers while proxying requests. In particular, modern web browsers automatically add a number of technical headers, such as User-Agent
, Origin
, Accept-Language
, Connection
, Referer
, Sec-Fetch-*
, etc., and modern server software automatically adds or modifies such headers as X-Powered-By
, Date
, Content-Length
, Content-Encoding
, X-Forwarded-For
, etc.
-This freedom in manipulating headers can result in unexpected problems if an API uses them to transmit data as the field names developed by an API vendor can accidentally overlap with existing conventional headers, or worse, such a collision might occur in the future at any moment. To avoid this issue, the practice of adding the prefix X-
to custom header names was frequently used in the past. More than ten years ago this practice was officially discouraged (see the detailed overview in RFC 66486 ). Nevertheless, the prefix has not been fully abolished, and many semi-standard headers still contain it (notably, X-Forwarded-For
). Therefore, using the X-
prefix reduces the probability of collision but does not eliminate it. The same RFC reasonably suggests using the API vendor name as a prefix instead of X-
. (We would rather recommend using both, i.e., sticking to the X-ApiName-FieldName
format. Here X-
is included for readability [to distinguish standard fields from custom ones], and the company or API name part helps avoid collisions with other non-standard header names).
+This freedom in manipulating headers can result in unexpected problems if an API uses them to transmit data as the field names developed by an API vendor can accidentally overlap with existing conventional headers, or worse, such a collision might occur in the future at any moment. To avoid this issue, the practice of adding the prefix X-
to custom header names was frequently used in the past. More than ten years ago this practice was officially discouraged (see the detailed overview in RFC 66486 ). Nevertheless, the prefix has not been fully abolished, and many semi-standard headers still contain it (notably, X-Forwarded-For
). Therefore, using the X-
prefix reduces the probability of collision but does not eliminate it. The same RFC reasonably suggests using the API vendor name as a prefix instead of X-
. (We would rather recommend using both, i.e., sticking to the X-ApiName-FieldName
format. Here X-
is included for readability [to distinguish standard fields from custom ones], and the company or API name part helps avoid collisions with other non-standard header names).
Additionally, headers are used as control flow instructions for so-called “content negotiation,” which allows the client and server to agree on a response format (through Accept*
headers) and to perform conditional requests that aim to reduce traffic by skipping response bodies, either fully or partially (through If-*
headers, such as If-Range
, If-Modified-Since
, etc.)
One important component of an HTTP request is a method (verb) that describes the operation being applied to a resource. RFC 9110 standardizes eight verbs — namely, GET
, POST
, PUT
, DELETE
, HEAD
, CONNECT
, OPTIONS
, and TRACE
— of which we as API developers are interested in the former four. The CONNECT
, OPTIONS
, and TRACE
methods are technical and rarely used in HTTP APIs (except for OPTIONS
, which needs to be implemented to ensure access to the API from a web browser). Theoretically, the HEAD
verb, which allows for requesting resource metadata only , might be quite useful in API design. However, for reasons unknown to us, it did not take root in this capacity.
-Apart from RFC 9110, many other specifications propose additional HTTP verbs, such as COPY
, LOCK
, SEARCH
, etc. — the full list can be found in the registry7 . However, only one of them gained widespread popularity — the PATCH
method. The reasons for this state of affairs are quite trivial: the five methods (GET
, POST
, PUT
, DELETE
, and PATCH
) are enough for almost any API.
+Apart from RFC 9110, many other specifications propose additional HTTP verbs, such as COPY
, LOCK
, SEARCH
, etc. — the full list can be found in the registry7 . However, only one of them gained widespread popularity — the PATCH
method. The reasons for this state of affairs are quite trivial: the five methods (GET
, POST
, PUT
, DELETE
, and PATCH
) are enough for almost any API.
HTTP verbs define two important characteristics of an HTTP call:
Theoretically, it is possible to use kebab-case
everywhere. However, most programming languages do not allow variable names and object fields in kebab-case
, so working with such an API would be quite inconvenient.
To wrap this up, the situation with casing is so spoiled and convoluted that there is no consistent solution to employ. In this book, we follow this rule: tokens are cased according to the common practice for the corresponding request component. If a token's position changes, the casing is changed as well. (However, we're far from recommending following this approach unconditionally. Our recommendation is rather to try to avoid increasing the entropy by choosing a solution that minimizes the probability of misunderstanding.)
-NB : Strictly speaking, JSON stands for “JavaScript Object Notation,” and in JavaScript, the default casing is camelCase
. However, we dare to say that JSON ceased to be a format bound to JavaScript long ago and is now a universal format for organizing communication between agents written in different programming languages. Employing camel_case
allows for easily moving a parameter from a query to a body, which is the most frequent case. Although the inverse solution (i.e., using camelCase
in query parameter names) is also possible.
References
+NB : Strictly speaking, JSON stands for “JavaScript Object Notation,” and in JavaScript, the default casing is camelCase
. However, we dare to say that JSON ceased to be a format bound to JavaScript long ago and is now a universal format for organizing communication between agents written in different programming languages. Employing camel_case
allows for easily moving a parameter from a query to a body, which is the most frequent case. Although the inverse solution (i.e., using camelCase
in query parameter names) is also possible.
References
Now let's discuss the specifics: what does it mean exactly to “follow the protocol's semantics” and “develop applications in accordance with the REST architectural style”? Remember, we are talking about the following principles:
Operations must be stateless
@@ -5074,7 +5074,7 @@ HTTP/1.1 200 O
-Step 2. Adding explicit user identifiers
+Step 1. Adding explicit user identifiers
NB : We used the /v1/orders?user_id
notation and not, let's say, /v1/users/{user_id}/orders
, because of two reasons:
@@ -5097,7 +5097,7 @@ HTTP/1.1 200 O
If operations with external data are unavoidable (for example, the authority making a call must be checked), the operations must be organized in a way that reduces them to checking the data integrity .
-In our example, we might get rid of unnecessary calls to service A in a different manner — by using stateless tokens, for example, employing the JWT standard1 . Then services B and C would be capable of deciphering tokens and extracting user identifiers on their own.
+In our example, we might get rid of unnecessary calls to service A in a different manner — by using stateless tokens, for example, employing the JWT standard1 . Then services B and C would be capable of deciphering tokens and extracting user identifiers on their own.
Let us take a step further and notice that the user profile rarely changes, so there is no need to retrieve it each time as we might cache it at the gateway level. To do so, we must form a cache key which is essentially the client identifier. We can do this by taking a long way:
@@ -5224,7 +5224,7 @@ Authorization: Bearer
<token>
This approach might be further enhanced by introducing granular permissions to carry out specific actions, access levels, additional ACL service calls, etc.
-
Importantly, the visible redundancy of the format ceases to exist: user_id
in the request is now not duplicated in the token payload as these identifiers carry different semantics: on which resource the operation is performed against who performs it. The two often coincide, but this coincidence is just a special case. Unfortunately, this doesn't negate the fact that it's quite easy simply to forget to implement this unobvious check in the code. This is the way.
References
+
Importantly, the visible redundancy of the format ceases to exist: user_id
in the request is now not duplicated in the token payload as these identifiers carry different semantics: on which resource the operation is performed against who performs it. The two often coincide, but this coincidence is just a special case. Unfortunately, this doesn't negate the fact that it's quite easy simply to forget to implement this unobvious check in the code. This is the way.
References
As we noted on several occasions in the previous chapters, neither the HTTP and URL standards nor REST architectural principles prescribe concrete semantics for the meaningful parts of a URL (notably, path fragments and key-value pairs in the query). The rules for organizing URLs in an HTTP API exist only to improve the API's readability and consistency from the developers' perspective . However, this doesn't mean they are unimportant. Quite the opposite: URLs in HTTP APIs are a means of describing abstraction levels and entities' responsibility areas. A well-designed API hierarchy should be reflected in a well-designed URL nomenclature.
NB : The lack of specific guidance from the specification editors naturally led to developers inventing it themselves. Many of these spontaneous practices can be found on the Internet, such as the requirement to use only nouns in URLs. They are often claimed to be a part of the standards or REST architectural principles (which they are not). Nevertheless, deliberately ignoring such self-proclaimed “best practices” is a rather risky decision for an API vendor as it increases the chances of being misunderstood.
Traditionally, the following semantics are considered to be the default:
@@ -5277,7 +5277,7 @@ X-OurCoffeeAPI-Version:
1
On the other hand, a response to a GET /v1/search
request must contain a representation of the /search
resource . Are search results a representation of a search engine? The meaning of a “search” operation is much better described as “processing the representation enclosed in the request according to the resource's own specific semantics,” which is exactly the definition of the POST
method. Additionally, how could we cache search requests? The results page is dynamically formed from a plethora of various sources, and a subsequent request with the same query might yield a different result.
In other words, with any operation that runs an algorithm rather than returns a predefined result (such as listing offers relevant to a search phrase), we will have to decide what to choose: following verb semantics or indicating side effects? Caching the results or hinting that the results are generated on the fly?
-
NB : The authors of the standard are also concerned about this dichotomy and have finally proposed the QUERY
HTTP method1 , which is basically a safe (i.e., non-modifying) version of POST
. However, we do not expect it to gain widespread adoption just as the existing SEARCH
verb2 did not.
+
NB : The authors of the standard are also concerned about this dichotomy and have finally proposed the QUERY
HTTP method1 , which is basically a safe (i.e., non-modifying) version of POST
. However, we do not expect it to gain widespread adoption just as the existing SEARCH
verb2 did not.
Unfortunately, we don't have simple answers to these questions. Within this book, we adhere to the following approach: the call signature should, first and foremost, be concise and readable. Complicating signatures for the sake of abstract concepts is undesirable. In relation to the mentioned issues, this means that:
@@ -5429,8 +5429,8 @@ then we end up with the following nomenclature of 8 URLs and 9-10 methods:
/v1/orders/search?user_id=<user_id>
to be acted upon with either GET
(for simple cases) or POST
(in more complex scenarios) to search for orders
PUT /v1/orders/{id}/archive
to archive the order
-
plus presumably a set of operations like POST /v1/orders/{id}/cancel
for executing atomic actions on entities. This is what is likely to happen in real life: the idea of CRUD as a methodology for describing typical operations applied to resources with a small set of uniform verbs quickly evolves towards a bucket of different endpoints, each covering a specific aspect of working with the entity during its lifecycle. This only proves that mnemonics are just helpful starting points; each situation requires a thorough understanding of the subject area and designing an API that fits it. However, if your task is to develop a “universal” interface that fits every kind of entity, we would strongly suggest starting with something like the ten-method nomenclature described above.
References
+
plus presumably a set of operations like POST /v1/orders/{id}/cancel
for executing atomic actions on entities. This is what is likely to happen in real life: the idea of CRUD as a methodology for describing typical operations applied to resources with a small set of uniform verbs quickly evolves towards a bucket of different endpoints, each covering a specific aspect of working with the entity during its lifecycle. This only proves that mnemonics are just helpful starting points; each situation requires a thorough understanding of the subject area and designing an API that fits it. However, if your task is to develop a “universal” interface that fits every kind of entity, we would strongly suggest starting with something like the ten-method nomenclature described above.
References
The examples of organizing HTTP APIs discussed in the previous chapters were mostly about “happy paths,” i.e., the direct path of working with an API in the absence of obstacles. It's now time to talk about the opposite case: how HTTP APIs should work with errors and how the standard and the REST architectural principles can help us.
Imagine that some actor (a client or a gateway) tries to create a new order:
POST /v1/orders?user_id=<user_id> HTTP/1.1
@@ -5488,7 +5488,7 @@ If-Match: <revision>
429 Too Many Requests
if some quotas are exceeded.
The editors of the specification are very well aware of this problem as they state that “the server SHOULD send a representation containing an explanation of the error situation, and whether it is a temporary or permanent condition.” This, however, contradicts the entire idea of a uniform machine-readable interface (and so does the idea of using arbitrary status codes). (Let us additionally emphasize that this lack of standard tools to describe business logic-bound errors is one of the reasons we consider the REST architectural style as described by Fielding in his 2008 article non-viable. The client must possess prior knowledge of error formats and how to work with them. Otherwise, it could restore its state after an error only by restarting the application.)
-NB : Not long ago, the editors of the standard proposed their own version of the JSON description specification for HTTP errors — RFC 94571 . You can use it, but keep in mind that it covers only the most basic scenario:
+NB : Not long ago, the editors of the standard proposed their own version of the JSON description specification for HTTP errors — RFC 94571 . You can use it, but keep in mind that it covers only the most basic scenario:
The error subtype is not transmitted in the metadata.
There is no distinction between a message for the user and a message for the developer.
@@ -5587,7 +5587,7 @@ Retry-After: 5
Employing a mixed approach, i.e., using status codes in accordance with their semantics to indicate an error family with additional (meta)data being passed in a specially developed format (similar to the code samples we gave above).
-Obviously, only approach #3 could be considered compliant with the standard. Let us be honest and say that the benefits of following it (especially compared to option #2a) are not very significant and only comprise better readability of logs and transparency for intermediate proxies.
References
+Obviously, only approach #3 could be considered compliant with the standard. Let us be honest and say that the benefits of following it (especially compared to option #2a) are not very significant and only comprise better readability of logs and transparency for intermediate proxies.
References
Let's summarize what was discussed in the previous chapters. To design a fine HTTP API one needs to:
Describe a happy path, i.e. draw a diagram of all HTTP calls that occur during a normal work cycle of an application.
@@ -5606,7 +5606,7 @@ Retry-After: 5
Include common headers (such as Date
, Content-Type
, Content-Encoding
, Content-Length
, Cache-Control
, Retry-After
, etc.) in the responses and generally avoid relying on clients to guess default protocol parameters correctly.
-Support the OPTIONS
method and the CORS protocol1 just in case your API needs to be accessed from a Web browser.
+Support the OPTIONS
method and the CORS protocol1 just in case your API needs to be accessed from a Web browser.
Choose a casing rule and a rule for transforming casing while moving a parameter from one part of an HTTP request to another.
@@ -5633,20 +5633,20 @@ Retry-After: 5
Familiarize yourself with at least the basics of typical vulnerabilities in HTTP APIs used by attackers, such as:
-CSRF2
-SSRF3
-HTTP Response Splitting4
-Unvalidated Redirects and Forwards5
+CSRF2
+SSRF3
+HTTP Response Splitting4
+Unvalidated Redirects and Forwards5
-and include protection against these attack vectors at the webserver software level. The OWASP community provides a good cheatsheet on the best HTTP API security practices.6
+and include protection against these attack vectors at the webserver software level. The OWASP community provides a good cheatsheet on the best HTTP API security practices.6
-In conclusion, we would like to make the following statement: building an HTTP API is relying on the common knowledge of HTTP call semantics and drawing benefits from it by leveraging various software built upon this paradigm, from client frameworks to server gateways, and developers reading and understanding API specifications. In this sense, the HTTP ecosystem provides probably the most comprehensive vocabulary, both in terms of profoundness and adoption, compared to other technologies, allowing for describing many different situations that may arise in client-server communication. While the technology is not perfect and has its flaws, for a public API vendor, it is the default choice, and opting for other technologies rather needs to be substantiated as of today.
References
+In conclusion, we would like to make the following statement: building an HTTP API is relying on the common knowledge of HTTP call semantics and drawing benefits from it by leveraging various software built upon this paradigm, from client frameworks to server gateways, and developers reading and understanding API specifications. In this sense, the HTTP ecosystem provides probably the most comprehensive vocabulary, both in terms of profoundness and adoption, compared to other technologies, allowing for describing many different situations that may arise in client-server communication. While the technology is not perfect and has its flaws, for a public API vendor, it is the default choice, and opting for other technologies rather needs to be substantiated as of today.
References
As we mentioned in the Introduction, the term “SDK” (which stands for “Software Development Kit”) lacks concrete meaning. The common understanding is that an SDK differs from an API as it provides both program interfaces and tools to work with them. This definition is hardly satisfactory as today any technology is likely to be provided with a bundled toolset.
However, there is a very specific narrow definition of an SDK: it is a client library that provides a high-level interface (usually a native one) to some underlying platform (such as a client-server API). Most often, we talk about libraries for mobile OSes or Web browsers that work on top of a general-purpose HTTP API.
Among such client SDKs, one case is of particular interest to us: those libraries that not only provide programmatic interfaces to work with an API but also offer ready-to-use visual components for developers. A classic example of such an SDK is the UI libraries provided by cartographical services. Since developing a map engine, especially a vector one, is a very complex task, maps API vendors provide both “wrappers” to their HTTP APIs (such as a search function) and visual components to work with geographical entities. The latter often include general-purpose elements (such as buttons, placemarks, context menus, etc.) that can be used independently from the main functionality of the API.
@@ -5658,21 +5658,21 @@ Retry-After: 5
To avoid being wordy, we will use the term “SDK” for the former and “UI libraries” for the latter.
NB : Strictly speaking, a UI library might either include a client-server API “wrapper” or not (i.e., just provide a “pure” API to some underlying system engine). In this Section, we will mostly talk about the first option as it is the most general case and the most challenging one in terms of API design. Most SDK development patterns we will discuss are also applicable to “pure” libraries.
Selecting a Framework for UI Component Development
-As UI is a high-level abstraction built upon OS primitives, there are specialized visual component frameworks available for almost every platform. Choosing such a framework might be regretfully challenging. For instance, in the case of the Web platform, which is both low-level and highly popular, the number of competing technologies for SDK development is beyond imagination. We could mention the most popular ones today, including React1 , Angular2 , Svelte3 , Vue.js4 , as well as those that maintain a strong presence like Bootstrap5 and Ember.6 Among these technologies, React demonstrates the most widespread adoption, still measured in single-digit percentages.7 At the same time, components written in “pure” JavaScript/CSS often receive criticism for being less convenient to use in these frameworks as each of them implements a rigid methodology. The situation with developing visual libraries for Windows is quite similar. The question of “which framework to choose for developing UI components for these platforms” regretfully has no simple answer. In fact, one will need to evaluate the markets and make decisions regarding each individual framework.
+As UI is a high-level abstraction built upon OS primitives, there are specialized visual component frameworks available for almost every platform. Choosing such a framework might be regretfully challenging. For instance, in the case of the Web platform, which is both low-level and highly popular, the number of competing technologies for SDK development is beyond imagination. We could mention the most popular ones today, including React1 , Angular2 , Svelte3 , Vue.js4 , as well as those that maintain a strong presence like Bootstrap5 and Ember.6 Among these technologies, React demonstrates the most widespread adoption, still measured in single-digit percentages.7 At the same time, components written in “pure” JavaScript/CSS often receive criticism for being less convenient to use in these frameworks as each of them implements a rigid methodology. The situation with developing visual libraries for Windows is quite similar. The question of “which framework to choose for developing UI components for these platforms” regretfully has no simple answer. In fact, one will need to evaluate the markets and make decisions regarding each individual framework.
In the case of actual mobile platforms (and MacOS), the current state of affairs is more favorable as they are more homogeneous. However, a different problem arises: modern applications typically need to support several such platforms simultaneously, which leads to code (and API nomenclatures) duplication.
-One potential solution could be using cross-platform mobile (React Native8 , Flutter9 , Xamarin10 , etc.) and desktop (JavaFX11 , QT12 , etc.) frameworks, or specialized technologies for specific tasks (such as Unity13 for game development). The inherent advantages of these technologies are faster code-writing and universalism (of both code and software engineers). The disadvantages are obvious as well: achieving maximum performance could be challenging, and many platform tools (such as debugging and profiling) will not work. As of today, we rather see a parity between these two approaches (several independent applications for each platform vs. one cross-platform application).
References
+One potential solution could be using cross-platform mobile (React Native8 , Flutter9 , Xamarin10 , etc.) and desktop (JavaFX11 , QT12 , etc.) frameworks, or specialized technologies for specific tasks (such as Unity13 for game development). The inherent advantages of these technologies are faster code-writing and universalism (of both code and software engineers). The disadvantages are obvious as well: achieving maximum performance could be challenging, and many platform tools (such as debugging and profiling) will not work. As of today, we rather see a parity between these two approaches (several independent applications for each platform vs. one cross-platform application).
References
The first question we need to clarify about SDKs (let us reiterate that we use this term to denote a native client library that allows for working with a technology-agnostic underlying client-server API) is why SDKs exist in the first place. In other words, why is using “wrappers” more convenient for frontend developers than working with the underlying API directly?
Several reasons are obvious:
@@ -5885,7 +5885,7 @@ api.subscribe (
The functionality of the underlying API
The methods of visualizing data and interacting with UI controls that are conventional for the application platform (possibly, creatively improved by our UX designers).
-These two subject areas could be far away from each other. Furthermore, the closer a UI is to representing the raw data, the less convenient it is for the user (huge forms for entering field values as a classical example1 ). If we aim to make an interface ergonomic, we need to replace “forms” with complex interfaces built atop both data and graphical primitives of the platform. Furthermore, these complex UI components will inevitably have their own inner state. This eventually leads to piling up complexities in the SDK architecture:
+These two subject areas could be far away from each other. Furthermore, the closer a UI is to representing the raw data, the less convenient it is for the user (huge forms for entering field values as a classical example1 ). If we aim to make an interface ergonomic, we need to replace “forms” with complex interfaces built atop both data and graphical primitives of the platform. Furthermore, these complex UI components will inevitably have their own inner state. This eventually leads to piling up complexities in the SDK architecture:
We have placed two buttons (to make an order and to show the coffee shop's location) plus a cancel action onto the offer view panel. These buttons may look identical and they react to the user's actions in the same way, but the way the SearchBox
component handles pressing each of them is completely different.
Imagine if we allow developers to add their own action buttons onto the panel, for which purpose we introduce a Button
class. We will soon learn that this functionality will be used to cover two diametrically opposite scenarios:
@@ -5934,7 +5934,7 @@ api.subscribe (
Formally speaking , this code is correct and does not violate any agreements. However, the readability and maintainability of this code are a catastrophe. The last place the next developer asked to change the button icon will look is the offer search function .
This functionality would appear more maintainable if no such customization opportunity was provided at all. Developers will be unhappy as they would need to implement their own search control from scratch just to replace an icon, but this implementation would be at least logical with icons defined somewhere in the rendering function.
NB : There are many other possibilities to allow developers to customize a button nested deeply within a component, such as exposing dependency injection or sub-component class factories, giving direct access to a rendered view, allowing to provide custom button layouts, etc. All of them are inherently subject to the same problem: it is complicated to consistently define the order and the priority of injections / rendering callbacks / custom layouts.
-Consistently solving all the problems listed above is unfortunately a very complex task. In the following chapters, we will discuss design patterns that allow for splitting responsibility areas between the component's sub-entities. However, it is important to understand one thing: full separation of concerns, meaning developing a functional SDK+UI that allows developers to independently overwrite the look, business logic, and UX of the components, is extremely expensive. In the best-case scenario, the nomenclature of entities will be tripled. So the universal advice is: think thrice before exposing the functionality of customizing UI components . Though the price of design mistakes in UI library APIs is typically not very high (customers rarely request a refund if button press animation is broken), a badly structured, unreadable and buggy SDK could hardly be viewed as a competitive advantage of your API.
References
+Consistently solving all the problems listed above is unfortunately a very complex task. In the following chapters, we will discuss design patterns that allow for splitting responsibility areas between the component's sub-entities. However, it is important to understand one thing: full separation of concerns, meaning developing a functional SDK+UI that allows developers to independently overwrite the look, business logic, and UX of the components, is extremely expensive. In the best-case scenario, the nomenclature of entities will be tripled. So the universal advice is: think thrice before exposing the functionality of customizing UI components . Though the price of design mistakes in UI library APIs is typically not very high (customers rarely request a refund if button press animation is broken), a badly structured, unreadable and buggy SDK could hardly be viewed as a competitive advantage of your API.
References
Let's transition to a more substantive conversation and try to understand why the requirement to allow the replacement of a component's subsystems with alternative implementations leads to a dramatic increase in interface complexity. We will continue studying the SearchBox
component from the previous chapter. Allow us to remind the reader of the factors that complicate the design of APIs for visual components:
Coupling heterogeneous functionality (such as business logic, appearance styling, and behavior) into a single entity
@@ -6482,7 +6482,7 @@ api.subscribe (
Finally, SearchBox
doesn't interact with either of them and only provides a context, methods to change it, and the corresponding notifications.
-By making these reductions, in fact, we end up with a setup that follows the “Model-View-Controller” (MVC) methodology1 . OfferList
and OfferPanel
(also, the code that displays the input field) constitute a view that the user observes and interacts with. Composer
is a controller that listens to the view 's events and modifies a model (SearchBox
itself).
+By making these reductions, in fact, we end up with a setup that follows the “Model-View-Controller” (MVC) methodology1 . OfferList
and OfferPanel
(also, the code that displays the input field) constitute a view that the user observes and interacts with. Composer
is a controller that listens to the view 's events and modifies a model (SearchBox
itself).
NB : to follow the letter of the paradigm, we must separate the model , which will be responsible only for the data, from SearchBox
itself. We leave this exercise to the reader.
MVC entities interaction chart
@@ -6514,15 +6514,15 @@ api.subscribe (
This rigidity, however, bears disadvantages as well. If we try to fully define the component's state, we must include such technicalities as, let's say, all animations being executed (and even the current percentages of execution). Therefore, a model will include all data of all abstraction levels for both hierarchies (semantic and visual) and also the calculated option values. In our example, this means that the model will store, for example, the currentSelectedOffer
field for OfferPanel
to use, the list of buttons in the panel, and even the calculated icon URLs for those buttons.
Such a full model poses a problem not only semantically and theoretically (as it mixes up heterogeneous data in one entity) but also very practically. Serializing such models will be bound to a specific API or application version (as they store all the technical fields, including those not exposed publicly in the API). Changing subcomponent implementation will result in breaking backward compatibility as old links and cached state will be unrestorable (or we will have to maintain a compatibility level to interpret serialized models from past versions).
Another ideological problem is organizing nested controllers. If there are subordinate subcomponents in the system, all the problems that an MV* approach solved return at a higher level: we have to allow nested controllers either to modify a global model or to call parent controllers. Both solutions imply strong coupling and require exquisite interface design skill; otherwise reusing components will be very hard.
-If we take a closer look at modern UI libraries that claim to employ MV* paradigms, we will learn they employ it quite loosely. Usually, only the main principle that a model defines UI and can only be modified through controllers is adopted. Nested components usually have their own models (in most cases, comprising a subset of the parent model enriched with the component's own state), and the global model contains only a limited number of fields. This approach is implemented in many modern UI frameworks, including those that claim they have nothing to do with MV* paradigms (React, for instance2 3 ).
-All these problems of the MVC paradigm were highlighted by Martin Fowler in his “GUI Architectures” essay.4 The proposed solution is the “Model-View-Presenter ” framework, in which the controller entity is replaced with a presenter . The responsibility of the presenter is not only translating events, but preparing data for views as well. This allows for full separation of abstraction levels (a model now stores only semantic data while a presenter transforms it into low-level parameters that define UI look; the set of these parameters is called the “Application Model” or “Presentation Model” in Fowler's text).
+If we take a closer look at modern UI libraries that claim to employ MV* paradigms, we will learn they employ it quite loosely. Usually, only the main principle that a model defines UI and can only be modified through controllers is adopted. Nested components usually have their own models (in most cases, comprising a subset of the parent model enriched with the component's own state), and the global model contains only a limited number of fields. This approach is implemented in many modern UI frameworks, including those that claim they have nothing to do with MV* paradigms (React, for instance2 3 ).
+All these problems of the MVC paradigm were highlighted by Martin Fowler in his “GUI Architectures” essay.4 The proposed solution is the “Model-View-Presenter ” framework, in which the controller entity is replaced with a presenter . The responsibility of the presenter is not only translating events, but preparing data for views as well. This allows for full separation of abstraction levels (a model now stores only semantic data while a presenter transforms it into low-level parameters that define UI look; the set of these parameters is called the “Application Model” or “Presentation Model” in Fowler's text).
MVP entities interaction chart
Fowler's paradigm closely resembles the Composer
concept we discussed in the previous chapter with one notable deviation. In MVP, a presenter is stateless (with possible exceptions of caches and closures) and it only deduces the data needed by views from the model data. If some low-level property needs to be manipulated, such as text color, the model needs to be extended in a manner that allows the presenter to calculate text color based on some high-level model data field. This concept significantly narrows the capability to replace subcomponents with alternate implementations.
-NB : let us clarify that the author of this book is not proposing Composer
as an alternative MV* methodology. The message in the previous chapter is that complex scenarios of decomposing UI components are only solved with artificially-introduced “bridges” of additional abstraction layers. How this bridge is called and what rules it brings are not as important.
References
+NB : let us clarify that the author of this book is not proposing Composer
as an alternative MV* methodology. The message in the previous chapter is that complex scenarios of decomposing UI components are only solved with artificially-introduced “bridges” of additional abstraction layers. How this bridge is called and what rules it brings are not as important.
References
Another method of reducing the complexity of building “bridges” that connect different subject areas in one component is to eliminate one of them. For instance, business logic could be removed: components might be entirely abstract, and the translation of UI events into useful actions hidden beyond the developer's control.
In this paradigm, the offer search code would look like this:
class SearchBox {
@@ -6698,10 +6698,10 @@ api.subscribe (
With this approach, we can implement not only locks, but also various scenarios to flexibly manage them. Let's add data regarding the acquirer in the lock function:
lock = await this .acquireLock (
'offerFullView' , '10s' , {
-
-
- reason : 'userSelectOffer' ,
- offer
+
+
+ reason : 'userSelectOffer' ,
+ offer
}
);
@@ -6729,7 +6729,82 @@ api.subscribe (
Asynchronously update the view once the data is loaded.
However, this doesn't change the problem definition: we still need a conflict resolution policy if another actor attempts to open the panel while the data is still loading, and for this we need shared resources and mechanisms for gaining exclusive access to them.
-Regrettably, in modern frontend development, such techniques involving taking control over UI elements while data is loading or animations are being performed are rarely used. Such asynchronous operations are considered fast enough to not be concerned about access collisions. However, if latencies are high (e.g., complex or multi-staged requests or animations occur), neglecting access management could be a major UX problem.
+Regrettably, in modern frontend development, such techniques involving taking control over UI elements while data is loading or animations are being performed are rarely used. Such asynchronous operations are considered fast enough to not be concerned about access collisions. However, if latencies are high (e.g., complex or multi-staged requests or animations occur), neglecting access management could be a major UX problem.
+Let's revisit one of the problems we outlined in the “Problems of Introducing UI Components ” chapter: the existence of multiple inheritance lines complicates customizing UI components as it implies that component properties could be inherited from any such line.
+For example, imagine we have a button that can borrow its iconUrl
property from two sources — from data [in our case, from offer data originated in the offer search results] and the component's options:
+class Button {
+ static DEFAULT_OPTIONS = {
+ …
+ iconUrl : <default icon >
+ }
+
+ constructor (data, options) {
+ this.data = data;
+ // Overriding default options
+ // with custom values
+ this.options = extend(
+ Button.DEFAULT_OPTIONS,
+ options
+ )
+ }
+
+ render() {
+ …
+ this.iconElement.src =
+ this.data.iconUrl ||
+ this.options.iconUrl
+ }
+}
+
+It is also plausible to suggest that the iconUrl
property in both hierarchies is inherited from some parent entities:
+
+The default option values could be defined in the base class which Button
extends.
+The data for the button could be hierarchical (for example, if we decide to group offers in the same coffee shop chain, and the icon is to be taken from the parent group).
+To facilitate customizing the visual style of the components, we could allow overriding icons in all SDK buttons.
+
+In this situation, a question of priorities arises: if a property is defined in several hierarchies (let's say, in the offer data and in the default options), how should the priorities be set to select one of them?
+The straightforward approach to tackling this issue is to prohibit any inheritance and force developers to explicitly set the properties they need. In our example, it would mean that developers will need to write something like this:
+const button = new Button (data);
+if (data.createOrderButtonIconUrl ) {
+ button.view .iconUrl =
+ data.createOrderButtonIconUrl ;
+} else if (data.parentCategory .iconUrl ) {
+ button.view .iconUrl =
+ data.parentCategory .iconUrl ;
+}
+
+The main advantage of this approach is obvious: developers implement the logic they need themselves. The disadvantages are also apparent: first, developers will need to write excessive and often copy-pasted code (“boilerplate”); second, they will soon become confused about which rules are in place and why.
+A slightly more complex solution is allowing inheritance but rigidly fixing priorities (let's say the value set in the component's options always takes precedence over the one set in the data, and they both are more important than any inherited value). However, for any complex API, the result will be the same: if developers need a different order of resolving priorities, they will write code similar to the one above.
+An alternative approach is to expose the possibility of defining rules for how exactly the icon is resolved for a specific button, either declaratively or imperatively:
+
+
+{
+ "button.checkout.iconUrl" : "@data.iconUrl"
+}
+
+
+
+api.options .addRule (
+ 'button.checkout.iconUrl' ,
+ (data, options ) => data.iconUrl
+)
+
+The most coherent implementation of this approach is the CSS technology.1 We are not actually proposing using a full CSS rule engine in component libraries (because of its overwhelming complexity and excessiveness for most cases), but we are cautiously drawing the reader's attention to the fact that supporting some subset of CSS-like rules could significantly simplify the task of customizing UI components.
+Calculated Values
+It is important not only to provide a mechanism for setting rules to determine how values are resolved but also to allow obtaining the value that was actually used. To achieve this, we need to distinguish between the concept of a set value and a computed value:
+
+button.view .width = '100%' ;
+
+
+button.view .computedStyle .width ;
+
+It is also a good practice to provide an event for changes in the computed value of such a calculated property.
References
+In the previous eight chapters, we aimed to convey two important observations:
+
+Developing a high-quality UI library is a very complex engineering task.
+This task cannot be mechanically reduced to auto-generating SDKs based on a specification or data model.
+
+Looking back at what was written, we cannot confidently claim that we found the best examples and the clearest wording for such a complex subject area. However, we hope that we have helped make the reader's life and the lives of their users a bit easier.
There are two important statements regarding APIs viewed as products.
@@ -6960,7 +7035,7 @@ api.subscribe (
The next simplification step is providing services for code generation. In this service, developers can choose from pre-built integration templates, customize option values, and obtain a ready-to-use piece of code that can be easily copied and pasted into their application code (and can be further customized by adding some level 1-3 code). This approach is sometimes called “point-and-click programming.” [In the case of our coffee API, an example of such a service might have a form or screen editor for developers to place UI elements and generate the working application code or a console script to automatically produce application boilerplate.]
-Finally, this approach can be simplified even further if the service generates not just code but a ready-to-use component / widget / frame and a one-liner to integrate it. [For example, if we allow embedding an iframe that handles the entire coffee ordering process directly on the partner's website, or describe the rules of forming a “deep link1 ” to our service.]
+Finally, this approach can be simplified even further if the service generates not just code but a ready-to-use component / widget / frame and a one-liner to integrate it. [For example, if we allow embedding an iframe that handles the entire coffee ordering process directly on the partner's website, or describe the rules of forming a “deep link1 ” to our service.]
Ultimately, we will end up with the concept of a meta-API where these high-level components have their own API built on top of the basic API.
@@ -6973,7 +7048,7 @@ api.subscribe (
Finally, ready-to-use components and widgets are under your full control, and you can experiment with the functionality exposed through them in partners' applications as if it were your own services. (However, this doesn't automatically mean that you can draw profits from having this control. For example, if you allow hotlinking pictures by their direct URL, your control over this integration is rather negligible, so it's generally better to provide integration options that allow for more control over the functionality in partners' applications.)
NB : While developing a “vertical” lineup of APIs, it is crucial to follow the principles discussed in the “On the Waterline of the Iceberg ” chapter. You will only be able to manipulate the content and behavior of a widget if developers cannot “escape the sandbox,” meaning they do not have direct access to low-level objects encapsulated within the widget.
-In general, your aim should be to have each partner use the API services in a manner that maximizes your profit as an API vendor. When a partner only needs a typical solution, you would benefit from making them use widgets as they are under your direct control. This will help ease the API version fragmentation problem and allow for experimentation to reach your KPIs. When the partner possesses expertise in the subject area and develops a unique service on top of your API, you would benefit from allowing full freedom in customizing the integration. This way, they can cover specific market niches and enjoy the advantage of offering more flexibility compared to services using competing APIs.
References
+In general, your aim should be to have each partner use the API services in a manner that maximizes your profit as an API vendor. When a partner only needs a typical solution, you would benefit from making them use widgets as they are under your direct control. This will help ease the API version fragmentation problem and allow for experimentation to reach your KPIs. When the partner possesses expertise in the subject area and develops a unique service on top of your API, you would benefit from allowing full freedom in customizing the integration. This way, they can cover specific market niches and enjoy the advantage of offering more flexibility compared to services using competing APIs.
References
As we described in the previous chapters, there are many API monetization models, both direct and indirect. Importantly, most of them are fully or conditionally free for partners, and the direct-to-indirect benefits ratio tends to change during the API lifecycle. That naturally leads us to the question of how exactly shall we measure the API's success and what goals are to be set for the product team.
Of course, the most explicit metric is money: if your API is monetized directly or attracts visitors to a monetized service, the rest of the chapter will be of little interest to you, maybe just as a case study. If, however, the contribution of the API to the company's income cannot be simply measured, you have to stick to other, synthetic, indicators.
The obvious key performance indicator (KPI) #1 is the number of end users and the number of integrations (i.e., partners using the API). Normally, they are in some sense a business health barometer: if there is a normal competitive situation among the API suppliers, and all of them are more or less in the same position, then the figure of how many developers (and consequently, how many end users) are using the API is the main metric of success for the API product.
diff --git a/docs/API.en.pdf b/docs/API.en.pdf
index 173f244..f5aae0c 100644
Binary files a/docs/API.en.pdf and b/docs/API.en.pdf differ
diff --git a/docs/API.en.sample.epub b/docs/API.en.sample.epub
new file mode 100644
index 0000000..0687f67
Binary files /dev/null and b/docs/API.en.sample.epub differ
diff --git a/docs/API.ru.epub b/docs/API.ru.epub
index 23ffe46..638bdd5 100644
Binary files a/docs/API.ru.epub and b/docs/API.ru.epub differ
diff --git a/docs/API.ru.html b/docs/API.ru.html
index 44b13e1..839fe34 100644
--- a/docs/API.ru.html
+++ b/docs/API.ru.html
@@ -673,13 +673,13 @@ ul.references li p a.back-anchor {
Ссылка:
-
+
Сергей Константинов API
-
Поделиться: facebook · · linkedin · reddit
Содержание
Поделиться: facebook · · linkedin · reddit
Содержание
§
@@ -2708,7 +2708,7 @@ X-Idempotency-Token: <токен>
…
}
-Т.к. заказы создаются намного реже, нежели читаются, мы можем существенно повысить производительность системы, если откажемся от гарантии возврата всегда самого актуального состояния ресурса из операции на чтение. Версионирование же поможет нам избежать проблем: создать заказ, не получив актуальной версии, невозможно. Фактически мы пришли к модели событийной консистентности1 (т.н. «согласованность в конечном счёте»): клиент сможет выполнить свой запрос когда-нибудь , когда получит, наконец, актуальные данные. В самом деле, согласованность в конечном счёте — скорее норма жизни для современных микросервисных архитектур, в которой может оказаться очень сложно как раз добиться обратного, т.е. строгой консистентности.
+Т.к. заказы создаются намного реже, нежели читаются, мы можем существенно повысить производительность системы, если откажемся от гарантии возврата всегда самого актуального состояния ресурса из операции на чтение. Версионирование же поможет нам избежать проблем: создать заказ, не получив актуальной версии, невозможно. Фактически мы пришли к модели событийной консистентности1 (т.н. «согласованность в конечном счёте»): клиент сможет выполнить свой запрос когда-нибудь , когда получит, наконец, актуальные данные. В самом деле, согласованность в конечном счёте — скорее норма жизни для современных микросервисных архитектур, в которой может оказаться очень сложно как раз добиться обратного, т.е. строгой консистентности.
NB : на всякий случай уточним, что выбирать подходящий подход вы можете только в случае разработки новых API. Если вы уже предоставляете эндпойнт, реализующий какую-то модель консистентности, вы не можете понизить её уровень (в частности, сменить строгую консистентность на слабую), даже если вы никогда не документировали текущее поведение явно (мы обсудим это требование детальнее в главе «О ватерлинии айсберга» раздела «Обратная совместимость»).
Однако, выбор слабой консистентности вместо сильной влечёт за собой и другие проблемы. Да, мы можем потребовать от партнёров дождаться получения последнего актуального состояния ресурса перед внесением изменений. Но очень неочевидно (и в самом деле неудобно) требовать от партнёров быть готовыми к тому, что они должны дождаться появления в том числе и тех изменений, которые сами же внесли.
@@ -2720,7 +2720,7 @@ X-Idempotency-Token: <токен>
Если мы не гарантируем сильную консистентность, то второй вызов может запросто вернуть пустой результат, ведь при чтении из реплики новый заказ мог просто до неё ещё не дойти.
-Важный паттерн, который поможет в этой ситуации — это имплементация модели «read-your-writes»2 , а именно гарантии, что клиент всегда «видит» те изменения, которые сам же и внёс. Поднять уровень слабой консистентности до read-your-writes можно, если предложить клиенту самому передать токен, описывающий его последние изменения.
+Важный паттерн, который поможет в этой ситуации — это имплементация модели «read-your-writes»2 , а именно гарантии, что клиент всегда «видит» те изменения, которые сам же и внёс. Поднять уровень слабой консистентности до read-your-writes можно, если предложить клиенту самому передать токен, описывающий его последние изменения.
let order = await api
.createOrder (…);
let pendingOrders = await api.
@@ -2781,8 +2781,8 @@ X-Idempotency-Token: <токен>
Математически вероятность получения ошибки выражается довольно просто: она равна отношению периода времени, требуемого для получения актуального состояния к типичному периоду времени, за который пользователь перезапускает приложение и повторяет заказ. (Следует, правда, отметить, что клиентское приложение может быть реализовано так, что даст вам ещё меньше времени, если оно пытается повторить несозданный заказ автоматически при запуске). Если первое зависит от технических характеристик системы (в частности, лага синхронизации, т.е. задержки репликации между мастером и копиями на чтение). А вот второе зависит от того, какого рода клиент выполняет операцию.
Если мы говорим о приложения для конечного пользователя, то типично время перезапуска измеряется для них в секундах, что в норме не должно превышать суммарного лага синхронизации — таким образом, клиентские ошибки будут возникать только в случае проблем с репликацией данных / ненадежной сети / перегрузки сервера.
Однако если мы говорим не о клиентских, а о серверных приложениях, здесь ситуация совершенно иная: если сервер решает повторить запрос (например, потому, что процесс был убит супервизором), он сделает это условно моментально — задержка может составлять миллисекунды. И в этом случае фон ошибок создания заказа будет достаточно значительным.
-Таким образом, возвращать по умолчанию событийно-консистентные данные вы можете, если готовы мириться с фоном ошибок или если вы можете обеспечить задержку получения актуального состояния много меньшую, чем время перезапуска приложения на целевой платформе.
Примечания
+Таким образом, возвращать по умолчанию событийно-консистентные данные вы можете, если готовы мириться с фоном ошибок или если вы можете обеспечить задержку получения актуального состояния много меньшую, чем время перезапуска приложения на целевой платформе.
Примечания
Продолжим рассматривать предыдущий пример. Пусть на старте приложение получает какое-то состояние системы, возможно, не самое актуальное. От чего ещё зависит вероятность коллизий и как мы можем её снизить?
Напомним, что вероятность эта равна отношению периода времени, требуемого для получения актуального состояния к типичному периоду времени, за который пользователь перезапускает приложение и повторяет заказ. Повлиять на знаменатель этой дроби мы практически не можем (если только не будем преднамеренно вносить задержку инициализации API, что мы всё же считаем крайней мерой). Обратимся теперь к числителю.
Наш сценарий использования, напомним, выглядит так:
@@ -2827,7 +2827,7 @@ X-Idempotency-Token: <токен>
организация ссылок на результаты операции и их кэширование (предполагается, что, если клиенту необходимо снова прочитать результат операции или же поделиться им с другим агентом, он может использовать для этого идентификатор задания);
обеспечение идемпотентности операций (для этого необходимо ввести подтверждение задания, и мы фактически получим схему с черновиками операции, описанную в главе «Описание конечных интерфейсов» );
-нативное же обеспечение устойчивости к временному всплеску нагрузки на сервис — новые задачи встают в очередь (возможно, приоритизированную), фактически имплементируя «маркерное ведро»1 ;
+нативное же обеспечение устойчивости к временному всплеску нагрузки на сервис — новые задачи встают в очередь (возможно, приоритизированную), фактически имплементируя «маркерное ведро»1 ;
организация взаимодействия в тех случаях, когда время исполнения операции превышает разумные значения (в случае сетевых API — типичное время срабатывания сетевых таймаутов, т.е. десятки секунд) либо является непредсказуемым.
Кроме того, асинхронное взаимодействие удобнее с точки зрения развития API в будущем: устройство системы, обрабатывающей такие запросы, может меняться в сторону усложнения и удлинения конвейера исполнения задачи, в то время как синхронным функциям придётся укладываться в разумные временные рамки, чтобы оставаться синхронными — что, конечно, ограничивает возможности рефакторинга внутренних механик.
@@ -2873,7 +2873,7 @@ X-Idempotency-Token: <токен>
status: "new"
}]} */
-NB : отметим также, что в формате асинхронного взаимодействия можно передавать не только бинарный статус (выполнено задание или нет), но и прогресс выполнения в процентах, если это возможно.
Примечания
+NB : отметим также, что в формате асинхронного взаимодействия можно передавать не только бинарный статус (выполнено задание или нет), но и прогресс выполнения в процентах, если это возможно.
Примечания
В предыдущей главе мы пришли вот к такому интерфейсу, позволяющему минимизировать коллизии при создании заказов:
let pendingOrders = await api
.getOngoingOrders ();
@@ -3059,7 +3059,7 @@ X-Idempotency-Token: <токен>
Другим способом организации такого перебора может быть дата создания записи, но этот способ чуть сложнее в имплементации:
дата создания двух записей может полностью совпадать, особенно если записи могут массово генерироваться программно; в худшем случае может получиться так, что в один момент времени было создано больше записей, чем максимальный лимит их извлечения, и тогда часть записей вообще нельзя будет перебрать;
-если хранилище данных поддерживает распределённую запись, то может оказаться, что более новая запись имеет чуть меньшую дату создания, нежели предыдущая известная (поскольку часы на разных виртуальных машинах могут идти чуть по-разному, и добиться хотя бы микросекундной точности крайне сложно1 , т.е. нарушится требование монотонности по признаку даты; если использование такого хранилища не имеет альтернативы, необходимо выбрать одно из двух зол:
+ если хранилище данных поддерживает распределённую запись, то может оказаться, что более новая запись имеет чуть меньшую дату создания, нежели предыдущая известная (поскольку часы на разных виртуальных машинах могут идти чуть по-разному, и добиться хотя бы микросекундной точности крайне сложно1 , т.е. нарушится требование монотонности по признаку даты; если использование такого хранилища не имеет альтернативы, необходимо выбрать одно из двух зол:
внести рукотворные задержки, т.е. возвращать в API только элементы, созданные более чем N секунд назад — так, чтобы N было заведомо больше неравномерности хода часов (эта техника может использоваться и в тех случаях, когда список формируется асинхронно) — однако надо иметь в виду, что это решение вероятностное и всегда есть шанс отдачи неверных данных в случае проблем с синхронизацией на бэкенде;
описать нестабильность порядка новых элементов списка в документации и переложить решение этой проблемы на партнёров.
@@ -3170,7 +3170,7 @@ X-Idempotency-Token: <токен>
}
События иммутабельны, и их список только пополняется, следовательно, организовать перебор этого списка вполне возможно. Да, событие — это не то же самое, что и сам заказ: к моменту прочтения партнёром события, заказ уже давно может изменить статус. Но, тем не менее, мы предоставили возможность перебрать все новые заказы, пусть и не самым оптимальным образом.
-NB : в вышеприведённых фрагментах кода мы опустили метаданные ответа — такие как общее число элементов в списке, флаг типа has_more_items
для индикации необходимости продолжить перебор и т.д. Хотя эти метаданные необязательны (клиент узнает размер списка, когда переберёт его полностью), их наличие повышает удобство работы с API для разработчиков, и мы рекомендуем их добавлять.
Примечания
+NB : в вышеприведённых фрагментах кода мы опустили метаданные ответа — такие как общее число элементов в списке, флаг типа has_more_items
для индикации необходимости продолжить перебор и т.д. Хотя эти метаданные необязательны (клиент узнает размер списка, когда переберёт его полностью), их наличие повышает удобство работы с API для разработчиков, и мы рекомендуем их добавлять.
Примечания
В предыдущей главе мы рассмотрели следующий кейс: партнёр получает информацию о новых событиях, произошедших в системе, периодически опрашивая эндпойнт, поддерживающий отдачу упорядоченных списков.
GET /v1/orders/created-history↵
?older_than=<item_id> &limit=<limit>
@@ -3183,29 +3183,29 @@ X-Idempotency-Token: <токен>
}, …]
}
-Подобный паттерн (известный как «поллинг»1 ) — наиболее часто встречающийся способ организации двунаправленной связи в API, когда партнёру требуется не только отправлять какие-то данные на сервер, но и получать оповещения от сервера об изменении какого-то состояния.
+Подобный паттерн (известный как «поллинг»1 ) — наиболее часто встречающийся способ организации двунаправленной связи в API, когда партнёру требуется не только отправлять какие-то данные на сервер, но и получать оповещения от сервера об изменении какого-то состояния.
При всей простоте, поллинг всегда заставляет искать компромисс между отзывчивостью, производительностью и пропускной способностью системы:
чем длиннее интервал между последовательными запросами, тем больше будет задержка между изменением состояния на сервере и получением информации об этом на клиенте, и тем потенциально большим будет объём данных, которые необходимо будет передать за одну итерацию;
с другой стороны, чем этот интервал короче, чем большее количество запросов будет совершаться зря, т.к. никаких изменений в системе за прошедшее время не произошло.
-Иными словами, поллинг всегда создаёт какой-то фоновый трафик в системе, но никогда не гарантирует максимальной отзывчивости. Иногда эту проблему решают с помощью «долгого поллинга» (long polling) (2 ) — т.е. целенаправленно замедляют отдачу сервером ответа на длительное (секунды, десятки секунд) время до тех пор, пока на сервере не появится сообщение для передачи — однако мы не рекомендуем использовать этот подход в современных системах из-за связанных технических проблем (в частности, в условиях ненадёжной сети у клиента нет способа понять, что соединение на самом деле потеряно, и нужно отправить новый запрос, а не ожидать ответа на текущий).
+Иными словами, поллинг всегда создаёт какой-то фоновый трафик в системе, но никогда не гарантирует максимальной отзывчивости. Иногда эту проблему решают с помощью «долгого поллинга» (long polling) (2 ) — т.е. целенаправленно замедляют отдачу сервером ответа на длительное (секунды, десятки секунд) время до тех пор, пока на сервере не появится сообщение для передачи — однако мы не рекомендуем использовать этот подход в современных системах из-за связанных технических проблем (в частности, в условиях ненадёжной сети у клиента нет способа понять, что соединение на самом деле потеряно, и нужно отправить новый запрос, а не ожидать ответа на текущий).
Если оказывается, что обычного поллинга для решения пользовательских задач недостаточно, то можно перейти к обратной модели (push ): сервер сам сообщает клиенту, что в системе произошли изменения.
Хотя и проблема, и способы её решения выглядят похоже, в настоящий момент применяются совершенно разные технологии для доставки сообщений от бэкенда к бэкенду и от бэкенда к клиентскому устройству.
Доставка сообщений на клиентское устройство
Поскольку разнообразные мобильные платформы и «умные устройства» (Internet of Things, IoT) сейчас составляют значительную долю всех клиентских устройств, на технологии взаимного обмена данных между сервером и конечным пользователем накладываются значительные ограничения с точки зрения экономии заряда батареи (и отчасти трафика). Многие производители платформ и устройств следят за потребляемыми приложением ресурсами, и могут отправлять приложение в фон или вовсе закрывать открытые соединения. В такой ситуации частый поллинг стоит применять только в активных фазах работы приложения (т.е. когда пользователь непосредственно взаимодействует с UI) либо если приложение работает в контролируемой среде (например, используется сотрудниками компании-партнера непосредственно в работе, и может быть добавлено в системные исключения).
Альтернатив поллингу на данный момент можно предложить три:
-Самый очевидный вариант — использование технологий, позволяющих передавать по одному соединению сообщения в обе стороны. Наиболее известной из таких технологий является WebSockets3 . Иногда для организации полнодуплексного соединения применяется Server Push, предусмотренный протоколом HTTP/24 , однако надо отметить, что формально спецификация не предусматривает такого использования. Также существует протокол WebRTC5 , но он, в основном, используется для обмена медиа-данными между клиентами, редко для клиент-серверного взаимодействия.
-Несмотря на то, что идея в целом выглядит достаточно простой и привлекательной, в реальности её использование довольно ограничено. Поддержки инициирования сервером отправки сообщения обратно на клиент практически нет в популярном серверном ПО и фреймворках (gRPC поддерживает потоки сообщений с сервера6 , но их всё равно должен инициировать клиент; использование потоков для пересылки сообщений по мере их возникновения — то же самое использование HTTP/2 Server Push в обход спецификации, что, фактически, работает как тот же самый long polling, только чуть более современный), и существующие стандарты спецификаций API также не поддерживают такой обмен данными: WebSockets является низкоуровневым протоколом, и формат взаимодействия придётся разработать самостоятельно.
+Самый очевидный вариант — использование технологий, позволяющих передавать по одному соединению сообщения в обе стороны. Наиболее известной из таких технологий является WebSockets3 . Иногда для организации полнодуплексного соединения применяется Server Push, предусмотренный протоколом HTTP/24 , однако надо отметить, что формально спецификация не предусматривает такого использования. Также существует протокол WebRTC5 , но он, в основном, используется для обмена медиа-данными между клиентами, редко для клиент-серверного взаимодействия.
+Несмотря на то, что идея в целом выглядит достаточно простой и привлекательной, в реальности её использование довольно ограничено. Поддержки инициирования сервером отправки сообщения обратно на клиент практически нет в популярном серверном ПО и фреймворках (gRPC поддерживает потоки сообщений с сервера6 , но их всё равно должен инициировать клиент; использование потоков для пересылки сообщений по мере их возникновения — то же самое использование HTTP/2 Server Push в обход спецификации, что, фактически, работает как тот же самый long polling, только чуть более современный), и существующие стандарты спецификаций API также не поддерживают такой обмен данными: WebSockets является низкоуровневым протоколом, и формат взаимодействия придётся разработать самостоятельно.
Дуплексные соединения по-прежнему страдают от ненадёжной сети и требуют дополнительных ухищрений для того, чтобы отличить сетевую проблему от отсутствия новых сообщений. Всё это приводит к тому, что данная технология используется в основном веб-приложениями.
-Вместо дуплексных соединений можно использовать два раздельных канала — один для отправки сообщений на сервер, другой для получения сообщений с сервера. Наиболее популярной технологией такого рода является MQTT7 . Хотя эта технология считается максимально эффективной в силу использования низкоуровневых протоколов, её достоинства порождают и её недостатки:
+Вместо дуплексных соединений можно использовать два раздельных канала — один для отправки сообщений на сервер, другой для получения сообщений с сервера. Наиболее популярной технологией такого рода является MQTT7 . Хотя эта технология считается максимально эффективной в силу использования низкоуровневых протоколов, её достоинства порождают и её недостатки:
технология в первую очередь предназначена для имплементации паттерна pub/sub и ценна наличием соответствующего серверного ПО (MQTT Broker); применить её для других задач, особенно для двунаправленного обмена данными, может быть сложно;
низкоуровневый протокол диктует необходимость разработки собственного формата данных.
-Существует также веб-стандарт отправки серверных сообщений Server-Sent Events8 (SSE). Однако по сравнению с WebSocket он менее функциональный (только текстовые данные, однонаправленный поток сообщений) и поэтому используется редко.
+Существует также веб-стандарт отправки серверных сообщений Server-Sent Events8 (SSE). Однако по сравнению с WebSocket он менее функциональный (только текстовые данные, однонаправленный поток сообщений) и поэтому используется редко.
Одна из неприятных особенностей технологии типа long polling / WebSocket / SSE / MQTT — необходимость поддерживать открытое соединение между клиентом и сервером, что для мобильных приложений может быть проблемой с точки зрения производительности и энергопотребления. Один из вариантов решения этой проблемы — делегирование отправки уведомлений стороннему сервису (самым популярным выбором на сегодня является Firebase Cloud Messaging от Google), который в свою очередь доставит уведомление через встроенные механизмы платформы. Использование встроенных в платформу сервисов получения уведомлений снимает с разработчика головную боль по написанию кода, поддерживающего открытое соединение, и снижает риски неполучения сообщения. Недостатками third-party серверов сообщений является необходимость платить за них и ограничения на размер сообщения.
Кроме того, отправка push-уведомлений на устройство конечного пользователя страдает от одной большой проблемы: процент успешной доставки уведомлений никогда не равен 100; потери сообщений могут достигать десятков процентов. С учётом ограничений на размер контента, скорее правильно говорить не о push-модели, а о комбинированной: приложение продолжает периодически опрашивать сервер, а пуши являются триггером для внеочередного опроса. (На самом деле, это соображение в той или иной мере применимо к любой технологии доставки сообщений на клиент. Низкоуровневые протоколы предоставляют больше возможностей управлять гарантиями доставки, но, с учётом ситуации с принудительным закрытием соединений системой, иметь в качестве страховки низкочастотный поллинг в приложении почти никогда не бывает лишним.)
@@ -3233,14 +3233,14 @@ X-Idempotency-Token: <токен>
Важно, что в любом случае должен существовать формальный контракт (очень желательно — в виде спецификации) на форматы запросов и ответов эндпойнта-webhook-а и возникающие ошибки.
-Так как webhook-и представляют собой обратный канал взаимодействия, для него придётся разработать отдельный способ авторизации — это партнёр должен проверить, что запрос исходит от нашего бэкенда, а не наоборот. Мы повторяем здесь настоятельную рекомендацию не изобретать безопасность и использовать существующие стандартные механизмы, например, mTLS9 , хотя в реальном мире с большой долей вероятности придётся использовать архаичные техники типа фиксации IP-адреса вызывающего сервера.
+Так как webhook-и представляют собой обратный канал взаимодействия, для него придётся разработать отдельный способ авторизации — это партнёр должен проверить, что запрос исходит от нашего бэкенда, а не наоборот. Мы повторяем здесь настоятельную рекомендацию не изобретать безопасность и использовать существующие стандартные механизмы, например, mTLS9 , хотя в реальном мире с большой долей вероятности придётся использовать архаичные техники типа фиксации IP-адреса вызывающего сервера.
Так как callback-эндпойнт разрабатывается партнёром, его URL нам априори неизвестен. Должен существовать интерфейс (возможно, в виде кабинета партнёра) для задания URL webhook-а (и публичных ключей авторизации).
Важно . К операции задания адреса callback-а нужно подходить с максимально возможной серьёзностью (очень желательно требовать второй фактор авторизации для подтверждения этой операции), поскольку, получив доступ к такой функциональности, злоумышленник может совершить множество весьма неприятных атак:
если указать в качестве приёмника сторонний URL, можно получить доступ к потоку всех заказов партнёра и при этом вызвать перебои в его работе;
такая уязвимость может также эксплуатироваться с целью организации DoS-атаки на сторонние сервисы;
-если указать в качестве webhook-а URL интранет-сервисов компании-провайдера API, можно осуществить SSRF-атаку10 на инфраструктуру самой компании.
+если указать в качестве webhook-а URL интранет-сервисов компании-провайдера API, можно осуществить SSRF-атаку10 на инфраструктуру самой компании.
Типичные проблемы интеграции через webhook
Двунаправленные интеграции (и клиентские, и серверные — хотя последние в большей степени) несут в себе очень неприятные риски для провайдера API. Если в общем случае качество работы API зависит в первую очередь от самого разработчика API, то в случае обратных вызовов всё в точности наоборот: качество работы интеграции напрямую зависит от того, как код webhook-эндпойнта написан партнёром. Мы можем столкнуться здесь с самыми различными видами проблем в партнёрском коде:
@@ -3259,7 +3259,7 @@ X-Idempotency-Token: <токен>
Помогите партнёру написать правильный код, зафиксировав в документации неочевидные моменты, с которыми могут быть незнакомы неопытные разработчики:
ключи идемпотентности каждой операции;
-гарантии доставки (exactly once, at least once; см. описание гарантий доставки11 на примере технологии Apache Kafka);
+гарантии доставки (exactly once, at least once; см. описание гарантий доставки11 на примере технологии Apache Kafka);
будет ли сервер генерировать параллельные запросы к webhook-у и, если да, каково максимальное количество одновременных запросов;
гарантирует ли сервер строгий порядок сообщений (запросы всегда доставляются в порядке от самого старого к самому новому)
размеры полей и сообщений в байтах;
@@ -3274,9 +3274,9 @@ X-Idempotency-Token: <токен>
Очереди сообщений
-Для внутренних API технология webhook-ов (то есть наличия программной возможности задавать URL обратного вызова) либо вовсе не нужна, либо решается с помощью протоколов Service Discovery12 , поскольку сервисы в составе одного бэкенда как правило равноправны — если сервис А может вызывать сервис Б, то и сервис Б может вызывать сервис А.
+Для внутренних API технология webhook-ов (то есть наличия программной возможности задавать URL обратного вызова) либо вовсе не нужна, либо решается с помощью протоколов Service Discovery12 , поскольку сервисы в составе одного бэкенда как правило равноправны — если сервис А может вызывать сервис Б, то и сервис Б может вызывать сервис А.
Однако все проблемы Webhook-ов, описанные нами выше, для таких обратных вызовов всё ещё актуальны. Вызов внутреннего сервиса всё ещё может окончиться false negative-ошибкой, внутренние клиенты могут не ожидать нарушения порядка пересылки сообщений и так далее.
-Для решения этих проблем, а также для большей горизонтальной масштабируемости технологий обратного вызова, были созданы сервисы очередей сообщений13 и, в частности, различные серверные реализации паттерна pub/sub14 . В настоящий момент pub/sub-архитектуры пользуются большой популярностью среди разработчиков, вплоть до перевода любого межсервисного взаимодействия на очереди событий.
+Для решения этих проблем, а также для большей горизонтальной масштабируемости технологий обратного вызова, были созданы сервисы очередей сообщений13 и, в частности, различные серверные реализации паттерна pub/sub14 . В настоящий момент pub/sub-архитектуры пользуются большой популярностью среди разработчиков, вплоть до перевода любого межсервисного взаимодействия на очереди событий.
NB : отметим, что ничего бесплатного в мире не бывает, и за эти гарантии доставки и горизонтальную масштабируемость необходимо платить:
межсерверное взаимодействие становится событийно-консистентным со всеми вытекающими отсюда проблемами;
@@ -3284,20 +3284,20 @@ X-Idempotency-Token: <токен>
в очереди могут скапливаться необработанные сообщения, внося нарастающие задержки, и решение этой проблемы на стороне подписчика может оказаться отнюдь не тривиальным.
Отметим также, что в публичных API зачастую используются обе технологии в связке — бэкенд API отправляет задание на вызов webhook-а в виде публикации события, которое специально предназначенный для этого внутренний сервис будет пытаться обработать путём вызова webhook-а.
-Теоретически можно представить себе и такую интеграцию, в которой разработчик API даёт партнёрам непосредственно прямой доступ к внутренней очереди сообщений, однако примеры таких API нам неизвестны.
Примечания
+Теоретически можно представить себе и такую интеграцию, в которой разработчик API даёт партнёрам непосредственно прямой доступ к внутренней очереди сообщений, однако примеры таких API нам неизвестны.
Примечания
Одно из неприятных ограничений почти всех перечисленных в предыдущей главе технологий — это относительно невысокий размер сообщения. Наиболее проблематичная ситуация с push-уведомлениями: Google Firebase Messaging на момент написания настоящей главы разрешал сообщения не более 4000 байт. Но и в серверной разработке ограничения заметны: например, Amazon SQS лимитирует размер сообщения 256 килобайтами. При разработке webhook-ов вы рискуете быстро упереться в размеры тел сообщений, выставленных на веб-серверах партнёров (например, в nginx по умолчанию разрешены тела запросов не более одного мегабайта). Это приводит нас к необходимости сделать два технических выбора:
содержит ли тело сообщения все данные необходимые для его обработки, или только уведомляет о факте изменения состояния;
@@ -3573,7 +3573,7 @@ Host: partners.host
Если операции в списке как-то зависят одна от другой (как в примере выше — партнёру нужно и сделать рефанд, и отменить заказ, выполнение только одной из этих операций бессмысленно) либо важен порядок исполнения операций, неатомарные эндпойнты будут постоянно приводить к проблемам. И даже если вам кажется, что в вашей предметной области таких проблем нет, в какой-то момент может оказаться, что вы чего-то не учли.
Поэтому наши рекомендации по организации эндпойнтов массовых изменений таковы:
-Если вы можете обойтись без таких эндпойнтов — обойдитесь. В server-to-server интеграциях экономия копеечная, в современных сетях с поддержкой протокола QUIC1 и мультиплексирования запросов тоже весьма сомнительная.
+Если вы можете обойтись без таких эндпойнтов — обойдитесь. В server-to-server интеграциях экономия копеечная, в современных сетях с поддержкой протокола QUIC1 и мультиплексирования запросов тоже весьма сомнительная.
Если такой эндпойнт всё же нужен, лучше реализовать его атомарно и предоставить SDK, которые помогут партнёрам не допускать типичные ошибки.
Если реализовать атомарный эндпойнт невозможно, тщательно продумайте дизайн API, чтобы не допустить ошибок, подобных описанным выше.
Вне зависимости от выбранного подхода, ответы сервера должны включать разбивку по подзапросам. В случае атомарных эндпойнтов это означает включение в ответ списка ошибок, из-за которых исполнение запроса не удалось, в идеале — со всеми потенциальными ошибками (т.е. с результатами проверок каждого подзапроса на валидность). Для неатомарных эндпойнтов необходимо возвращать список со статусами каждого подзапроса и всеми возникшими ошибками.
@@ -3600,7 +3600,7 @@ Host: partners.host
}]
}
-На всякий случай уточним, что вложенные операции должны быть сами по себе идемпотентны. Если же это не так, то следует каким-то детерминированным образом сгенерировать внутренние ключи идемпотентности на каждую вложенную операцию в отдельности (в простейшем случае — считать токен идемпотентности внутренних запросов равным токену идемпотентости внешнего запроса, если это допустимо в рамках предметной области; иначе придётся использовать составные токены — в нашем случае, например, в виде <order_id>:<external_token>
).
Примечания
+На всякий случай уточним, что вложенные операции должны быть сами по себе идемпотентны. Если же это не так, то следует каким-то детерминированным образом сгенерировать внутренние ключи идемпотентности на каждую вложенную операцию в отдельности (в простейшем случае — считать токен идемпотентности внутренних запросов равным токену идемпотентости внешнего запроса, если это допустимо в рамках предметной области; иначе придётся использовать составные токены — в нашем случае, например, в виде <order_id>:<external_token>
).
Примечания
Описанный в предыдущей главе пример со списком операций, который может быть выполнен частично, естественным образом подводит нас к следующей проблеме дизайна API. Что, если изменение не является атомарной идемпотентной операцией (как изменение статуса заказа), а представляет собой низкоуровневую перезапись нескольких полей объекта? Рассмотрим следующий пример.
POST /v1/orders/
@@ -3691,7 +3691,7 @@ X-Idempotency-Token: <токен>
-Это решение можно улучшить путём ввода явных управляющих конструкций вместо «магических значений» и введением мета-опций операции (скажем, фильтра по именам полей, как это принято в gRPC поверх Protobuf1 ), например, так:
+Это решение можно улучшить путём ввода явных управляющих конструкций вместо «магических значений» и введением мета-опций операции (скажем, фильтра по именам полей, как это принято в gRPC поверх Protobuf1 ), например, так:
@@ -3807,8 +3807,8 @@ X-Idempotency-Token: <токен>
}
Этот подход существенно сложнее в имплементации, но является единственным возможным вариантом реализации совместного редактирования, поскольку он явно отражает, что в действительности делал пользовать с представлением объекта. Имея данные в таком формате возможно организовать и оффлайн-редактирование, когда пользовательские изменения накапливаются и сервер впоследствии автоматически разрешает возможные конфликты, основываясь на истории ревизий.
-NB : один из подходов к этой задаче — разработка такой номенклатуры операций над данными (например, conflict-free replicated data type (CRDT )2 ), в которой любые действия транзитивны (т.е. конечное состояние системы не зависит от того, в каком порядке они были применены). Мы, однако, склонны считать такой подход применимым только к весьма ограниченным предметным областям — поскольку в реальной жизни нетранзитивные действия находятся почти всегда. Если один пользователь ввёл в документ новый текст, а другой пользователь удалил документ — никакого разумного (т.е. удовлетворительного с точки зрения обоих акторов) способа автоматического разрешения конфликта здесь нет, необходимо явно спросить пользователей, что бы они хотели сделать с возникшим конфликтом.
Примечания
+NB : один из подходов к этой задаче — разработка такой номенклатуры операций над данными (например, conflict-free replicated data type (CRDT )2 ), в которой любые действия транзитивны (т.е. конечное состояние системы не зависит от того, в каком порядке они были применены). Мы, однако, склонны считать такой подход применимым только к весьма ограниченным предметным областям — поскольку в реальной жизни нетранзитивные действия находятся почти всегда. Если один пользователь ввёл в документ новый текст, а другой пользователь удалил документ — никакого разумного (т.е. удовлетворительного с точки зрения обоих акторов) способа автоматического разрешения конфликта здесь нет, необходимо явно спросить пользователей, что бы они хотели сделать с возникшим конфликтом.
Примечания
В предыдущих главах мы много говорили о том, что фон ошибок — не только неизбежное зло в любой достаточно большой системе, но и, зачастую, осознанное решение, которое позволяет сделать систему более масштабируемой и предсказуемой.
Зададим себе, однако, вопрос: а что значит «более предсказуемая» система? Для нас как для вендора API это достаточно просто: процент ошибок (в разбивке по типам) достаточно стабилен, и им можно пользоваться как индикатором возникающих технических проблем (если он растёт) и как KPI для технических улучшений и рефакторингов (если он падает).
Но вот для разработчиков-партнёров понятие «предсказуемость поведения API» имеет совершенно другой смысл: насколько хорошо и полно они в своём коде могут покрыть различные сценарии использования API и потенциальные проблемы — или, иными словами, насколько явно из документации и номенклатуры методов и ошибок API становится ясно, какие типовые ошибки могут возникнуть и как с ними работать.
@@ -4495,7 +4495,7 @@ X-Idempotency-Token: <токен>
Вышестоящие сущности должны при этом оставаться информационными контекстами для нижестоящих, т.е. не предписывать конкретное поведение, а только сообщать о своём состоянии и предоставлять функциональность для его изменения (прямую через соответствующие методы либо косвенную через получение определённых событий).
Конкретная функциональность, т.е. работа непосредственно с «железом», нижележащим API платформы, должна быть делегирована сущностям самого низкого уровня.
-NB . В этих правилах нет ничего особенно нового: в них легко опознаются принципы архитектуры SOLID1 2 — что неудивительно, поскольку SOLID концентрируется на контрактно-ориентированном подходе к разработке, а API по определению и есть контракт. Мы лишь добавляем в эти принципы понятие уровней абстракции и информационных контекстов.
+NB . В этих правилах нет ничего особенно нового: в них легко опознаются принципы архитектуры SOLID1 2 — что неудивительно, поскольку SOLID концентрируется на контрактно-ориентированном подходе к разработке, а API по определению и есть контракт. Мы лишь добавляем в эти принципы понятие уровней абстракции и информационных контекстов.
Остаётся, однако, неотвеченным вопрос о том, как изначально выстроить номенклатуру сущностей таким образом, чтобы расширение API не превращало её в мешанину из различных неконсистентных методов разных эпох. Впрочем, ответ на него довольно очевиден: чтобы при абстрагировании не возникало неловких ситуаций, подобно рассмотренному нами примеру с полями рецептов, все сущности необходимо изначально рассматривать как частную реализацию некоторого более общего интерфейса, даже если никаких альтернативных реализаций в настоящий момент не предвидится.
Например, разрабатывая API эндпойнта POST /search
мы должны были задать себе вопрос: а «результат поиска» — это абстракция над каким интерфейсом? Для этого нам нужно аккуратно декомпозировать эту сущность, чтобы понять, каким своим срезом она выступает во взаимодействии с какими объектами.
Тогда мы придём к пониманию, что результат поиска — это, на самом деле, композиция двух интерфейсов:
@@ -4518,8 +4518,8 @@ X-Idempotency-Token: <токен>
А что такое, в свою очередь, «абстрактное представление результатов поиска в UI»? Есть ли у нас какие-то другие виды поисков, не является ли ISearchItemViewParameters
сам наследником какого-либо другого интерфейса или композицией других интерфейсов?
-Замена конкретных имплементаций интерфейсами позволяет не только точнее ответить на многие вопросы, которые должны были у вас возникнуть в ходе проектирования API, но и наметить множество возможных векторов развития API, что поможет избежать проблем с неконсистентностью API в ходе дальнейшей эволюции программного продукта.
Примечания
+Замена конкретных имплементаций интерфейсами позволяет не только точнее ответить на многие вопросы, которые должны были у вас возникнуть в ходе проектирования API, но и наметить множество возможных векторов развития API, что поможет избежать проблем с неконсистентностью API в ходе дальнейшей эволюции программного продукта.
Примечания
Помимо вышеперечисленных абстрактных принципов хотелось бы также привести набор вполне конкретных рекомендаций по внесению изменений в существующий API с поддержанием обратной совместимости.
То, что вы не давали формальных гарантий и обязательств, совершенно не означает, что эти неформальные гарантии и обязательства можно нарушать. Зачастую даже исправление ошибок в API может привести к неработоспособности чьего-то кода. Можно привести следующий пример из реальной жизни, с которым столкнулся автор этой книги:
@@ -4565,8 +4565,8 @@ X-Idempotency-Token: <токен>
Несмотря на все приёмы и принципы, изложенные в настоящем разделе, с большой вероятностью вы ничего не сможете сделать с накапливающейся неконсистентностью вашего API. Да, можно замедлить скорость накопления, предусмотреть какие-то проблемы заранее, заложить запасы устойчивости — но предугадать всё решительно невозможно. На этом этапе многие разработчики склонны принимать скоропалительные решения — т.е. выпускать новые минорные версии API с явным или неявным нарушением обратной совместимости в целях исправления ошибок дизайна.
Так делать мы крайне не рекомендуем — поскольку, напомним, API является помимо прочего и мультипликатором ваших ошибок. Что мы рекомендуем — так это завести блокнот душевного покоя, где вы будете записывать выученные уроки, которые потом нужно будет не забыть применить на практике при выпуске новой мажорной версии API.
Вопросы организации HTTP API — к большому сожалению, одни из самых «холиварных». Будучи одной из самых популярных и притом весьма непростых в понимании технологий (ввиду большого по объёму и фрагментированного на отдельные RFC стандарта), спецификация HTTP обречена быть плохо понятой и превратно истолкованной миллионами разработчиков и многими тысячами учебных пособий. Поэтому, прежде чем переходить непосредственно к полезной части настоящего раздела, мы обязаны дать уточнения, о чём же всё-таки пойдёт речь.
-Начнём с небольшой исторической справки. Выполнение запросов пользователя на удалённом сервере — одна из базовых задач в программировании ещё со времён мейнфреймов, естественным образом получившая новый импульс развития с появлением ARPANET. Хотя первые высокоуровневые протоколы сетевого взаимодействия работали в терминах отправки по сети сообщений (см., например, протокол DEL предложенный в одном из самых первых RFC — RFC-5 от 1969 года1 ), довольно быстро теоретики пришли к мысли, что было бы гораздо удобнее, если бы обращение к удалённому узлу сети и использование удалённых ресурсов с точки зрения сигнатуры вызова ничем не отличались от обращения к локальной памяти и локальным ресурсам. Эту концепцию строго сформулировал под названием Remote Procedure Call (RPC) в 1981 году сотрудник знаменитой лаборатории Xerox в Пало-Альто Брюс Нельсон2 и он же был соавтором первой практической имплементации предложенной парадигмы — Sun RPC3 4 , существующей и сегодня под названием ONC RPC.
-Первые широко распространённые RPC-протоколы — такие как упомянутый Sun RPC, Java RMI5 , CORBA6 — чётко следовали парадигме. Технология позволила сделать именно то, о чём писал Нельсон — не различать локальное и удалённое исполнение кода. Вся «магия» скрыта внутри обвязки, которая генерирует имплементацию работы с удалённым сервером, а с форматом передачи данных разработчик и вовсе не сталкивается.
+Начнём с небольшой исторической справки. Выполнение запросов пользователя на удалённом сервере — одна из базовых задач в программировании ещё со времён мейнфреймов, естественным образом получившая новый импульс развития с появлением ARPANET. Хотя первые высокоуровневые протоколы сетевого взаимодействия работали в терминах отправки по сети сообщений (см., например, протокол DEL предложенный в одном из самых первых RFC — RFC-5 от 1969 года1 ), довольно быстро теоретики пришли к мысли, что было бы гораздо удобнее, если бы обращение к удалённому узлу сети и использование удалённых ресурсов с точки зрения сигнатуры вызова ничем не отличались от обращения к локальной памяти и локальным ресурсам. Эту концепцию строго сформулировал под названием Remote Procedure Call (RPC) в 1981 году сотрудник знаменитой лаборатории Xerox в Пало-Альто Брюс Нельсон2 и он же был соавтором первой практической имплементации предложенной парадигмы — Sun RPC3 4 , существующей и сегодня под названием ONC RPC.
+Первые широко распространённые RPC-протоколы — такие как упомянутый Sun RPC, Java RMI5 , CORBA6 — чётко следовали парадигме. Технология позволила сделать именно то, о чём писал Нельсон — не различать локальное и удалённое исполнение кода. Вся «магия» скрыта внутри обвязки, которая генерирует имплементацию работы с удалённым сервером, а с форматом передачи данных разработчик и вовсе не сталкивается.
Удобство использования технологии, однако, оказалось её же Ахиллесовой пятой:
требование работы с удалёнными вызовами как с локальными приводит к высокой сложности протокола в силу необходимости поддерживать разнообразные возможности высокоуровневых языков программирования;
@@ -4590,7 +4590,7 @@ X-Idempotency-Token: <токен>
во-первых, люди не запоминают IP-адреса и предпочитают оперировать «говорящими» именами;
во-вторых, IP-адрес является технической сущностью, связанной с узлом сети, а разработчики хотели бы иметь возможность добавлять и изменять узлы, не нарушая работы своих приложений.
-Удобной (и опять же имеющей почти стопроцентное проникновение) абстракцией над IP-адресами оказалась система доменных имён, позволяющий назначить узлам сети человекочитаемые синонимы. Появление доменных имён потребовало разработки клиент-серверных протоколов более высокого, чем TCP/IP, уровня, и для передачи текстовых (гипертекстовых) данных таким протоколом стал HTTP 0.97 , разработанный Тимом Бёрнерсом-Ли опубликованный в 1991 году. Помимо поддержки обращения к узлам сети по именам, HTTP также предоставил ещё одну очень удобную абстракцию, а именно назначение собственных адресов эндпойнтам, работающим на одном сетевом узле.
+Удобной (и опять же имеющей почти стопроцентное проникновение) абстракцией над IP-адресами оказалась система доменных имён, позволяющий назначить узлам сети человекочитаемые синонимы. Появление доменных имён потребовало разработки клиент-серверных протоколов более высокого, чем TCP/IP, уровня, и для передачи текстовых (гипертекстовых) данных таким протоколом стал HTTP 0.97 , разработанный Тимом Бёрнерсом-Ли опубликованный в 1991 году. Помимо поддержки обращения к узлам сети по именам, HTTP также предоставил ещё одну очень удобную абстракцию, а именно назначение собственных адресов эндпойнтам, работающим на одном сетевом узле.
Протокол был очень прост и всего лишь описывал способ получить документ, открыв TCP/IP соединение с сервером и передав строку вида GET адрес_документа
. Позднее протокол был дополнен стандартом URL, позволяющим детализировать адрес документа, и далее протокол начал развиваться стремительно: появились новые глаголы помимо GET
, статусы ответов, заголовки, типы данных и так далее.
HTTP появился изначально для передачи размеченного гипертекста, что для программных интерфейсов подходит слабо. Однако HTML со временем эволюционировал в более строгий и машиночитаемый XML, который быстро стал одним из общепринятых форматов описания вызовов API. (Забегая вперёд, скажем, что в начале 2000-х XML был быстро вытеснен ещё более простым и интероперабельным JSON.)
Поскольку, с одной стороны, HTTP был простым и понятным протоколом, позволяющим осуществлять произвольные запросы к удаленным серверам по их доменным именам, и, с другой стороны, быстро оброс почти бесконечным количеством разнообразных расширений над базовой функциональностью, он стал второй точкой, к которой сходятся сетевые технологии: практически все запросы к API внутри TCP/IP-сетей осуществляются по протоколу HTTP (и даже если используется альтернативный протокол, запросы в нём всё равно зачастую оформлены в виде HTTP-пакетов просто ради удобства). HTTP, однако, идеологически совершенно противоположен RPC, поскольку не предполагает ни нативной обвязки для функций удалённого вызова, ни тем более разделяемого доступа к памяти. Зато HTTP предложил несколько очень удобных концепций для наращивания производительности серверов, такие как управление кэшированием из коробки и концепцию прозрачных прокси.
@@ -4623,26 +4623,26 @@ X-Idempotency-Token: <токен>
семантика вызовов HTTP-эндпойнтов соответствует спецификации;
никакие из веб-стандартов нигде не нарушается специально.
-Такое API мы будем для краткости называть просто «HTTP API» или «JSON-over-HTTP API» . Мы понимаем, что такое использование терминологически не полностью корректно, но писать каждый раз «JSON-over-HTTP эндпойнты, утилизирующие семантику, описанную в стандартах HTTP и URL» или «JSON-over-HTTP API, соответствующий архитектурным ограничениям REST» не представляется возможным. Что касается термина «REST API», то как мы покажем дальше, у него нет консистентного определения, поэтому его использования мы также стараемся избегать.
Примечания
+Такое API мы будем для краткости называть просто «HTTP API» или «JSON-over-HTTP API» . Мы понимаем, что такое использование терминологически не полностью корректно, но писать каждый раз «JSON-over-HTTP эндпойнты, утилизирующие семантику, описанную в стандартах HTTP и URL» или «JSON-over-HTTP API, соответствующий архитектурным ограничениям REST» не представляется возможным. Что касается термина «REST API», то как мы покажем дальше, у него нет консистентного определения, поэтому его использования мы также стараемся избегать.
Примечания
Как мы обсудили в предыдущей главе, в настоящий момент выбор технологии для разработки клиент-серверных API сводится к выбору либо ресурсоориентированного подхода (то, что принято называть «REST API», а мы, напомним, будем использовать термин «HTTP API»), либо одного из современных RPC-протоколов. Как мы отмечали, концептуально разница не очень значительна; однако технически разные фреймворки используют протокол HTTP совершенно по-разному:
Во-первых , разные фреймворки опираются на разные форматы передаваемых данных:
-HTTP API и некоторые RPC (JSON-RPC1 , GraphQL2 ) полагаются в основном на формат JSON3 (опционально дополненный передачей бинарных файлов);
-gRPC4 , а также Thrift5 , Avro6 и другие специализированные RPC-протоколы полагаются на бинарные форматы (такие как Protocol Buffers7 , FlatBuffers8 и собственный формат Apache Avro);
-наконец, некоторые RPC-протоколы (SOAP9 , XML-RPC10 ) используют для передачи данных формат XML11 (что многими разработчиками сегодня воспринимается скорее как устаревшая практика).
+HTTP API и некоторые RPC (JSON-RPC1 , GraphQL2 ) полагаются в основном на формат JSON3 (опционально дополненный передачей бинарных файлов);
+gRPC4 , а также Thrift5 , Avro6 и другие специализированные RPC-протоколы полагаются на бинарные форматы (такие как Protocol Buffers7 , FlatBuffers8 и собственный формат Apache Avro);
+наконец, некоторые RPC-протоколы (SOAP9 , XML-RPC10 ) используют для передачи данных формат XML11 (что многими разработчиками сегодня воспринимается скорее как устаревшая практика).
Во-вторых , существующие реализации различаются подходом к утилизации протокола HTTP:
либо клиент-серверное взаимодействие опирается на описанные в стандарте HTTP возможности — этот подход характеризует HTTP API;
либо HTTP утилизируется как транспорт, и поверх него выстроен дополнительный уровень абстракции (т.е. возможности HTTP, такие как номенклатура ошибок или заголовков, сознательно редуцируются до минимального уровня, а вся мета-информация переносится на уровень вышестоящего протокола) — этот подход характерен для RPC протоколов.
-У читателя может возникнуть резонный вопрос — а почему вообще существует такая дихотомия: одни API полагаются на стандартную семантику HTTP, другие полностью от неё отказываются в пользу новоизобретённых стандартов, а третьи существуют где-то посередине. Например, если мы посмотрим на формат ответа в JSON-RPC12 , то мы обнаружим, что он легко мог бы быть заменён на стандартные средства протокола HTTP. Вместо
+У читателя может возникнуть резонный вопрос — а почему вообще существует такая дихотомия: одни API полагаются на стандартную семантику HTTP, другие полностью от неё отказываются в пользу новоизобретённых стандартов, а третьи существуют где-то посередине. Например, если мы посмотрим на формат ответа в JSON-RPC12 , то мы обнаружим, что он легко мог бы быть заменён на стандартные средства протокола HTTP. Вместо
HTTP/1.1 200 OK
{
@@ -4698,15 +4698,15 @@ X-Idempotency-Token: <токен>
Будем здесь честны: большинство существующих имплементаций HTTP API действительно страдают от указанных проблем. Тем не менее, мы берём на себя смелость заявить, что все эти проблемы большей частью надуманы, и их решению не уделяют большого внимания потому, что указанные накладные расходы не являются сколько-нибудь заметными для большинства вендоров API. В частности:
-Если мы говорим об избыточности формата, то необходимо сделать важную оговорку: всё вышесказанное верно, если мы не применяем сжатие. Сравнения показывают13 , что использование gzip практически нивелирует разницу в размере JSON документов относительно альтернативных бинарных форматов (а есть ещё и специально предназначенные для текстовых данных архиваторы, например, brotli14 ).
+Если мы говорим об избыточности формата, то необходимо сделать важную оговорку: всё вышесказанное верно, если мы не применяем сжатие. Сравнения показывают13 , что использование gzip практически нивелирует разницу в размере JSON документов относительно альтернативных бинарных форматов (а есть ещё и специально предназначенные для текстовых данных архиваторы, например, brotli14 ).
Вообще говоря, если такая нужда появляется, то и в рамках HTTP API вполне можно регулировать список возвращаемых полей ответа, это вполне соответствует духу и букве стандарта. Однако, мы должны заметить, что экономия трафика на возврате частичных состояний (которую мы рассматривали подробно в главе «Частичные обновления ») очень редко бывает оправдана.
-Если использовать стандартные десериализаторы JSON, разница по сравнению с бинарными форматами может оказаться действительно очень большой. Если, однако, эти накладные расходы являются проблемой, стоит обратиться к альтернативным десериализаторам — в частности, simdjson15 . Благодаря оптимизированному низкоуровневому коду simdjson показывает отличную производительность, которой может не хватить только совсем уж экзотическим API.
+Если использовать стандартные десериализаторы JSON, разница по сравнению с бинарными форматами может оказаться действительно очень большой. Если, однако, эти накладные расходы являются проблемой, стоит обратиться к альтернативным десериализаторам — в частности, simdjson15 . Благодаря оптимизированному низкоуровневому коду simdjson показывает отличную производительность, которой может не хватить только совсем уж экзотическим API.
-Комбинация gzip/brotli + simdjson во многом делает бессмысленным использование в клиент-серверной коммуникации оптимизированных вариантов JSON — таких как, например, BSON16 .
+Комбинация gzip/brotli + simdjson во многом делает бессмысленным использование в клиент-серверной коммуникации оптимизированных вариантов JSON — таких как, например, BSON16 .
@@ -4754,26 +4754,26 @@ X-Idempotency-Token: <токен>
потенциальную зависимость от Google.
В остальном, gRPC вне всяких сомнений один из наиболее современных и производительных протоколов.
-GraphQL представляет собой любопытный подход, который объединяет концепцию «ресурсов» в HTML (т.е. фокусируется на подробном описании форматов доступных данных и отношений между доменами), при этом предоставляя чрезвычайно богатый язык запросов для извлечения нужных наборов полей. Основная область его применения — это насыщенные разнородными данными предметные области (как, в общем-то, и следует из названия, GraphQL — скорее механизм распределённых запросов к абстрактному хранилищу данных, нежели парадигма разработки API). Предоставление внешних GraphQL API на сегодня скорее экзотика, поскольку с ростом количества данных и запросов GraphQL-сервисом становится очень сложно управлять17 .
-NB : теоретически, API может обладать двойным интерфейсом, например, и JSON-over-HTTP, и gRPC. Так как в основе всех современных фреймворков лежит формальное описание форматов данных и доступных операций, и эти форматы можно при желании транслировать друг в друга, такой мульти-API технически возможен. Практически же примеры таких API нам неизвестны — по-видимому, накладные расходы на поддержание двойного интерфейса не покрывают потенциальную выгоду от большего удобства для разработчиков.
Примечания
+GraphQL представляет собой любопытный подход, который объединяет концепцию «ресурсов» в HTML (т.е. фокусируется на подробном описании форматов доступных данных и отношений между доменами), при этом предоставляя чрезвычайно богатый язык запросов для извлечения нужных наборов полей. Основная область его применения — это насыщенные разнородными данными предметные области (как, в общем-то, и следует из названия, GraphQL — скорее механизм распределённых запросов к абстрактному хранилищу данных, нежели парадигма разработки API). Предоставление внешних GraphQL API на сегодня скорее экзотика, поскольку с ростом количества данных и запросов GraphQL-сервисом становится очень сложно управлять17 .
+NB : теоретически, API может обладать двойным интерфейсом, например, и JSON-over-HTTP, и gRPC. Так как в основе всех современных фреймворков лежит формальное описание форматов данных и доступных операций, и эти форматы можно при желании транслировать друг в друга, такой мульти-API технически возможен. Практически же примеры таких API нам неизвестны — по-видимому, накладные расходы на поддержание двойного интерфейса не покрывают потенциальную выгоду от большего удобства для разработчиков.
Примечания
Прежде, чем перейти непосредственно к паттернам проектирования HTTP API, мы должны сделать ещё одно терминологическое отступление. Очень часто HTTP API, соответствующие данному нами в главе «О концепции HTTP API » определению, называют «REST API» или «RESTful API». В настоящем разделе мы эти термины не используем, поскольку оба этих термина неформальные и не несут конкретного смысла.
-Что такое «REST»? Как мы упоминали ранее, в 2000 году один из авторов спецификаций HTTP и URI Рой Филдинг защитил докторскую диссертацию на тему «Архитектурные стили и дизайн архитектуры сетевого программного обеспечения», пятая глава которой была озаглавлена как «Representational State Transfer (REST)»1 .
+Что такое «REST»? Как мы упоминали ранее, в 2000 году один из авторов спецификаций HTTP и URI Рой Филдинг защитил докторскую диссертацию на тему «Архитектурные стили и дизайн архитектуры сетевого программного обеспечения», пятая глава которой была озаглавлена как «Representational State Transfer (REST)»1 .
Как нетрудно убедиться, прочитав эту главу, она представляет собой абстрактный обзор распределённой сетевой архитектуры, вообще не привязанной ни к HTTP, ни к URL. Более того, она вовсе не посвящена правилам дизайна API — в этой главе Филдинг методично перечисляет ограничения , с которыми приходится сталкиваться разработчику распределённого сетевого программного обеспечения. Вот они:
клиент и сервер не знают внутреннего устройства друг друга (клиент-серверная архитектура);
@@ -4791,26 +4791,26 @@ X-Idempotency-Token: <токен>
раз есть интерфейс взаимодействия, значит, под него всегда можно мимикрировать, а значит, требование независимости имплементации клиента и сервера всегда выполнимо;
раз можно сделать альтернативную имплементацию сервера — значит, можно сделать и многослойную архитектуру, поставив дополнительный прокси между клиентом и сервером;
поскольку клиент представляет собой вычислительную машину, он всегда хранит хоть какое-то состояние и кэширует хоть какие-то данные;
-наконец, code-on-demand вообще лукавое требование, поскольку в архитектуре фон Неймана2 всегда можно объявить данные, полученные по сети, «инструкциями» на некотором формальном языке, а код клиента — их интерпретатором.
+наконец, code-on-demand вообще лукавое требование, поскольку в архитектуре фон Неймана2 всегда можно объявить данные, полученные по сети, «инструкциями» на некотором формальном языке, а код клиента — их интерпретатором.
Да, конечно, вышеприведённое рассуждение является софизмом, доведением до абсурда. Самое забавное в этом упражнении состоит в том, что мы можем довести его до абсурда и в другую сторону, объявив ограничения REST неисполнимыми. Например, очевидно, что требование code-on-demand противоречит требованию независимости клиента и сервера — клиент должен уметь интерпретировать код с сервера, написанный на вполне конкретном языке. Что касается правила на букву S («stateless»), то систем, в которых сервер вообще не хранит никакого контекста клиента в мире вообще практически нет, поскольку почти ничего полезного для клиента в такой системе сделать нельзя. (Чего, кстати, Филдинг прямым текстом требует: «коммуникация … не может получать никаких преимуществ от того, что на сервере хранится какой-то контекст».)
-Наконец, сам Филдинг внёс дополнительную энтропию в вопрос, выпустив в 2008 году разъяснение3 , что же он имел в виду. В частности, в этой статье утверждается, что:
+Наконец, сам Филдинг внёс дополнительную энтропию в вопрос, выпустив в 2008 году разъяснение3 , что же он имел в виду. В частности, в этой статье утверждается, что:
разработка REST API должна фокусироваться на описании медиатипов, представляющих ресурсы; при этом клиент вообще ничего про эти медиатипы знать не должен;
в REST API не должно быть фиксированных имён ресурсов и операций над ними, клиент должен извлекать эту информацию из ответов сервера.
-REST по Филдингу-2008 подразумевает, что клиент, получив каким-то образом ссылку на точку входа в REST API, далее должен быть в состоянии полностью выстроить взаимодействие с API, не обладая вообще никаким априорным знанием о нём, и уж тем более не должен содержать никакого специально написанного кода для работы с этим API. Это требование — гораздо более сильное, нежели принципы, описанные в диссертации 2000 года. В частности, из идеи REST-2008 вытекает отсутствие фиксированных шаблонов URL для выполнения операций над ресурсами — предполагается, что такие URL присутствуют в виде гиперссылок в представлениях ресурсов (эта концепция известна также под названием HATEOAS4 ). Диссертация же 2000 года никаких строгих определений «гипермедиа», которые препятствовали бы идее конструирования ссылок на основе априорных знаний об API (например, по спецификации), не содержит.
+REST по Филдингу-2008 подразумевает, что клиент, получив каким-то образом ссылку на точку входа в REST API, далее должен быть в состоянии полностью выстроить взаимодействие с API, не обладая вообще никаким априорным знанием о нём, и уж тем более не должен содержать никакого специально написанного кода для работы с этим API. Это требование — гораздо более сильное, нежели принципы, описанные в диссертации 2000 года. В частности, из идеи REST-2008 вытекает отсутствие фиксированных шаблонов URL для выполнения операций над ресурсами — предполагается, что такие URL присутствуют в виде гиперссылок в представлениях ресурсов (эта концепция известна также под названием HATEOAS4 ). Диссертация же 2000 года никаких строгих определений «гипермедиа», которые препятствовали бы идее конструирования ссылок на основе априорных знаний об API (например, по спецификации), не содержит.
NB : оставляя за скобками тот факт, что Филдинг весьма вольно истолковал свою собственную диссертацию, просто отметим, что ни одна существующая система в мире не удовлетворяет описанию REST по Филдингу-2008.
-Нам неизвестно, почему из всех обзоров абстрактной сетевой архитектуры именно концепция Филдинга обрела столь широкую популярность; очевидно другое: теория Филдинга, преломившись в умах миллионов программистов (включая самого Филдинга), превратилась в целую инженерную субкультуру. Путём редукции абстракций REST применительно конкретно к протоколу HTTP и стандарту URL родилась химера «RESTful API», конкретного смысла которой никто не знает5 .
+Нам неизвестно, почему из всех обзоров абстрактной сетевой архитектуры именно концепция Филдинга обрела столь широкую популярность; очевидно другое: теория Филдинга, преломившись в умах миллионов программистов (включая самого Филдинга), превратилась в целую инженерную субкультуру. Путём редукции абстракций REST применительно конкретно к протоколу HTTP и стандарту URL родилась химера «RESTful API», конкретного смысла которой никто не знает5 .
Хотим ли мы тем самым сказать, что REST является бессмысленной концепцией? Отнюдь нет. Мы только хотели показать, что она допускает чересчур широкую интерпретацию, в чём одновременно кроется и её сила, и её слабость.
С одной стороны, благодаря многообразию интерпретаций, разработчики API выстроили какое-то размытое, но всё-таки полезное представление о «правильной» архитектуре HTTP API. С другой стороны, за отсутствием чётких определений тема REST API превратилась в один из самых больших источников холиваров среди программистов — притом холиваров совершенно бессмысленных, поскольку популярное представление о REST не имеет вообще никакого отношения ни к тому REST, который описан в диссертации Филдинга (и тем более к тому REST, который Филдинг описал в своём манифесте 2008 года).
-Термин «архитектурный стиль REST» и производный от него «REST API» в последующих главах мы использовать не будем, поскольку, как видно из написанного выше, в этом нет никакой нужды — на все описанные Филдингом ограничения мы многократно ссылались по ходу предыдущих глав, поскольку, повторимся, распределённое сетевое API попросту невозможно разработать, не руководствуясь ими. Однако HTTP API (подразумевая под этим JSON-over-HTTP эндпойнты, утилизирующие семантику, описанную в стандартах HTTP и URL), как мы его будем определять в последующих главах, фактически соответствует усреднённому представлению о «REST/RESTful API», как его можно найти в многочисленных учебных пособиях.
Примечания
+Термин «архитектурный стиль REST» и производный от него «REST API» в последующих главах мы использовать не будем, поскольку, как видно из написанного выше, в этом нет никакой нужды — на все описанные Филдингом ограничения мы многократно ссылались по ходу предыдущих глав, поскольку, повторимся, распределённое сетевое API попросту невозможно разработать, не руководствуясь ими. Однако HTTP API (подразумевая под этим JSON-over-HTTP эндпойнты, утилизирующие семантику, описанную в стандартах HTTP и URL), как мы его будем определять в последующих главах, фактически соответствует усреднённому представлению о «REST/RESTful API», как его можно найти в многочисленных учебных пособиях.
Примечания
Важное подготовительное упражнение, которое мы должны сделать — это дать описание формата HTTP-запросов и ответов и прояснить базовые понятия. Многое из написанного ниже может показаться читателю самоочевидным, но, увы, специфика протокола такова, что даже базовые сведения о нём, без которых мы не сможем двигаться дальше, разбросаны по обширной и фрагментированной документации, и даже опытные разработчики могут не знать тех или иных нюансов. Ниже мы попытаемся дать структурированный обзор протокола в том объёме, который необходим нам для проектирования HTTP API.
-В описании семантики и формата протокола мы будем руководствоваться свежевышедшим RFC 91101 , который заменил аж девять предыдущих спецификаций, описывавших разные аспекты технологии (при этом большое количество различной дополнительной функциональности всё ещё покрывается отдельными стандартами. В частности, принципы HTTP-кэширования описаны в отдельном RFC 91112 , а широко используемый в API метод PATCH
так и не вошёл в основной RFC и регулируется RFC 57893 ).
+В описании семантики и формата протокола мы будем руководствоваться свежевышедшим RFC 91101 , который заменил аж девять предыдущих спецификаций, описывавших разные аспекты технологии (при этом большое количество различной дополнительной функциональности всё ещё покрывается отдельными стандартами. В частности, принципы HTTP-кэширования описаны в отдельном RFC 91112 , а широко используемый в API метод PATCH
так и не вошёл в основной RFC и регулируется RFC 57893 ).
HTTP-запрос представляет собой (1) применение определённого глагола к URL с (2) указанием версии протокола, (3) передачей дополнительной мета-информации в заголовках и, возможно, (4) каких-то данных в теле запроса:
POST /v1/orders HTTP/1.1
Host: our-api-host.tld
@@ -4834,10 +4834,10 @@ Content-Type: application/json
"id" : 123
}
-NB : в HTTP/2 (и будущем HTTP/3) вместо единого текстового формата используются отдельные бинарные фреймы для передачи заголовков и данных4 . Этот факт не влияет на излагаемые архитектурные принципы, но во избежание двусмысленности мы будем давать примеры в формате HTTP/1.1.
+NB : в HTTP/2 (и будущем HTTP/3) вместо единого текстового формата используются отдельные бинарные фреймы для передачи заголовков и данных4 . Этот факт не влияет на излагаемые архитектурные принципы, но во избежание двусмысленности мы будем давать примеры в формате HTTP/1.1.
URL — единица адресации в HTTP API (некоторые евангелисты технологии даже используют термин «пространство URL» как синоним для Мировой паутины). Предполагается, что HTTP API должен использовать систему адресов столь же гранулярную, как и предметная область; иными словами, у любых сущностей, которыми мы можем манипулировать независимо, должен быть свой URL.
-Формат URL регулируется отдельным стандартом5 , который развивает независимое сообщество Web Hypertext Application Technology Working Group (WHATWG). Считается, что концепция URL (вместе с понятием универсального имени ресурса, URN) составляет более общую сущность URI (универсальный идентификатор ресурса). (Разница между URL и URN заключается в том, что URL позволяет найти некоторый ресурс в рамках некоторого протокола доступа, в то время как URN — «внутреннее» имя объекта, которое само по себе никак не помогает получить к нему доступ.)
+Формат URL регулируется отдельным стандартом5 , который развивает независимое сообщество Web Hypertext Application Technology Working Group (WHATWG). Считается, что концепция URL (вместе с понятием универсального имени ресурса, URN) составляет более общую сущность URI (универсальный идентификатор ресурса). (Разница между URL и URN заключается в том, что URL позволяет найти некоторый ресурс в рамках некоторого протокола доступа, в то время как URN — «внутреннее» имя объекта, которое само по себе никак не помогает получить к нему доступ.)
URL принято раскладывать на составляющие, каждая из которых опциональна. В стандарте перечислены разнообразные исторические наслоения (например, передача логинов и паролей в URL или использование не-UTF кодировки), которые мы опустим. В рамках дизайна HTTP API нам интересны следующие компоненты:
схема (scheme) — протокол обращения (в нашем случае всегда https:
);
@@ -4873,7 +4873,7 @@ Content-Type: application/json
Заголовки — это метаинформация , привязанная к запросу или ответу. Она может описывать какие-то свойства передаваемых данных (например, Content-Length
), дополнительные сведения о клиенте или сервере (User-Agent
, Date
), или просто содержать поля, не относящиеся непосредственно к смыслу запроса/ответа (например, Authorization
).
Важное свойство заголовков — это возможность считывать их до того, как получено тело сообщения. Таким образом, заголовки могут, во-первых, сами по себе влиять на обработку запроса или ответа, и ими можно относительно легко манипулировать при проксировании — и многие сетевые агенты действительно это делают, добавляя или модифицируя заголовки по своему усмотрению (в частности, современные веб-браузеры добавляют к запросам целую коллекцию заголовков: User-Agent
, Origin
, Accept-Language
, Connection
, Referer
, Sec-Fetch-*
и так далее, а современное ПО веб-серверов, в свою очередь, автоматически добавляет или модифицирует такие заголовки как X-Powered-By
, Date
, Content-Length
, Content-Encoding
, X-Forwarded-For
).
-Подобное вольное обращение с заголовками создаёт определённые проблемы, если ваш API предусматривает передачу дополнительных полей метаданных, поскольку придуманные вами имена полей могут случайно совпасть с какими-то из существующих стандартных имён (или ещё хуже — в будущем появится новое стандартное поле, совпадающее с вашим). Долгое время во избежание подобных коллизий использовался префикс X-
; уже более 10 лет как эта практика объявлена устаревшей и не рекомендуется к использованию (см. подробный разбор вопроса в RFC 66486 ), однако отказа от этого префикса по факту не произошло (и многие широко распространённые нестандартные заголовки, например, X-Forwarded-For
, его всё ещё содержат). Таким образом, использование префикса X-
вероятность коллизий снижает, но не устраняет. Тот же RFC вполне разумно предлагает использовать вместо X-
префикс в виде имени компании. (Мы со своей стороны склонны рекомендовать использовать оба префикса в формате X-ApiName-FieldName
; префикс X-
для читабельности [чтобы отличать специальные заголовки от стандартных], а префикс с именем компании или API — чтобы не произошло коллизий с каким-нибудь другим нестандартным префиксом.)
+Подобное вольное обращение с заголовками создаёт определённые проблемы, если ваш API предусматривает передачу дополнительных полей метаданных, поскольку придуманные вами имена полей могут случайно совпасть с какими-то из существующих стандартных имён (или ещё хуже — в будущем появится новое стандартное поле, совпадающее с вашим). Долгое время во избежание подобных коллизий использовался префикс X-
; уже более 10 лет как эта практика объявлена устаревшей и не рекомендуется к использованию (см. подробный разбор вопроса в RFC 66486 ), однако отказа от этого префикса по факту не произошло (и многие широко распространённые нестандартные заголовки, например, X-Forwarded-For
, его всё ещё содержат). Таким образом, использование префикса X-
вероятность коллизий снижает, но не устраняет. Тот же RFC вполне разумно предлагает использовать вместо X-
префикс в виде имени компании. (Мы со своей стороны склонны рекомендовать использовать оба префикса в формате X-ApiName-FieldName
; префикс X-
для читабельности [чтобы отличать специальные заголовки от стандартных], а префикс с именем компании или API — чтобы не произошло коллизий с каким-нибудь другим нестандартным префиксом.)
Помимо прочего заголовки используются как управляющие конструкции — это т.н. «content negotiation», т.е. договорённость клиента и сервера о формате ответа (через заголовки Accept*
) и условные запросы, позволяющие сэкономить трафик на возврате ответа целиком или частично (через заголовки If-*
-заголовки, такие как If-Range
, If-Modified-Since
и так далее).
Стандарт предписывает как минимум один обязательный заголовок — Host
. Ещё несколько заголовков необязательны, но на практике почти всегда используются:
@@ -4884,7 +4884,7 @@ Content-Type: application/json
В дальнейших примерах мы эти заголовки будем опускать для лучшей читабельности.
Важнейшая составляющая HTTP запроса — это глагол (метод), описывающий операцию, применяемую к ресурсу. RFC 9110 стандартизирует восемь глаголов — GET
, POST
, PUT
, DELETE
, HEAD
, CONNECT
, OPTIONS
и TRACE
— из которых нас как разработчиков API интересует первые четыре. CONNECT
, OPTIONS
и TRACE
— технические методы, которые очень редко используются в HTTP API (за исключением OPTIONS
, который необходимо реализовать, если необходим доступ к API из браузера). Теоретически, HEAD
(метод получения только метаданных , то есть заголовков, ресурса) мог бы быть весьма полезен в HTTP API, но по неизвестным нам причинам практически в этом смысле не используется.
-Помимо RFC 9110, множество других RFC предлагают использовать дополнительные HTTP-глаголы (такие, например, как COPY
, LOCK
, SEARCH
— полный список можно найти в реестре7 ), однако из всего разнообразия предложенных стандартов лишь один имеет широкое хождение — метод PATCH
. Причины такого положения дел довольно тривиальны — этих пяти методов (GET
, POST
, PUT
, DELETE
, PATCH
) достаточно для почти любого HTTP API.
+Помимо RFC 9110, множество других RFC предлагают использовать дополнительные HTTP-глаголы (такие, например, как COPY
, LOCK
, SEARCH
— полный список можно найти в реестре7 ), однако из всего разнообразия предложенных стандартов лишь один имеет широкое хождение — метод PATCH
. Причины такого положения дел довольно тривиальны — этих пяти методов (GET
, POST
, PUT
, DELETE
, PATCH
) достаточно для почти любого HTTP API.
HTTP-глагол определяет два важных свойства HTTP-вызова:
Чисто теоретически возможно использование kebab-case
во всех случаях, но в большинстве языков программирования имена переменных и полей объектов в kebab-case
недопустимы, что приведёт к неудобству работы с таким API.
Короче говоря, ситуация с кейсингом настолько плоха и запутана, что консистентного и удобного решения попросту нет. В этой книге мы придерживаемся следующего правила: токены даются в том кейсинге, который является общепринятым для той секции запроса, в которой находится токен; если положение токена меняется, то меняется и кейсинг. (Мы далеки от того, чтобы рекомендовать этот подход всюду; наша общая рекомендация, скорее — не умножать энтропию и пытаться минимизировать такого рода коллизии.)
-NB : вообще говоря, JSON исходно — это JavaScript Object Notation, а в языке JavaScript кейсинг по умолчанию - camelCase
. Мы, тем не менее, позволим себе утверждать, что JSON давно перестал быть форматом данных, привязанным к JavaScript, и в настоящее время используется для организации взаимодействия агентов, реализованных на любых языках программирования. Использование snake_case
, по крайней мере, позволяет легко перебрасывать параметр из query в тело и обратно, что, обычно, является наиболее частотным кейсом при разработке HTTP API. Впрочем, обратный вариант (использование camelCase
в именах query-параметров) тоже допустим.
Примечания
+NB : вообще говоря, JSON исходно — это JavaScript Object Notation, а в языке JavaScript кейсинг по умолчанию - camelCase
. Мы, тем не менее, позволим себе утверждать, что JSON давно перестал быть форматом данных, привязанным к JavaScript, и в настоящее время используется для организации взаимодействия агентов, реализованных на любых языках программирования. Использование snake_case
, по крайней мере, позволяет легко перебрасывать параметр из query в тело и обратно, что, обычно, является наиболее частотным кейсом при разработке HTTP API. Впрочем, обратный вариант (использование camelCase
в именах query-параметров) тоже допустим.
Примечания
Перейдём теперь к конкретике: что конкретно означает «следовать семантике протокола» и «разрабатывать приложение в соответствии с архитектурным стилем REST». Напомним, речь идёт о следующих принципах:
операции должны быть stateless;
@@ -5134,7 +5134,7 @@ HTTP/1.1 200 O
если всё же какие-то дополнительные операции с внешними данными требуется производить (например, проверять, авторизована ли запрашивающая сторона на выполнение операции), то следует организовать операцию так, чтобы свести её к проверке целостности переданных данных .
-В нашем примере мы могли бы избавиться от лишних запросов к сервису A иначе — начав использовать stateless-токены, например, по стандарту JWT1 . Тогда сервисы B и C смогут сами раскодировать токен и извлечь идентификатор пользователя.
+В нашем примере мы могли бы избавиться от лишних запросов к сервису A иначе — начав использовать stateless-токены, например, по стандарту JWT1 . Тогда сервисы B и C смогут сами раскодировать токен и извлечь идентификатор пользователя.
Пойдём теперь чуть дальше и подметим, что профиль пользователя меняется достаточно редко, и нет никакой нужды каждый раз получать его заново — мы могли бы организовать кэш профилей на стороне гейтвея D. Для этого нам нужно сформировать ключ кэша, которым фактически является идентификатор клиента. Мы можем пойти длинным путём:
@@ -5261,7 +5261,7 @@ Authorization: Bearer <token>
Этот подход можно в дальнейшем усложнять: добавлять гранулярные разрешения выполнять конкретные операции, вводить уровни доступа, проверку прав в реальном времени через дополнительный вызов ACL-сервиса и так далее.
-Важно, что кажущаяся избыточность перестала быть таковой: user_id
в запросе теперь не дублируется в данных токена; эти идентификаторы имеют разный смысл: над каким ресурсом исполняется операция и кто исполняет операцию. Совпадение этих двух сущностей — пусть частотный, но всё же частный случай. Что, к сожалению, не отменяет его неочевидности и возможности легко забыть выполнить проверку в коде. Таков путь.
Примечания
+Важно, что кажущаяся избыточность перестала быть таковой: user_id
в запросе теперь не дублируется в данных токена; эти идентификаторы имеют разный смысл: над каким ресурсом исполняется операция и кто исполняет операцию. Совпадение этих двух сущностей — пусть частотный, но всё же частный случай. Что, к сожалению, не отменяет его неочевидности и возможности легко забыть выполнить проверку в коде. Таков путь.
Примечания
Как мы уже отмечали в предыдущих главах, стандарты HTTP и URL, а также принципы REST, не предписывают определённой семантики значимым компонентам URL (в частности, частям path и парам ключ-значение в query). Правила организации URL в HTTP API существуют только для читабельности кода и удобства разработчика . Что, впрочем, совершенно не означает, что они неважны: напротив, URL в HTTP API являются средством выразить уровни абстракции и области ответственности объектов. Правильный дизайн иерархии сущностей в API должен быть отражён в правильном дизайне номенклатуры URL.
NB : отсутствие строгих правил естественным образом привело к тому, что многие разработчики их просто придумали сами для себя. Некоторые наиболее распространённые стихийные практики, например, требование использовать в URL только существительные, в советах по разработке HTTP API в Интернете часто выдаются за стандарты или требования REST, которыми они не являются. Тем не менее, демонстративное игнорирование таких самопровозглашённых правил тоже не лучший подход для провайдера API, поскольку он увеличивает шансы быть неверно понятым.
Традиционно частям URL приписывается следующая семантика:
@@ -5314,7 +5314,7 @@ X-OurCoffeeAPI-Version: 1
С другой стороны, согласно семантике операции, GET /v1/search
должен возвращать представление ресурса search
. Но разве результаты поиска являются представлением ресурса-поисковика? Смысл операции «поиск» гораздо точнее описывается фразой «обработка запроса в соответствии с внутренней семантикой ресурса», т.е. соответствует методу POST
. Кроме того, можем ли мы вообще говорить о кэшировании поисковых запросов? Страница результатов поиска формируется динамически из множества источников, и повторный запрос с той же поисковой фразой почти наверняка выдаст другой список результатов.
Иными словами, для любых операций, результат которых представляет собой результат работы какого-то алгоритма (например, список релевантных предложений по запросу) мы всегда будем сталкиваться с выбором, что важнее: семантика глагола или отсутствие побочных эффектов? Кэширование ответа или индикация того, что операция вычисляет результаты на лету?
-NB : эта дихотомия волнует не только нас, но и авторов стандарта, которые в конечном итоге предложили новый глагол QUERY
1 , который по сути является немодифицирующим POST
. Мы, однако, сомневаемся, что он получит широкое распространение — поскольку уже существующий SEARCH
2 оказался в этом качестве никому не нужен.
+NB : эта дихотомия волнует не только нас, но и авторов стандарта, которые в конечном итоге предложили новый глагол QUERY
1 , который по сути является немодифицирующим POST
. Мы, однако, сомневаемся, что он получит широкое распространение — поскольку уже существующий SEARCH
2 оказался в этом качестве никому не нужен.
Простых ответов на вопросы выше у нас, к сожалению, нет. В рамках настоящей книги мы придерживаемся следующего подхода: сигнатура вызова в первую очередь должна быть лаконична и читабельна. Усложнение сигнатур в угоду абстрактным концепциям нежелательно. Применительно к указанным проблемам это означает, что:
@@ -5457,8 +5457,8 @@ Location: /v1/orders/{id}
/v1/orders/search?user_id=<user_id>
для поиска заказов через метод GET
(простые случаи) или POST
(если необходимы сложные фильтры);
PUT /v1/orders/{id}/archive
для архивирования заказа.
-плюс, вероятно, потребуются частные операции типа POST /v1/orders/{id}/cancel
для проведения атомарных изменений. Именно это произойдёт в реальной жизни: идея CRUD как методологии описания типичных операций над ресурсом с помощью небольшого набора HTTP-глаголов быстро превратится в семейство эндпойнтов, каждый из которых покрывает какой-то аспект жизненного цикла сущности. Это всего лишь означает, что CRUD-мнемоника даёт только стартовый набор гипотез; любая конкретная предметная область требует вдумчивого подхода и дизайна подходящего API. Если же перед вами стоит задача разработать «универсальный» интерфейс, который подходит для работы с любыми сущностями, лучше сразу начинайте с номенклатуры в 10 методов типа описанной выше.
Примечания
+плюс, вероятно, потребуются частные операции типа POST /v1/orders/{id}/cancel
для проведения атомарных изменений. Именно это произойдёт в реальной жизни: идея CRUD как методологии описания типичных операций над ресурсом с помощью небольшого набора HTTP-глаголов быстро превратится в семейство эндпойнтов, каждый из которых покрывает какой-то аспект жизненного цикла сущности. Это всего лишь означает, что CRUD-мнемоника даёт только стартовый набор гипотез; любая конкретная предметная область требует вдумчивого подхода и дизайна подходящего API. Если же перед вами стоит задача разработать «универсальный» интерфейс, который подходит для работы с любыми сущностями, лучше сразу начинайте с номенклатуры в 10 методов типа описанной выше.
Примечания
Рассмотренные в предыдущих главах примеры организации API согласно стандарту HTTP и принципам REST покрывают т.н. «happy path», т.е. стандартный процесс работы с API в отсутствие ошибок. Конечно, нам не менее интересен и обратный кейс — каким образом HTTP API следует работать с ошибками, и чем стандарт и архитектурные принципы могут нам в этом помочь. Пусть какой-то агент в системе (неважно, клиент или гейтвей) пытается создать новый заказ:
POST /v1/orders?user_id=<user_id> HTTP/1.1
Authorization: Bearer <token>
@@ -5515,7 +5515,7 @@ If-Match: <ревизия>
429 Too Many Requests
при превышении лимитов.
Разработчики стандарта HTTP об этой проблеме вполне осведомлены, и отдельно отмечают, что для решения бизнес-сценариев необходимо передавать в метаданных либо теле ответа дополнительные данные для описания возникшей ситуации («the server SHOULD send a representation containing an explanation of the error situation, and whether it is a temporary or permanent condition»), что (как и введение новых специальных кодов ошибок) противоречит самой идее унифицированного машиночитаемого формата ошибок. (Отметим, что отсутствие стандартов описания ошибок в бизнес-логике — одна из основных причин, по которым мы считаем разработку REST API как его описал Филдинг в манифесте 2008 года невозможной; клиент должен обладать априорным знанием о том, как работать с метаинформацией об ошибке, иначе он сможет восстанавливать своё состояние после ошибки только перезагрузкой.)
-NB : не так давно разработчики стандарта предложили собственную версию спецификации JSON-описания HTTP-ошибок — RFC 94571 . Вы можете воспользоваться ей, но имейте в виду, что она покрывает только самый базовый сценарий:
+NB : не так давно разработчики стандарта предложили собственную версию спецификации JSON-описания HTTP-ошибок — RFC 94571 . Вы можете воспользоваться ей, но имейте в виду, что она покрывает только самый базовый сценарий:
подтип ошибки не передаётся в мета-информации;
нет разделения на сообщение для пользователя и сообщение для разработчика;
@@ -5613,7 +5613,7 @@ Retry-After: 5
Применить смешанный подход, то есть использовать статус-код согласно его семантике для индикации рода ошибки и вложенные (мета)данные в специально разработанном формате для детализации (подобно фрагментам кода, предложенным нами в настоящей главе).
-Как нетрудно заметить, считать соответствующим стандарту можно только подход (3). Будем честны и скажем, что выгоды следования ему, особенно по сравнению с вариантом (2а), не очень велики и состоят в основном в чуть лучшей читабельности логов и большей прозрачности для промежуточных прокси.
Примечания
+Как нетрудно заметить, считать соответствующим стандарту можно только подход (3). Будем честны и скажем, что выгоды следования ему, особенно по сравнению с вариантом (2а), не очень велики и состоят в основном в чуть лучшей читабельности логов и большей прозрачности для промежуточных прокси.
Примечания
Подведём итог описанному в предыдущих главах. Чтобы разработать качественный HTTP API, необходимо:
Описать happy path, т.е. составить диаграмму вызовов для стандартного цикла работы клиентского приложения.
@@ -5632,7 +5632,7 @@ Retry-After: 5
Включайте в ответы стандартные заголовки — Date
, Content-Type
, Content-Encoding
, Content-Length
, Cache-Control
, Retry-After
— и вообще старайтесь не полагаться на то, что клиент правильно догадывается о параметрах протокола по умолчанию.
-Поддержите метод OPTIONS
и протокол CORS1 на случай, если ваш API захотят использовать из браузеров.
+Поддержите метод OPTIONS
и протокол CORS1 на случай, если ваш API захотят использовать из браузеров.
Определитесь с правилами выбора кейсинга параметров (и преобразований кейсинга при перемещении параметра между различными частями запроса) и придерживайтесь их.
@@ -5659,20 +5659,20 @@ Retry-After: 5
Ознакомьтесь хотя бы с основными видами уязвимостей в типичных имплементациях HTTP API, которыми могут воспользоваться злоумышленники:
-CSRF2
-SSRF3
-HTTP Response Splitting4
-Unvalidated Redirects and Forwards5
+CSRF2
+SSRF3
+HTTP Response Splitting4
+Unvalidated Redirects and Forwards5
-и заложите защиту от этих векторов атак на уровне вашего серверного ПО. Организация OWASP предоставляет хороший обзор лучших security-практик для HTTP API6 .
+и заложите защиту от этих векторов атак на уровне вашего серверного ПО. Организация OWASP предоставляет хороший обзор лучших security-практик для HTTP API6 .
-В заключение хотелось бы сказать следующее: HTTP API — это способ организовать ваше API так, чтобы полагаться на понимание семантики операций как разнообразным программным обеспечением, от клиентских фреймворков до серверных гейтвеев, так и разработчиком, который читает спецификацию. В этом смысле экосистема HTTP предоставляет пожалуй что наиболее широкий (и в плане глубины, и в плане распространённости) по сравнению с другими технологиями словарь для описания самых разнообразных ситуаций, возникающих во время работы клиент-серверных приложений. Разумеется, эта технология не лишена своих недостатков, но для разработчика публичного API она является выбором по умолчанию — на сегодняшний день скорее надо обосновывать отказ от HTTP API чем выбор в его пользу.
Примечания
+В заключение хотелось бы сказать следующее: HTTP API — это способ организовать ваше API так, чтобы полагаться на понимание семантики операций как разнообразным программным обеспечением, от клиентских фреймворков до серверных гейтвеев, так и разработчиком, который читает спецификацию. В этом смысле экосистема HTTP предоставляет пожалуй что наиболее широкий (и в плане глубины, и в плане распространённости) по сравнению с другими технологиями словарь для описания самых разнообразных ситуаций, возникающих во время работы клиент-серверных приложений. Разумеется, эта технология не лишена своих недостатков, но для разработчика публичного API она является выбором по умолчанию — на сегодняшний день скорее надо обосновывать отказ от HTTP API чем выбор в его пользу.
Примечания
Как мы отмечали во Введении, аббревиатура «SDK» («Software Development Kit»), как и многие из обсуждавшихся ранее терминов, не имеет конкретного значения. Считается, что SDK отличается от API тем, что помимо программных интерфейсов содержит и готовые инструменты для работы с ними. Определение это, конечно, лукавое, поскольку почти любая технология сегодня идёт в комплекте со своим набором инструментов.
Тем не менее, у термина SDK есть и более узкое значение, в котором он часто используется: это клиентская библиотека, которая предоставляет высокоуровневый (обычно, нативный) интерфейс для работы с некоторой нижележащей платформой (и в частности с клиент-серверным API). Чаще всего речь идёт о библиотеках для мобильных ОС или веб браузеров, которые работают поверх HTTP API сервиса общего назначения.
Среди подобных клиентских SDK особо выделяются те, которые предоставляют не только программные интерфейсы для работы с API, но также и готовые визуальные компоненты, которые разработчик может использовать. Классический пример такого SDK — это библиотеки картографических сервисов; в силу исключительной сложности самостоятельной реализации движка работы с картами (особенно векторными) вендоры API карт предоставляют и «обёртки» к HTTP API (например, поисковому), и готовые библиотеки для работы с географическими сущностями, которые часто включают в себя и визуальные компоненты общего назначения — кнопки, метки, контекстные меню — которые могут применяться и совершенно самостоятельно вне контекста API (SDK) как такового.
@@ -5684,21 +5684,21 @@ Retry-After: 5
Во избежание нагромождения подобных оборотов мы будем называть первый тип инструментов просто «SDK», а второй — «UI-библиотеки».
NB : вообще говоря, UI-библиотека может как включать в себя обёртку над клиент-серверным API, так и предоставлять чистый API к графическому движку. В рамках этой книги мы будем говорить, в основном, о первом варианте как о более общем случае (и более сложном с точки зрения проектирования API). Многие паттерны дизайна SDK, которые мы опишем далее, применимы и к «чистым» библиотекам без клиент-серверной составляющей.
Выбор фреймворка для разработки UI-компонентов
-Поскольку UI — высокоуровневая абстракция над примитивами ОС, почти для всех платформ существуют специализированные фреймворки для разработки визуальных компонентов. Выбор такого фреймворка, увы, может быть непростым занятием. Например, в случае веб-платформы её низкоуровневость и популярность привели к тому, что на сегодня количество конкурирующих технологий, предоставляющих фреймворки для разработки SDK, превосходит всякое воображение. Можно упомянуть наиболее распространённые на сегодня React1 , Angular2 , Svelte3 , Vue.js4 и не сдающие своих позиций Bootstrap5 и Ember6 . Из перечисленных технологий наибольшее проникновение имеет React, но оно всё ещё измеряется в единицах процентов7 . При этом компоненты на «чистом» JavaScript/CSS при этом часто критикуются как неудобные к использованию в перечисленных фреймворках, так как каждый из них реализует весьма строгие методологии. Примерно такая же ситуация наблюдается, например, с разработкой визуальных библиотек для Windows. Вопрос «на каком фреймворке разрабатывать UI-компоненты для этих платформ», увы, не имеет простого ответа — фактически, вам придётся измерять рынки и принимать решения по каждому фреймворку отдельно.
+Поскольку UI — высокоуровневая абстракция над примитивами ОС, почти для всех платформ существуют специализированные фреймворки для разработки визуальных компонентов. Выбор такого фреймворка, увы, может быть непростым занятием. Например, в случае веб-платформы её низкоуровневость и популярность привели к тому, что на сегодня количество конкурирующих технологий, предоставляющих фреймворки для разработки SDK, превосходит всякое воображение. Можно упомянуть наиболее распространённые на сегодня React1 , Angular2 , Svelte3 , Vue.js4 и не сдающие своих позиций Bootstrap5 и Ember6 . Из перечисленных технологий наибольшее проникновение имеет React, но оно всё ещё измеряется в единицах процентов7 . При этом компоненты на «чистом» JavaScript/CSS при этом часто критикуются как неудобные к использованию в перечисленных фреймворках, так как каждый из них реализует весьма строгие методологии. Примерно такая же ситуация наблюдается, например, с разработкой визуальных библиотек для Windows. Вопрос «на каком фреймворке разрабатывать UI-компоненты для этих платформ», увы, не имеет простого ответа — фактически, вам придётся измерять рынки и принимать решения по каждому фреймворку отдельно.
Лучше дела обстоят с актуальными мобильными платформами (а также MacOS), которые гораздо более гомогенны. Однако здесь возникает другая проблема — современные приложения, как правило, поддерживают сразу несколько таких платформ, что приводит к дублированию кода (и номенклатуры API).
-Решением этой проблемы может быть использование кросс-платформенных мобильных (React Native8 , Flutter9 , Xamarin10 ) и десктопных (JavaFX11 , QT12 ) фреймворков, а также узкоспециализированных решений для конкретных задач (например, Unity13 для разработки игр). Несомненным преимуществом таких технологий является скорость разработки и универсальность (как кода, так и программистов). Недостатки также достаточно очевидны — от таких приложений может быть сложно добиться оптимальной производительности, и к ним часто неприменимы многие стандартные инструменты, доступные для конкретной платформы; например, отладка и профайлинг могут быть затруднены. На сегодня скорее наблюдается паритет между двумя этими подходами (несколько фактически независимых приложений, написанных на поддерживаемых платформой языках vs. одно кросс-плафторменное приложение).
Примечания
+Решением этой проблемы может быть использование кросс-платформенных мобильных (React Native8 , Flutter9 , Xamarin10 ) и десктопных (JavaFX11 , QT12 ) фреймворков, а также узкоспециализированных решений для конкретных задач (например, Unity13 для разработки игр). Несомненным преимуществом таких технологий является скорость разработки и универсальность (как кода, так и программистов). Недостатки также достаточно очевидны — от таких приложений может быть сложно добиться оптимальной производительности, и к ним часто неприменимы многие стандартные инструменты, доступные для конкретной платформы; например, отладка и профайлинг могут быть затруднены. На сегодня скорее наблюдается паритет между двумя этими подходами (несколько фактически независимых приложений, написанных на поддерживаемых платформой языках vs. одно кросс-плафторменное приложение).
Примечания
Первый вопрос об SDK (напомним, так мы будем называть нативную клиентскую библиотеку, предоставляющую доступ к technology-agnostic клиент-серверному API), который мы должны прояснить — почему вообще такое явление как SDK существует. Иными словами, почему использование обёртки для фронтенд-разработчика является более удобным, нежели работа с нижележащим API напрямую.
Некоторые причины лежат на поверхности:
@@ -5911,7 +5911,7 @@ api.subscribe (
собственно функциональность нижележащего API;
принятые в рамках платформы концепции визуализации данных и взаимодействия с элементами управления (возможно, творчески развитые нашими разработками).
-Эти две предметных области могут находиться весьма далеко друг от друга. Более того, чем более интерфейс приближен к сырым данным, тем он, как правило, менее удобен для пользователя (как классический пример — огромные формы ввода данных1 ). Если мы хотим построить эргономичный интерфейс, нам придётся заменить формы сложными интерфейсными надстройками над данными и над графическими примитивами платформы, причём имеющими собственное состояние. Это, в свою очередь, ведёт к накоплению проблем в архитектуре SDK:
+Эти две предметных области могут находиться весьма далеко друг от друга. Более того, чем более интерфейс приближен к сырым данным, тем он, как правило, менее удобен для пользователя (как классический пример — огромные формы ввода данных1 ). Если мы хотим построить эргономичный интерфейс, нам придётся заменить формы сложными интерфейсными надстройками над данными и над графическими примитивами платформы, причём имеющими собственное состояние. Это, в свою очередь, ведёт к накоплению проблем в архитектуре SDK:
Посмотрим на панель действий с предложениями. Допустим, мы размещаем на ней две кнопки — «заказать» и «показать на карте» — плюс действие «отменить». Эти кнопки выглядят одинаково и реагируют на действия пользователя одинаково — но при этом осуществляют абсолютно не имеющие ничего общего друг с другом действия.
Допустим, мы предоставили программисту возможность добавить свои кнопки действий на панель, для чего предоставим в составе SDK класс Button
. Достаточно быстро мы выясним, что этой функциональностью будут пользоваться в двух основных диаметрально противоположных сценариях:
@@ -5955,12 +5955,12 @@ api.subscribe (
});
return res;
}
-})
+});
Формально этот подход корректен и никаких рекомендаций не нарушает. Но с точки зрения связности кода, его читабельности — это полная катастрофа, поскольку следующий разработчик, которого попросят заменить иконку кнопке , очень вряд ли пойдёт читать код функции поиска предложений .
Если бы возможность кастомизации вообще не предоставлялась, эту функциональность было бы гораздо проще поддерживать. Да, разработчики были бы не рады необходимости разработать с нуля собственную панель поиска просто для замены иконки. Но в их коде замена иконки хотя бы будет находиться в ожидаемом месте — где-то в функции рендеринга панели.
NB : существует много других возможностей позволить разработчику кастомизировать кнопку, запрятанную где-то глубоко в дебрях компонента: разрешить dependency injection или переопределение фабрик суб-компонентов, предоставить прямой доступ к отрендеренному представлению компонента, настроить пользовательские макеты кнопок и так далее. Все они страдают от той же проблемы: крайне сложно консистентно описать порядок и приоритет применения инъекций / обработчиков событий рендеринга / пользовательских шаблонов.
-С решением вышеуказанных проблем, увы, всё обстоит очень сложно. В следующих главах мы рассмотрим паттерны проектирования, позволяющие в том числе разделить области ответственности составляющих компонента; но очень важно уяснить одну важную мысль: полное разделение, то есть разработка функционального SDK+UI, дающего разработчику свободу в переопределении и внешнего вида, и бизнес-логики, и UX компонентов — невероятно дорогая в разработке задача, которая в лучшем случае утроит вашу иерархию абстракций. Универсальный совет здесь ровно один: три раза подумайте прежде чем предоставлять возможность программной настройки UI-компонентов . Хотя цена ошибки дизайна программных интерфейсов для UI-библиотек, как правило, не очень высока (вряд ли клиент потребует рефанд из-за неработающей анимации нажатия кнопки), плохо структурированный, нечитабельный и глючный SDK вряд ли может рассматриваться как сильное клиентское преимущество вашего API.
Примечания
+С решением вышеуказанных проблем, увы, всё обстоит очень сложно. В следующих главах мы рассмотрим паттерны проектирования, позволяющие в том числе разделить области ответственности составляющих компонента; но очень важно уяснить одну важную мысль: полное разделение, то есть разработка функционального SDK+UI, дающего разработчику свободу в переопределении и внешнего вида, и бизнес-логики, и UX компонентов — невероятно дорогая в разработке задача, которая в лучшем случае утроит вашу иерархию абстракций. Универсальный совет здесь ровно один: три раза подумайте прежде чем предоставлять возможность программной настройки UI-компонентов . Хотя цена ошибки дизайна программных интерфейсов для UI-библиотек, как правило, не очень высока (вряд ли клиент потребует рефанд из-за неработающей анимации нажатия кнопки), плохо структурированный, нечитабельный и глючный SDK вряд ли может рассматриваться как сильное клиентское преимущество вашего API.
Примечания
Перейдём к более предметному разговору и попробуем объяснить, почему требование возможности замены одной из подсистем компонента приводит к кратному усложнению интерфейсов. Продолжим рассматривать пример компонента SearchBox
из предыдущей главы. Напомним, что мы выделили следующие факторы, осложняющие проектирование API визуальных компонентов:
объединение в одном объекте разнородной функциональности, а именно — бизнес-логики, настройки внешнего вида и поведения компонента;
@@ -6509,7 +6509,7 @@ api.subscribe (
наконец, SearchBox
никак не взаимодействует ни с тем, ни с другим — только лишь предоставляет контекст, методы его изменения и нотификации.
-Сделав подобное упрощение, мы фактически получили компонент, следующий методологии «Model-View-Controller» (MVC)1 : OfferList
и OfferPanel
(а также код показа поля ввода) — это различные view, которые непосредственно наблюдает пользователь и взаимодействует с ними; Composer
— это controller, который получает события от view и модифицирует модель (сам SearchBox
).
+Сделав подобное упрощение, мы фактически получили компонент, следующий методологии «Model-View-Controller» (MVC)1 : OfferList
и OfferPanel
(а также код показа поля ввода) — это различные view, которые непосредственно наблюдает пользователь и взаимодействует с ними; Composer
— это controller, который получает события от view и модифицирует модель (сам SearchBox
).
NB : следуя букве подхода, мы должны выделить из SearchBox
компонент-модель, который отвечает только за данные. Это упражнение мы оставим читателю.
Схема взаимодействия компонентов в MVC
@@ -6541,15 +6541,15 @@ api.subscribe (
Однако эта жёсткость влечёт за собой недостатки. Если задаться целью полностью описать состояние компонента, то мы обязаны внести в него и такие данные, как выполняющиеся сейчас анимации и даже процент их выполнения. Таким образом, модель обязана будет содержать в себе все данные всех уровней абстракции и, более того, каким-то образом включать в себя две или более иерархии подчинения (по семантической и визуальной иерархиям, а так же, возможно, вычисляемые значения опций). В нашем примере это означает, например, что модель должна будет хранить и currentSelectedOffer
для OfferPanel
, и список показанных кнопок, и даже вычисленные значения иконок для кнопок.
Подобная полная модель представляет собой проблему не только теоретически и семантически (перемешивание в одной сущности разнородных данных), но и в практическом смысле — сериализация таких моделей окажется ограничена рамками конкретной версии API или приложения (поскольку они содержат все внутренние переменные, включая непубличные). Если мы в следующей версии изменим реализацию субкомпонентов, то старые ссылки и закэшированные состояния перестанут работать (либо нам потребуется держать слой совместимости, описывающий, как интерпретировать модели предыдущих версий).
Другая идеологическая проблема подхода — организация вложенных контроллеров. В системе с дочерними субкомпонентами все те проблемы, которые решил MV*-подход, возвращаются на новом уровне: нам придётся разрешить вложенным контроллерам либо перепрыгивать через уровни абстракции и модифицировать корневую модель, либо вызывать методы контроллеров родительских компонент. Оба подхода влекут за собой сильную связность сущностей и требуют очень аккуратного проектирования, иначе переиспользование компонентов окажется затруднено.
-Если мы внимательно посмотрим на современные UI библиотеки, выполненные в MV*-парадигмах, то увидим, что они следуют парадигме весьма нестрого и лишь заимствуют из неё основной принцип: модель полностью определяет внешний вид компонента, и любые визуальные изменения инициируются через изменение модели контроллером. Дочерние компоненты при этом обычно имеют собственные модели (часто в виде подмножества родительской модели, дополненного собственным состоянием компонента), и в глобальной модели приложения находится только ограниченный набор полей. Этот подход адаптирован во многих современных UI-фреймворках, даже тех, которые от MV*-парадигм открещиваются (например, React2 3 ).
-На эти же проблемы MVC-подхода обращает внимание в своём эссе Мартин Фаулер в своём эссе «Архитектуры GUI»4 , и предлагает решение в виде фреймворка Model-View-Presenter (MVP), в котором место controller-а занимает сущность-presenter, в обязанности которой входит не только трансляция событий, но и подготовка данных для view, что позволяет разделить уровни абстракции (модель хранит только семантичные данные, описывающие предметную область, presenter — низкоуровневые данные, описывающие UI, в терминологии Фаулера — application model или presentation model).
+Если мы внимательно посмотрим на современные UI библиотеки, выполненные в MV*-парадигмах, то увидим, что они следуют парадигме весьма нестрого и лишь заимствуют из неё основной принцип: модель полностью определяет внешний вид компонента, и любые визуальные изменения инициируются через изменение модели контроллером. Дочерние компоненты при этом обычно имеют собственные модели (часто в виде подмножества родительской модели, дополненного собственным состоянием компонента), и в глобальной модели приложения находится только ограниченный набор полей. Этот подход адаптирован во многих современных UI-фреймворках, даже тех, которые от MV*-парадигм открещиваются (например, React2 3 ).
+На эти же проблемы MVC-подхода обращает внимание в своём эссе Мартин Фаулер в своём эссе «Архитектуры GUI»4 , и предлагает решение в виде фреймворка Model-View-Presenter (MVP), в котором место controller-а занимает сущность-presenter, в обязанности которой входит не только трансляция событий, но и подготовка данных для view, что позволяет разделить уровни абстракции (модель хранит только семантичные данные, описывающие предметную область, presenter — низкоуровневые данные, описывающие UI, в терминологии Фаулера — application model или presentation model).
Схема взаимодействия компонентов в MVP
Предложенная Фаулером парадигма во многом схожа с концепцией Composer
-а, которую мы обсуждали в предыдущей главе, но с одним заметным различием. По мысли Фаулера собственного состояния у presenter-а нет (за исключением, возможно, кэшей и замыканий), он только вычисляет данные, необходимые для показа визуального интерфейса, из данных модели. Если необходимо манипулировать каким-то низкоуровневым свойством, например, цветом текста в интерфейсе, то нужно разработать модель так, чтобы цвет текста вычислялся presenter-ом из какого-то высокоуровневого поля в модели (возможно, искусственно введённого), что ограничивает возможности альтернативных имплементаций субкомпонентов.
-NB : на всякий случай уточним, что автор этой книги не предлагает Composer
как альтернативную MV*-методологию. Идея предыдущей главы состоит в том, что сложные сценарии декомпозиции UI-компонентов решаются только искусственным введением мостиков-уровней абстракции. Неважно, как мы этот мостик назовём и какие правила для него придумаем.
Примечания
+NB : на всякий случай уточним, что автор этой книги не предлагает Composer
как альтернативную MV*-методологию. Идея предыдущей главы состоит в том, что сложные сценарии декомпозиции UI-компонентов решаются только искусственным введением мостиков-уровней абстракции. Неважно, как мы этот мостик назовём и какие правила для него придумаем.
Примечания
Другой способ обойти сложность «переброса мостов» между несколькими предметными областями, которые нам приходится сводить в рамках одного UI-компонента — это убрать одну из них. Как правило, речь идёт о бизнес-логике: мы можем разработать компоненты полностью абстрактными, и скрыть все трансляции UI-событий в полезные действия вне контроля разработчика.
В такой парадигме код поиска предложений выглядел бы так:
class SearchBox {
@@ -6726,11 +6726,11 @@ api.subscribe (
В таком подходе мы можем реализовать не только блокировки, но и различные сценарии, которые позволяют нам более гибко ими управлять. Добавим в функцию захвата ресурса дополнительные данные о целях захвата:
lock = await this .acquireLock (
'offerFullView' , '10s' , {
-
-
-
- reason : 'userSelectOffer' ,
- offer
+
+
+
+ reason : 'userSelectOffer' ,
+ offer
}
);
@@ -6757,7 +6757,82 @@ api.subscribe (
асинхронно обновить отображение при получении ответа от сервера.
Однако в постановке проблемы это ничего не меняет: нам всё ещё нужно разработать политику разрешения конфликтов для случая, если какой-то актор пытается открыть панель предложения, пока загрузка данных ещё не закончена, для чего нам вновь нужны разделяемые ресурсы и их захват.
-Отметим, что в современном фронтенде (к нашему большому сожалению) подобные упражнения с захватом контроля на время загрузки данных или анимации компонентов практически не производятся (считается, что такие асинхронные операции происходят быстро, и коллизии доступа не представляют собой проблемы). Однако, если асинхронные операции выполняются долго (происходят длительные или многоступенчатые загрузки данных, сложные анимации), пренебрежение организацией доступа может быть очень серьёзной UX-проблемой.
+Отметим, что в современном фронтенде (к нашему большому сожалению) подобные упражнения с захватом контроля на время загрузки данных или анимации компонентов практически не производятся (считается, что такие асинхронные операции происходят быстро, и коллизии доступа не представляют собой проблемы). Однако, если асинхронные операции выполняются долго (происходят длительные или многоступенчатые загрузки данных, сложные анимации), пренебрежение организацией доступа может быть очень серьёзной UX-проблемой.
+Вернёмся к одной из проблем, описанных в главе «Проблемы встраивания UI-компонентов »: наличие множественных линий наследования усложняет кастомизация компонентов, поскольку подразумевает, что они могут наследовать важные свойства по любой из вертикалей.
+Пусть у нас имеется кнопка, которая получает одно и то же свойство iconUrl
по двум вертикалям — из данных [т.е., в случае нашего примера, из результатов поиска предложений] и из настроек отображения:
+class Button {
+ static DEFAULT_OPTIONS = {
+ …
+ iconUrl : <иконка по умолчанию>
+ }
+
+ constructor (data, options) {
+ this .data = data;
+
+
+ this .options = extend (
+ Button .DEFAULT_OPTIONS ,
+ options
+ )
+ }
+
+ render ( ) {
+ …
+ this .iconElement .src =
+ this .data .iconUrl ||
+ this .options .iconUrl
+ }
+}
+
+При этом мы можем легко представить себе, что по обеим иерархиям свойство iconUrl
было получено от кого-то из родителей по любой из вертикалей:
+
+опции по умолчанию могут быть определены в базовом классе, от которого унаследован Button
;
+данные, на которых строится кнопка, могут быть общими для группы кнопок или сами по себе быть иерархическими (например, если мы будем группировать предложения сети кофеен и наследовать иконку именно родительской группы);
+в целях облегчить кастомизацию визуального стиля компонент мы можем разрешить переопределять иконку вообще всем кнопкам через задание свойств по умолчанию для всего SDK.
+
+В этой ситуации у нас возникает вопрос: если значение определено сразу в нескольких иерархиях (например, и в данных предложения, и в опциях по умолчанию), каким образом задавать приоритеты, чтобы выбирать одно из них?
+Простой подход «в лоб» к этому вопросу — попросту запретить наследование и заставить разработчика копировать все нужные ему свойства. То есть в нашем примере сам разработчик должен написать что-то типа:
+const button = new Button (data);
+if (data.createOrderButtonIconUrl ) {
+ button.view .iconUrl =
+ data.createOrderButtonIconUrl ;
+} else if (data.parentCategory .iconUrl ) {
+ button.view .iconUrl =
+ data.parentCategory .iconUrl ;
+}
+
+Достоинства простого решения очевидны — разработчик сам имплементирует ту логику, которая ему нужна. Недостатки тоже очевидны — во-первых, это лишний и зачастую дублирующийся код; во-вторых, разработчик быстро запутается в том, какие правила он реализовал и почему.
+Чуть более сложный подход к проблеме — разрешить наследование, но строго зафиксировать приоритеты (скажем, заданное в опциях отображения значение всегда важнее заданного в данных, и они оба всегда важнее любого унаследованного свойства). Однако в достаточно сложном API результат будет тот же самым: если разработчику необходим другой порядок приоритетов, ему придётся задавать нужные свойства вручную, т.е. в итоге писать код, подобный вышеприведённому.
+Альтернативный подход — это предоставить возможность задавать правила, каким образом для конкретной кнопки определяется её иконка, декларативно или императивно:
+
+
+{
+ "button.checkout.iconUrl" : "@data.iconUrl"
+}
+
+
+
+api.options .addRule (
+ 'button.checkout.iconUrl' ,
+ (data, options ) => data.iconUrl
+)
+
+Наиболее последовательная реализация этого подхода — CSS1 . Мы не то чтобы рекомендуем использовать CSS-подобные правила в библиотеках компонентов (в силу потрясающей сложности их имплементации и, в большинстве случаев, избыточности), но осторожно замечаем, что поддержка какого-то простого подмножества подобного рода правил значительно облегчает кастомизацию визуальных компонент для разработчиков.
+Вычисленные значения
+Очень важно не забыть предоставить разработчику не только способы задать приоритеты параметров, но и возможность узнать, какой же из вариантов значения был применён. Для этого мы должны разделить заданные и вычисленные значения:
+
+button.view .width = '100%' ;
+
+
+button.view .computedStyle .width ;
+
+Хорошей практикой также будет предоставлять доступ не только к вычисляемым значениям, но и к событию изменения этого значения.
Примечания
+Предыдущие восемь глав были написаны нами, чтобы раскрыть две очень важные мысли:
+
+разработка качественной UI-библиотеки — это отдельная и весьма непростая инженерная задача;
+и эта задача не сводится к автоматической генерации SDK по спецификации / модели данных.
+
+Оглядываясь на всё написанное, мы с трудом можем сказать, что нашли лучшие примеры и самые понятные слова для описания такой сложной предметной области. Мы, тем не менее, надеемся, что сделали вашу жизнь — и жизнь ваших пользователей — чуточку проще.
Когда мы говорим об API как о продукте, необходимо чётко зафиксировать два важных тезиса.
@@ -6985,7 +7060,7 @@ api.subscribe (
Базовый уровень — работы с продуктовыми сущностями через формальные интерфейсы. [В случае нашего учебного API этому уровню соответствует HTTP API заказа.]
Упростить работу с продуктовыми сущностями можно, предоставив SDK для различных платформ, скрывающие под собой сложности работы с формальными интерфейсами и адаптирующие концепции API под соответствующие парадигмы (что позволяет разработчикам, знакомым только с конкретной платформой, не тратить время и не разбираться в формальных интерфейсах и протоколах).
Ещё более упростить работу можно с помощью сервисов, генерирующих код. В таком интерфейсе разработчик выбирает один из представленных шаблонов интеграции, кастомизирует некоторые параметры, и получает на выходе готовый фрагмент кода, который он может вставить в своё приложение (и, возможно, дописать необходимую функциональность с использованием API 1-3 уровней). Подобного рода подход ещё часто называют «программированием мышкой». [В случае нашего кофейного API примером такого сервиса мог бы служить визуальный редактор форм/экранов, в котором пользователь расставляет UI элементы и получает полный код приложения, или консольный скрипт, который генерирует «скелет» приложения.]
-Ещё более упростить такой подход можно, если результатом работы такого сервиса будет уже не код поверх API, а готовый компонент / виджет / фрейм, подключаемый одной строкой. [Например, если мы дадим возможность разработчику вставлять на свой сайт iframe, в котором можно заказать кофе, кастомизированный под нужды заказчика, либо, ещё проще, описать правила формирования «deep link-а»1 , который приведёт пользователя на наш сервис.]
+Ещё более упростить такой подход можно, если результатом работы такого сервиса будет уже не код поверх API, а готовый компонент / виджет / фрейм, подключаемый одной строкой. [Например, если мы дадим возможность разработчику вставлять на свой сайт iframe, в котором можно заказать кофе, кастомизированный под нужды заказчика, либо, ещё проще, описать правила формирования «deep link-а»1 , который приведёт пользователя на наш сервис.]
В конечном итоге можно прийти к концепции мета-API, когда готовые визуальные компоненты тоже будут иметь какое-то свой высокоуровневый API, который «под капотом» будет обращаться к базовым API.
Важным преимуществом наличия линейки продуктов API является не только возможность адаптировать его к возможностям конкретного разработчика, но и увеличение уровня вашего контроля над кодом, встроенным в приложение партнёра.
@@ -6997,7 +7072,7 @@ api.subscribe (
Наконец, готовые компоненты и виджеты находятся полностью под вашим контролем, и вы можете экспериментировать с доступной через них функциональностью так же свободно, как если бы это было ваше собственное приложение. (Здесь следует, правда, отметить, что не всегда от этого контроля есть толк: например, если вы позволяете вставлять изображение по прямому URL, ваш контроль над этой интеграцией практически отсутствует; при прочих равных следует выбирать тот вид интеграции, который позволяет получить больший контроль над соответствующей функциональностью в приложении партнёра.)
NB . При разработке «вертикального» семейства API замечания, описанные в главе «О ватерлинии айсберга» особенно важны. Вы можете свободно манипулировать контентом и поведением виджета, если и только если у разработчика нет способа «сбежать из песочницы», т.е. напрямую получить низкоуровневый доступ к объектам внутри виджета.
-Как правило, вы должны стремиться к тому, чтобы каждый партнёрский сервис использовал тот вид API, который вам как разработчику наиболее выгоден. Там, где партнёр не стремится создать какую-то уникальную функциональность и размещает типовое решение, вам выгодно иметь виджет, который полностью находится под вашим контролем и, с одной стороны, снимает с вас головную боль относительно обновления версий API, и, с другой стороны, даёт вам свободу экспериментировать с внешним видом и поведением интеграций с целью оптимизации ваших KPI. Там, где партнёр обладает экспертизой и желанием разработать какой-то уникальный сервис поверх вашего API, вам выгодно предоставить ему максимальную свободу действий, чтобы, во-первых, покрыть тем самым уникальные продуктовые ниши, и, во-вторых, обладать конкурентным преимуществом в виде возможности глубокой кастомизации относительно других API на рынке.
Примечания
+Как правило, вы должны стремиться к тому, чтобы каждый партнёрский сервис использовал тот вид API, который вам как разработчику наиболее выгоден. Там, где партнёр не стремится создать какую-то уникальную функциональность и размещает типовое решение, вам выгодно иметь виджет, который полностью находится под вашим контролем и, с одной стороны, снимает с вас головную боль относительно обновления версий API, и, с другой стороны, даёт вам свободу экспериментировать с внешним видом и поведением интеграций с целью оптимизации ваших KPI. Там, где партнёр обладает экспертизой и желанием разработать какой-то уникальный сервис поверх вашего API, вам выгодно предоставить ему максимальную свободу действий, чтобы, во-первых, покрыть тем самым уникальные продуктовые ниши, и, во-вторых, обладать конкурентным преимуществом в виде возможности глубокой кастомизации относительно других API на рынке.
Примечания
Как мы описали выше, существует большое количество различных моделей монетизации API, прямой и косвенной. Важной их особенностью является то, что, во-первых, большинство из них является частично или полностью бесплатными для партнёра, а, во-вторых, соотношение прямой и косвенной выгоды часто меняется в течение жизненного цикла API. Возникает вопрос, каким же образом следует измерять успех API и какие цели ставить продуктовой команде.
Разумеется, наиболее прямым измеримым показателем являются деньги: если ваш API имеет прямую монетизацию либо явно приводит пользователей на монетизируемый сервис, то остальная часть главы будет вам интересна разве что в рамках общего развития. Если же напрямую измерить вклад API в доходы компании не получается, придётся прибегать к иным, синтетическим, показателям.
Очевидный ключевой показатель эффективности (Key Performance Indicator, KPI) №1 — это количество конечных пользователей и количество интеграций (читай, партнёров, использующих API). В нормальной ситуации он является в определённом смысле барометром состояния бизнеса: если предположить, что на рынке наблюдается здоровая конкуренция между API разных поставщиков, и все находятся в более-менее одинаковом положении, то количество использующих API разработчиков (и как производная — конечных пользователей) и есть главный показатель успеха продукта.
diff --git a/docs/API.ru.pdf b/docs/API.ru.pdf
index 2007502..052496b 100644
Binary files a/docs/API.ru.pdf and b/docs/API.ru.pdf differ
diff --git a/docs/index.html b/docs/index.html
index 34cd214..767e68e 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -42,7 +42,7 @@
Subscribe for updates on
Follow me on · · Substack
- Support this work on Patreon · buy Kindle version [1st edition]
+ Support this work on Patreon · buy Kindle version
Share: · · · ⚙️⚙️⚙️
API-first development is one of the hottest technical topics nowadays since many companies have started to realize that APIs serves as a multiplier to their opportunities — but it amplifies the design mistakes as well.
@@ -123,7 +123,7 @@