1
0
mirror of https://github.com/twirl/The-API-Book.git synced 2025-03-17 20:42:26 +02:00
This commit is contained in:
Sergey Konstantinov 2022-02-06 15:54:34 +03:00
parent 9db7f5a58e
commit 422f22d567
25 changed files with 114 additions and 110 deletions

Binary file not shown.

Binary file not shown.

View File

@ -331,14 +331,15 @@ h1 {
<a href="https://www.patreon.com/yatwirl">www.patreon.com/yatwirl</a>
</p>
<p>
The API-first development is one of the hottest technical topics in
2020, since many companies started to realize that API serves as a
The API-first development is one of the hottest technical topics
nowadays, since many companies started to realize that API serves as a
multiplicator to their opportunities—but it also amplifies the design
mistakes as well.
</p>
<p>
The book is dedicated to designing APIs: how to build the architecture
properly, from a high-level planning down to final interfaces.
properly, from a high-level planning down to final interfaces, and to
extend API in a backwards-compatible manner.
</p>
<p>
Illustrations by Maria Konstantinova<br><a href="https://www.instagram.com/art.mari.ka/">www.instagram.com/art.mari.ka/</a>

Binary file not shown.

Binary file not shown.

View File

@ -2,11 +2,11 @@
<meta charset="utf-8">
<title>Сергей Константинов. API</title>
<meta name="author" content="Сергей Константинов">
<meta name="description" content="Разработка API — особый навык: API является как мультипликатором ваших возможностей, так и мультипликатором ваших ошибок. Эта книга написана для того, чтобы поделиться опытом и изложить лучшие практики проектирования API. Книга состоит из трёх больших разделов. В первом разделе мы поговорим о проектировании API на стадии разработки концепции — как грамотно выстроить архитектуру, от крупноблочного планирования до конечных интерфейсов. Второй раздел посвящён развитию существующих API с сохранением обратной совместимости">
<meta name="description" content="Разработка API — особый навык: API является как мультипликатором ваших возможностей, так и мультипликатором ваших ошибок. Эта книга написана для того, чтобы поделиться опытом и изложить лучшие практики проектирования API. Книга состоит из двух больших разделов. В первом разделе мы поговорим о проектировании API на стадии разработки концепции — как грамотно выстроить архитектуру, от крупноблочного планирования до конечных интерфейсов. Второй раздел посвящён развитию существующих API с сохранением обратной совместимости">
<meta property="og:title" content="Сергей Константинов. API">
<meta property="og:url" content="https://twirl.github.io/The-API-Book/docs/API.ru.html">
<meta property="og:type" content="article">
<meta property="og:description" content="Разработка API — особый навык: API является как мультипликатором ваших возможностей, так и мультипликатором ваших ошибок. Эта книга написана для того, чтобы поделиться опытом и изложить лучшие практики проектирования API. Книга состоит из трёх больших разделов. В первом разделе мы поговорим о проектировании API на стадии разработки концепции — как грамотно выстроить архитектуру, от крупноблочного планирования до конечных интерфейсов. Второй раздел посвящён развитию существующих API с сохранением обратной совместимости">
<meta property="og:description" content="Разработка API — особый навык: API является как мультипликатором ваших возможностей, так и мультипликатором ваших ошибок. Эта книга написана для того, чтобы поделиться опытом и изложить лучшие практики проектирования API. Книга состоит из двух больших разделов. В первом разделе мы поговорим о проектировании API на стадии разработки концепции — как грамотно выстроить архитектуру, от крупноблочного планирования до конечных интерфейсов. Второй раздел посвящён развитию существующих API с сохранением обратной совместимости">
<meta property="og:locale" content="ru_RU">
<style>@font-face {
@ -332,14 +332,15 @@ h1 {
</p>
<p>
«API-first» подход — одна из самых горячих горячих тем в разработке
программного обеспечения в 2020. Многие компании начали понимать, что
API выступает мультипликатором их возможностей — но также умножает и
программного обеспечения в наше время. Многие компании начали понимать,
что API выступает мультипликатором их возможностей — но также умножает и
допущенные ошибки.
</p>
<p>
Эта книга посвящена проектированию API: как правильно выстроить
архитектуру, начиная с высокоуровневого планирования и заканчивая
деталями реализации конкретных интерфейсов.
деталями реализации конкретных интерфейсов, и как развивать API, не
нарушая обратную совместимость.
</p>
<p>
Иллюстрации: Мария Константинова<br><a href="https://www.instagram.com/art.mari.ka/">www.instagram.com/art.mari.ka/</a>
@ -367,7 +368,7 @@ h1 {
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
<path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px" class="octo-arm"></path>
<path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a>
<div class="page-break"></div><nav><h2 class="toc">Содержание</h2><ul class="table-of-contents"><li><a href="#section-1">Введение</a><ul><li><a href="#chapter-1">Глава 1. О структуре этой книги</a></li><li><a href="#chapter-2">Глава 2. Определение API</a></li><li><a href="#chapter-3">Глава 3. Критерии качества API</a></li><li><a href="#chapter-4">Глава 4. Обратная совместимость</a></li><li><a href="#chapter-5">Глава 5. О версионировании</a></li><li><a href="#chapter-6">Глава 6. Условные обозначения и терминология</a></li></ul></li><li><a href="#section-2">Раздел I. Проектирование API</a><ul><li><a href="#chapter-7">Глава 7. Пирамида контекстов API</a></li><li><a href="#chapter-8">Глава 8. Определение области применения</a></li><li><a href="#chapter-9">Глава 9. Разделение уровней абстракции</a></li><li><a href="#chapter-10">Глава 10. Разграничение областей ответственности</a></li><li><a href="#chapter-11">Глава 11. Описание конечных интерфейсов</a></li><li><a href="#chapter-12">Глава 12. Приложение к разделу I. Модельное API</a></li></ul></li><li><a href="#section-3">Раздел II. Обратная совместимость</a><ul><li><a href="#chapter-13">Глава 13. Постановка проблемы обратной совместимости</a></li><li><a href="#chapter-14">Глава 14. О ватерлинии айсберга</a></li><li><a href="#chapter-15">Глава 15. Расширение через абстрагирование</a></li><li><a href="#chapter-16">Глава 16. Сильная связность и сопутствующие проблемы</a></li><li><a href="#chapter-17">Глава 17. Слабая связность</a></li><li><a href="#chapter-18">Глава 18. Интерфейсы как универсальный паттерн</a></li><li><a href="#chapter-19">Глава 19. Блокнот душевного покоя</a></li></ul></li></ul></nav><div style="page-break-after: always;"></div><h2><a href="#section-1" class="anchor" id="section-1">Введение</a></h2><h3><a href="#chapter-1" class="anchor" id="chapter-1">Глава 1. О структуре этой книги</a></h3>
<div class="page-break"></div><nav><h2 class="toc">Содержание</h2><ul class="table-of-contents"><li><a href="#section-1">Введение</a><ul><li><a href="#chapter-1">Глава 1. О структуре этой книги</a></li><li><a href="#chapter-2">Глава 2. Определение API</a></li><li><a href="#chapter-3">Глава 3. Критерии качества API</a></li><li><a href="#chapter-4">Глава 4. Обратная совместимость</a></li><li><a href="#chapter-5">Глава 5. О версионировании</a></li><li><a href="#chapter-6">Глава 6. Условные обозначения и терминология</a></li></ul></li><li><a href="#section-2">Раздел I. Проектирование API</a><ul><li><a href="#chapter-7">Глава 7. Пирамида контекстов API</a></li><li><a href="#chapter-8">Глава 8. Определение области применения</a></li><li><a href="#chapter-9">Глава 9. Разделение уровней абстракции</a></li><li><a href="#chapter-10">Глава 10. Разграничение областей ответственности</a></li><li><a href="#chapter-11">Глава 11. Описание конечных интерфейсов</a></li><li><a href="#chapter-12">Глава 12. Приложение к разделу I. Модельный API</a></li></ul></li><li><a href="#section-3">Раздел II. Обратная совместимость</a><ul><li><a href="#chapter-13">Глава 13. Постановка проблемы обратной совместимости</a></li><li><a href="#chapter-14">Глава 14. О ватерлинии айсберга</a></li><li><a href="#chapter-15">Глава 15. Расширение через абстрагирование</a></li><li><a href="#chapter-16">Глава 16. Сильная связность и сопутствующие проблемы</a></li><li><a href="#chapter-17">Глава 17. Слабая связность</a></li><li><a href="#chapter-18">Глава 18. Интерфейсы как универсальный паттерн</a></li><li><a href="#chapter-19">Глава 19. Блокнот душевного покоя</a></li></ul></li></ul></nav><div style="page-break-after: always;"></div><h2><a href="#section-1" class="anchor" id="section-1">Введение</a></h2><h3><a href="#chapter-1" class="anchor" id="chapter-1">Глава 1. О структуре этой книги</a></h3>
<p>Книга, которую вы держите в руках, состоит из введения и двух больших разделов (ещё один находится в разработке).</p>
<p>В первом разделе мы поговорим о проектировании API на стадии разработки концепции — как грамотно выстроить архитектуру, от крупноблочного планирования до конечных интерфейсов.</p>
<p>Второй раздел посвящён жизненному циклу API — как интерфейсы эволюционируют со временем и как развивать продукт так, чтобы отвечать потребностям пользователей.</p>
@ -389,22 +390,22 @@ h1 {
<p>Отличие древнеримского акведука от хорошего API состоит лишь в том, что API предлагает <em>программный</em> контракт. Для связывания двух областей необходимо написать некоторый <em>код</em>. Цель этой книги: помочь вам разработать API, так же хорошо выполняющий свою задачу, как и древнеримский акведук.</p>
<p>Акведук хорошо иллюстрирует и другую проблему разработки API: вашими пользователями являются инженеры. Вы не поставляете воду напрямую потребителю: к вашей инженерной мысли подключаются заказчики путём пристройки к ней каких-то своих инженерных конструкций. С одной стороны, вы можете обеспечить водой гораздо больше людей, нежели если бы вы сами подводили трубы к каждому крану. С другой — качество инженерных решений заказчика вы не можете контролировать, и проблемы с водой, вызванные некомпетентностью подрядчика, неизбежно будут валить на вас.</p>
<p>Именно поэтому проектирование API налагает на вас несколько большую ответственность. <strong>API является как мультипликатором ваших возможностей, так и мультипликатором ваших ошибок</strong>.</p><div class="page-break"></div><h3><a href="#chapter-3" class="anchor" id="chapter-3">Глава 3. Критерии качества API</a></h3>
<p>Прежде чем излагать рекомендации, нам следует определиться с тем, что мы считаем «хорошим» API, и какую пользу мы получаем от того, что наше API «хорошее».</p>
<p>Прежде чем излагать рекомендации, нам следует определиться с тем, что мы считаем «хорошим» API, и какую пользу мы получаем от того, что наш API «хороший».</p>
<p>Начнём со второго вопроса. Очевидно, «хорошесть» API определяется в первую очередь тем, насколько он помогает разработчикам решать стоящие перед ними задачи. (Можно резонно возразить, что решение задач, стоящих перед разработчиками, не обязательно влечёт за собой выполнение целей, которые мы ставим перед собой, предлагая разработчикам API. Однако манипуляция общественным мнением не входит в область интересов автора этой книги: здесь и далее предполагается, что API существует в первую очередь для того, чтобы разработчики решали с его помощью свои задачи, а не ради каких-то завуалированных целей).</p>
<p>Как же дизайн API может помочь разработчику? Очень просто: API должно решать задачи <em>максимально удобно и понятно</em>. Путь разработчика от формулирования своей задачи до написания работающего кода должен быть максимально коротким. Это, в том числе, означает, что:</p>
<p>Как же дизайн API может помочь разработчику? Очень просто: API должен решать задачи <em>максимально удобно и понятно</em>. Путь разработчика от формулирования своей задачи до написания работающего кода должен быть максимально коротким. Это, в том числе, означает, что:</p>
<ul>
<li>из структуры вашего API должно быть максимально очевидно, как решить ту или иную задачу; в идеале разработчику должно быть достаточно одного взгляда на документацию, чтобы понять, с помощью каких сущностей следует решать поставленную задачу;</li>
<li>API должно быть читаемым: в идеале разработчик, просто глядя в номенклатуру методов, сразу пишет правильный код, не углубляясь в детали (особенно — детали реализации!); немаловажно уточнить, что из интерфейсов объектов должно быть понятно не только решение задачи, но и возможные ошибки и исключения;</li>
<li>API должно быть консистентно: при разработке новой функциональности, т.е. при обращении к каким-то незнакомым сущностям в API, разработчик может действовать по аналогии с уже известными ему концепциями API, и его код будет работать.</li>
<li>API должен быть читаемым: в идеале разработчик, просто глядя в номенклатуру методов, сразу пишет правильный код, не углубляясь в детали (особенно — детали реализации!); немаловажно уточнить, что из интерфейсов объектов должно быть понятно не только решение задачи, но и возможные ошибки и исключения;</li>
<li>API должен быть консистентен: при разработке новой функциональности, т.е. при обращении к каким-то незнакомым сущностям в API, разработчик может действовать по аналогии с уже известными ему концепциями API, и его код будет работать.</li>
</ul>
<p>Однако статическое удобство и понятность API — это простая часть. В конце концов, никто не стремится специально сделать API нелогичным и нечитаемым — всегда при разработке мы начинаем с каких-то понятных базовых концепций. При минимальном опыте проектирования сложно сделать ядро API, не удовлетворяющее критериям очевидности, читаемости и консистентности.</p>
<p>Проблемы возникают, когда мы начинаем API развивать. Добавление новой функциональности рано или поздно приводит к тому, что некогда простое и понятное API становится наслоением разных концепций, а попытки сохранить обратную совместимость приводят к нелогичным, неочевидным и попросту плохим решениям. Отчасти это связано так же и с тем, что невозможно обладать полным знанием о будущем: ваше понимание о «правильном» API тоже будет меняться со временем, как в объективной части (какие задачи и каким образом решает API), так и в субъективной — что такое очевидность, читабельность и консистентность для вашего API.</p>
<p>Принципы, которые мы будем излагать ниже, во многом ориентированы именно на то, чтобы API правильно развивалось во времени и не превращалось в нагромождение разнородных неконсистентных интерфейсов. Важно понимать, что такой подход тоже не бесплатен: необходимость держать в голове варианты развития событий и закладывать возможность изменений в API означает избыточность интерфейсов и возможно излишнее абстрагирование. И то, и другое, помимо прочего, усложняет и работу программиста, пользующегося вашим API. <strong>Закладывание перспектив «на будущее» имеет смысл, только если это будущее у API есть, иначе это попросту оверинжиниринг</strong>.</p><div class="page-break"></div><h3><a href="#chapter-4" class="anchor" id="chapter-4">Глава 4. Обратная совместимость</a></h3>
<p>Проблемы возникают, когда мы начинаем API развивать. Добавление новой функциональности рано или поздно приводит к тому, что некогда простой и понятный API становится наслоением разных концепций, а попытки сохранить обратную совместимость приводят к нелогичным, неочевидным и попросту плохим решениям. Отчасти это связано так же и с тем, что невозможно обладать полным знанием о будущем: ваше понимание о «правильном» API тоже будет меняться со временем, как в объективной части (какие задачи и каким образом решает API), так и в субъективной — что такое очевидность, читабельность и консистентность для вашего API.</p>
<p>Принципы, которые мы будем излагать ниже, во многом ориентированы именно на то, чтобы API правильно развивался во времени и не превращался в нагромождение разнородных неконсистентных интерфейсов. Важно понимать, что такой подход тоже не бесплатен: необходимость держать в голове варианты развития событий и закладывать возможность изменений в API означает избыточность интерфейсов и возможно излишнее абстрагирование. И то, и другое, помимо прочего, усложняет и работу программиста, пользующегося вашим API. <strong>Закладывание перспектив «на будущее» имеет смысл, только если это будущее у API есть, иначе это попросту оверинжиниринг</strong>.</p><div class="page-break"></div><h3><a href="#chapter-4" class="anchor" id="chapter-4">Глава 4. Обратная совместимость</a></h3>
<p>Обратная совместимость — это некоторая <em>временна́я</em> характеристика качества вашего API. Именно необходимость поддержания обратной совместимости отличает разработку API от разработки программного обеспечения вообще.</p>
<p>Разумеется, обратная совместимость не абсолютна. В некоторых предметных областях выпуск новых обратно несовместимых версий API является вполне рутинной процедурой. Тем не менее, каждый раз, когда выпускается новая обратно несовместимая версия API, всем разработчикам приходится инвестировать какое-то ненулевое количество усилий, чтобы адаптировать свой код к новой версии. В этом плане выпуск новых версий API является некоторого рода «налогом» на потребителей — им нужно тратить вполне осязаемые деньги только для того, чтобы их продукт продолжал работать.</p>
<p>Конечно, крупные компании с прочным положением на рынке могут позволить себе такой налог взимать. Более того, они могут вводить какие-то санкции за отказ от перехода на новые версии API, вплоть до отключения приложений.</p>
<p>С нашей точки зрения, подобное поведение ничем не может быть оправдано. Избегайте скрытых налогов на своих пользователей. Если вы можете не ломать обратную совместимость — не ломайте её.</p>
<p>Да, безусловно, поддержка старых версий API — это тоже своего рода налог. Технологии меняются, и, как бы хорошо ни было спроектировано ваше API, всего предусмотреть невозможно. В какой-то момент ценой поддержки старых версий становится невозможность предоставлять новую функциональность и поддерживать новые платформы, и выпустить новую версию всё равно придётся. Однако вы по крайней мере сможете убедить своих потребителей в необходимости перехода.</p>
<p>Да, безусловно, поддержка старых версий API — это тоже своего рода налог. Технологии меняются, и, как бы хорошо ни был спроектирован ваш API, всего предусмотреть невозможно. В какой-то момент ценой поддержки старых версий становится невозможность предоставлять новую функциональность и поддерживать новые платформы, и выпустить новую версию всё равно придётся. Однако вы по крайней мере сможете убедить своих потребителей в необходимости перехода.</p>
<p>Более подробно о жизненном цикле API и политиках выпуска новых версий будет рассказано в разделе II.</p><div class="page-break"></div><h3><a href="#chapter-5" class="anchor" id="chapter-5">Глава 5. О версионировании</a></h3>
<p>Здесь и далее мы будем придерживаться принципов версионирования <a href="https://semver.org/">semver</a>:</p>
<ol>
@ -446,7 +447,7 @@ Cache-Control: no-cache
<li>в ответе также могут находиться дополнительные заголовки, на которые мы обращаем внимание;</li>
<li>телом ответа является JSON, состоящий из единственного поля <code>error_message</code>; отсутствие значения поля означает, что его значением является именно то, что в этом поле и ожидается — в данном случае какое-то сообщение об ошибке.</li>
</ul>
<p>Здесь термин «клиент» означает «приложение, установленное на устройстве пользователя, использующее рассматриваемое API». Приложение может быть как нативным, так и веб-приложением. Термины «агент» и «юзер-агент» являются синонимами термина «клиент».</p>
<p>Здесь термин «клиент» означает «приложение, установленное на устройстве пользователя, использующее рассматриваемый API». Приложение может быть как нативным, так и веб-приложением. Термины «агент» и «юзер-агент» являются синонимами термина «клиент».</p>
<p>Ответ (частично или целиком) и тело запроса могут быть опущены, если в контексте обсуждаемого вопроса их содержание не имеет значения.</p>
<p>Возможна сокращённая запись вида: <code>POST /some-resource</code> <code>{…,"some_parameter",…}</code><code>{ "operation_id" }</code>; тело запроса и/или ответа может опускаться аналогично полной записи.</p>
<p>Чтобы сослаться на это описание будут использоваться выражения типа «метод <code>POST /v1/bucket/{id}/some-resource</code>» или, для простоты, «метод <code>some-resource</code>» или «метод <code>bucket/some-resource</code>» (если никаких других <code>some-resource</code> в контексте главы не упоминается и перепутать не с чем).</p>
@ -458,13 +459,13 @@ Cache-Control: no-cache
<li>разграничение областей ответственности;</li>
<li>описание конечных интерфейсов.</li>
</ul>
<p>Этот алгоритм строит API сверху вниз, от общих требований и сценариев использования до конкретной номенклатуры сущностей; фактически, двигаясь этим путем, вы получите на выходе готовое API — чем этот подход и ценен.</p>
<p>Этот алгоритм строит API сверху вниз, от общих требований и сценариев использования до конкретной номенклатуры сущностей; фактически, двигаясь этим путем, вы получите на выходе готовый API — чем этот подход и ценен.</p>
<p>Может показаться, что наиболее полезные советы приведены в последнем разделе, однако это не так; цена ошибки, допущенной на разных уровнях весьма различна. Если исправить плохое именование довольно просто, то исправить неверное понимание того, зачем вообще нужен API, практически невозможно.</p>
<p><strong>NB</strong>. Здесь и далее мы будем рассматривать концепции разработки API на примере некоторого гипотетического API заказа кофе в городских кофейнях. На всякий случай сразу уточним, что пример является синтетическим; в реальной ситуации, если бы такой API пришлось проектировать, он, вероятно, был бы совсем не похож на наш выдуманный пример.</p><div class="page-break"></div><h3><a href="#chapter-8" class="anchor" id="chapter-8">Глава 8. Определение области применения</a></h3>
<p>Ключевой вопрос, который вы должны задать себе четыре раза, выглядит так: какую проблему мы решаем? Задать его следует четыре раза с ударением на каждом из четырёх слов.</p>
<ol>
<li>
<p><em>Какую</em> проблему мы решаем? Можем ли мы чётко описать, в какой ситуации гипотетическим потребителям-разработчикам нужно наше API?</p>
<p><em>Какую</em> проблему мы решаем? Можем ли мы чётко описать, в какой ситуации гипотетическим потребителям-разработчикам нужен наш API?</p>
</li>
<li>
<p>Какую <em>проблему</em> мы решаем? А мы правда уверены, что описанная выше ситуация — проблема? Действительно ли кто-то готов платить (в прямом и переносном смысле) за то, что ситуация будет как-то автоматизирована?</p>
@ -502,7 +503,7 @@ Cache-Control: no-cache
<h4>Почему API?</h4>
<p>Поскольку наша книга посвящена не просто разработке программного обеспечения, а разработке API, то на все эти вопросы мы должны взглянуть под другим ракурсом: а почему для решения этих задач требуется именно API, а не просто программное обеспечение? В нашем вымышленном примере мы должны спросить себя: зачем нам нужно предоставлять сервис для других разработчиков, чтобы они могли готовить кофе своим клиентам, а не сделать своё приложение для конечного потребителя?</p>
<p>Иными словами, должна иметься веская причина, по которой два домена разработки ПО должны быть разделены: есть оператор(ы), предоставляющий API; есть оператор(ы), предоставляющий сервисы пользователям. Их интересы в чем-то различны настолько, что объединение этих двух ролей в одном лице нежелательно. Более подробно мы изложим причины и мотивации делать именно API в разделе III.</p>
<p>Заметим также следующее: вы должны браться делать API тогда и только тогда, когда в ответе на второй вопрос написали «потому что в этом состоит наша экспертиза». Разрабатывая API вы занимаетесь некоторой мета-разработкой: вы пишете ПО для того, чтобы другие могли разрабатывать ПО для решения задачи пользователя. Не обладая экспертизой в обоих этих доменах (API и конечные продукты) написать хорошее API сложно.</p>
<p>Заметим также следующее: вы должны браться делать API тогда и только тогда, когда в ответе на второй вопрос написали «потому что в этом состоит наша экспертиза». Разрабатывая API, вы занимаетесь некоторой мета-разработкой: вы пишете ПО для того, чтобы другие могли разрабатывать ПО для решения задачи пользователя. Не обладая экспертизой в обоих этих доменах (API и конечные продукты) написать хороший API сложно.</p>
<p>Для нашего умозрительного примера предположим, что в недалеком будущем произошло разделение рынка кофе на две группы игроков: одни предоставляют само железо, кофейные аппараты, а другие имеют доступ к потребителю — примерно как это произошло, например, с рынком авиабилетов, где есть собственно авиакомпании, осуществляющие перевозку, и сервисы планирования путешествий, где люди выбирают варианты перелётов. Мы хотим агрегировать доступ к железу, чтобы владельцы приложений могли встраивать заказ кофе.</p>
<h4>Что и как</h4>
<p>Закончив со всеми теоретическими упражнениями, мы должны перейти непосредственно к дизайну и разработке API, имея понимание по двум пунктам.</p>
@ -532,7 +533,7 @@ Cache-Control: no-cache
<p>Возможность поддерживать обратную совместимость; правильно подобранные уровни абстракции позволят нам в дальнейшем добавлять новую функциональность, не меняя интерфейс.</p>
</li>
<li>
<p>Поддержание интероперабельности. Правильно выделенные низкоуровневые абстракции позволят нам адаптировать наше API к другим платформам, не меняя высокоуровневый интерфейс.</p>
<p>Поддержание интероперабельности. Правильно выделенные низкоуровневые абстракции позволят нам адаптировать наш API к другим платформам, не меняя высокоуровневый интерфейс.</p>
</li>
</ol>
<p>Допустим, мы имеем следующий интерфейс:</p>
@ -582,7 +583,7 @@ GET /v1/orders/{id}
<p>Наконец, от структур данных, в которых удобно оперировать пользователю к структурам данных, максимально приближенных к «сырым» - в нашем случае от «лунго» и «сети кофеен "Ромашка"» - к сырым байтовый данным, описывающим состояние кофе-машины марки «Доброе утро» в процессе приготовления напитка.</p>
</li>
</ol>
<p>Чем дальше находятся друг от друга программные контексты, которые соединяет наше API - тем более глубокая иерархия сущностей должна получиться у нас в итоге.</p>
<p>Чем дальше находятся друг от друга программные контексты, которые соединяет наш API - тем более глубокая иерархия сущностей должна получиться у нас в итоге.</p>
<p>В нашем примере с определением готовности кофе мы явно пришли к тому, что нам требуется промежуточный уровень абстракции:</p>
<ul>
<li>с одной стороны, «заказ» не должен содержать информацию о датчиках и сенсорах кофе-машины;</li>
@ -605,7 +606,7 @@ GET /v1/orders/{id}
}
</code></pre>
<p>Мы называем этот подход «наивным» не потому, что он неправильный; напротив, это вполне логичное решение «по умолчанию», если вы на данном этапе ещё не знаете или не понимаете, как будет выглядеть ваше API. Проблема его в том, что он умозрительный: он не добавляет понимания того, как устроена предметная область.</p>
<p>Мы называем этот подход «наивным» не потому, что он неправильный; напротив, это вполне логичное решение «по умолчанию», если вы на данном этапе ещё не знаете или не понимаете, как будет выглядеть ваш API. Проблема его в том, что он умозрительный: он не добавляет понимания того, как устроена предметная область.</p>
<p>Хороший разработчик в нашем примере должен спросить: хорошо, а какие вообще говоря существуют варианты? Как можно определять готовность напитка? Если вдруг окажется, что сравнение объёмов — единственный способ определения готовности во всех без исключения кофе-машинах, то почти все рассуждения выше — неверны: можно совершенно спокойно включать в интерфейсы определение готовности кофе по объёму, т.к. никакого другого и не существует. Прежде, чем что-то абстрагировать — надо представлять, <em>что</em> мы, собственно, абстрагируем.</p>
<p>Для нашего примера допустим, что мы сели изучать спецификации API кофе-машин и выяснили, что существует принципиально два класса устройств:</p>
<ul>
@ -650,7 +651,7 @@ POST /cancel
// Формат аналогичен формату ответа `POST /execute`
GET /execution/status
</code></pre>
<p><strong>NB</strong>. На всякий случай отметим, что данное API нарушает множество описанных нами принципов проектирования, начиная с отсутствия версионирования; оно приведено в таком виде по двум причинам: (1) чтобы мы могли показать, как спроектировать API более удачно; (2) скорее всего, в реальной жизни вы получите именно такое API от производителей кофе-машин, и это ещё довольно вменяемый вариант.</p>
<p><strong>NB</strong>. На всякий случай отметим, что данный API нарушает множество описанных нами принципов проектирования, начиная с отсутствия версионирования; он приведен в таком виде по двум причинам: (1) чтобы мы могли показать, как спроектировать API более удачно; (2) скорее всего, в реальной жизни вы получите именно такой API от производителей кофе-машин, и это ещё довольно вменяемый вариант.</p>
</li>
<li>
<p>Машины с предустановленными функциями:</p>
@ -878,7 +879,7 @@ GET /sensors
<p>С точки зрения верхнеуровневого API отмена заказа является терминальным действием, т.е. никаких последующих операций уже быть не может; а с точки зрения низкоуровневого API обработка заказа продолжается, т.к. нужно дождаться, когда стакан будет утилизирован, и после этого освободить кофе-машину (т.е. разрешить создание новых рантаймов на ней). Это вторая задача для уровня исполнения: связывать оба статуса, внешний (заказ отменён) и внутренний (исполнение продолжается).</p>
</li>
</ol>
<p>Может показаться, что соблюдение правила изоляции уровней абстракции является избыточным и заставляет усложнять интерфейс. И это в действительности так: важно понимать, что никакая гибкость, логичность, читабельность и расширяемость не бывает бесплатной. Можно построить API так, чтобы оно выполняло свою функцию с минимальными накладными расходами, по сути — дать интерфейс к микроконтроллерам кофе-машины. Однако пользоваться им будет крайне неудобно, и расширяемость такого API будет нулевой.</p>
<p>Может показаться, что соблюдение правила изоляции уровней абстракции является избыточным и заставляет усложнять интерфейс. И это в действительности так: важно понимать, что никакая гибкость, логичность, читабельность и расширяемость не бывает бесплатной. Можно построить API так, чтобы он выполнял свою функцию с минимальными накладными расходами, по сути — дать интерфейс к микроконтроллерам кофе-машины. Однако пользоваться им будет крайне неудобно, и расширяемость такого API будет нулевой.</p>
<p>Выделение уровней абстракции, прежде всего, <em>логическая</em> процедура: как мы объясняем себе и разработчику, из чего состоит наш API. <strong>Абстрагируемая дистанция между сущностями существует объективно</strong>, каким бы образом мы ни написали конкретные интерфейсы. Наша задача состоит только лишь в том, чтобы эта дистанция была разделена на уровни <em>явно</em>. Чем более неявно разведены (или, хуже того, перемешаны) уровни абстракции, тем сложнее будет разобраться в вашем API, и тем хуже будет написан использующий его код.</p>
<h4>Потоки данных</h4>
<p>Полезное упражнение, позволяющее рассмотреть иерархию уровней абстракции API — исключить из рассмотрения все частности и построить — в голове или на бумаге — дерево потоков данных: какие данные протекают через объекты вашего API и как видоизменяются на каждом этапе.</p>
@ -964,9 +965,9 @@ GET /sensors
</ul>
</li>
</ol>
<p>Если внимательно посмотреть на каждый объект, то мы увидим, что, в итоге, каждый объект оказался в смысле своей ответственности составным. Например, <code>program</code> будет оперировать данными высшего уровня (рецепт и кофе-машина), дополняя их терминами своего уровня (идентификатор запуска). Это совершенно нормально: API должно связывать контексты.</p>
<p>Если внимательно посмотреть на каждый объект, то мы увидим, что, в итоге, каждый объект оказался в смысле своей ответственности составным. Например, <code>program</code> будет оперировать данными высшего уровня (рецепт и кофе-машина), дополняя их терминами своего уровня (идентификатор запуска). Это совершенно нормально: API должен связывать контексты.</p>
<h4>Сценарии использования</h4>
<p>На этом уровне, когда наше API уже в целом понятно устроено и спроектировано, мы должны поставить себя на место разработчика и попробовать написать код. Наша задача: взглянуть на номенклатуру сущностей и понять, как ими будут пользоваться.</p>
<p>На этом уровне, когда наш API уже в целом понятно устроен и спроектирован, мы должны поставить себя на место разработчика и попробовать написать код. Наша задача: взглянуть на номенклатуру сущностей и понять, как ими будут пользоваться.</p>
<p>Представим, что нам поставили задачу, пользуясь нашим кофейным API, разработать приложение для заказа кофе. Какой код мы напишем?</p>
<p>Очевидно, первый шаг — нужно предоставить пользователю возможность выбора, чего он, собственно хочет. И первый же шаг обнажает неудобство использования нашего API: никаких методов, позволяющих пользователю что-то выбрать в нашем API нет. Разработчику придётся сделать что-то типа такого:</p>
<ul>
@ -1181,7 +1182,7 @@ app.display(offers);
}, …]
}
</code></pre>
<p>Такое API читать и воспринимать гораздо удобнее, нежели сплошную простыню различных атрибутов. Более того, возможно, стоит на будущее сразу дополнительно сгруппировать, например, <code>place</code> и <code>route</code> в одну структуру <code>location</code>, или <code>offer</code> и <code>pricing</code> в одну более общую структуру.</p>
<p>Такой API читать и воспринимать гораздо удобнее, нежели сплошную простыню различных атрибутов. Более того, возможно, стоит на будущее сразу дополнительно сгруппировать, например, <code>place</code> и <code>route</code> в одну структуру <code>location</code>, или <code>offer</code> и <code>pricing</code> в одну более общую структуру.</p>
<p>Важно, что читабельность достигается не просто снижением количества сущностей на одном уровне. Декомпозиция должна производиться таким образом, чтобы разработчик при чтении интерфейса сразу понимал: так, вот здесь находится описание заведения, оно мне пока неинтересно и углубляться в эту ветку я пока не буду. Если перемешать данные, которые нужны в моменте одновременно для выполнения действия по разным композитам — это только ухудшит читабельность, а не улучшит.</p>
<p>Дополнительно правильная декомпозиция поможет нам в решении задачи расширения и развития API, о чем мы поговорим в разделе II.</p><div class="page-break"></div><h3><a href="#chapter-11" class="anchor" id="chapter-11">Глава 11. Описание конечных интерфейсов</a></h3>
<p>Определив все сущности, их ответственность и отношения друг с другом, мы переходим непосредственно к разработке API: нам осталось прописать номенклатуру всех объектов, полей, методов и функций в деталях. В этой главе мы дадим сугубо практические советы, как сделать API удобным и понятным.</p>
@ -1189,7 +1190,7 @@ app.display(offers);
<h5><a href="#chapter-11-paragraph-0" id="chapter-11-paragraph-0" class="anchor">0. Правила — это всего лишь обобщения</a></h5>
<p>Правила не действуют безусловно и не означают, что можно не думать головой. У каждого правила есть какая-то рациональная причина его существования. Если в вашей ситуации нет причин следовать правилу — значит, следовать ему не нужно.</p>
<p>Например, требование консистентности номенклатуры существует затем, чтобы разработчик тратил меньше времени на чтение документации; если вам <em>необходимо</em>, чтобы разработчик обязательно прочитал документацию по какому-то методу, вполне разумно сделать его сигнатуру нарочито неконсистентно.</p>
<p>Это соображение применимо ко всем принципам ниже. Если из-за следования правилам у вас получается неудобное, громоздкое, неочевидное API — это повод пересмотреть правила (или API).</p>
<p>Это соображение применимо ко всем принципам ниже. Если из-за следования правилам у вас получается неудобный, громоздкий, неочевидный API — это повод пересмотреть правила (или API).</p>
<p>Важно понимать, что вы вольны вводить свои собственные конвенции. Например, в некоторых фреймворках сознательно отказываются от парных методов <code>set_entity</code> / <code>get_entity</code> в пользу одного метода <code>entity</code> с опциональным параметром. Важно только проявить последовательность в её применении — если такая конвенция вводится, то абсолютно все методы API должны иметь подобную полиморфную сигнатуру, или по крайней мере должен существовать принцип именования, отличающий такие комбинированные методы от обычных вызовов.</p>
<h5><a href="#chapter-11-paragraph-1" id="chapter-11-paragraph-1" class="anchor">1. Явное лучше неявного</a></h5>
<p>Из названия любой сущности должно быть очевидно, что она делает и к каким сайд-эффектам может привести её использование.</p>
@ -1255,7 +1256,7 @@ strpbrk (str1, str2)
<p><strong>Плохо</strong>: <code>GET /news</code> — неясно, будет ли получена какая-то конкретная новость или массив новостей.</p>
<p><strong>Хорошо</strong>: <code>GET /news-list</code>.</p>
<p>Аналогично, если ожидается булево значение, то это должно быть очевидно из названия, т.е. именование должно описывать некоторое качественное состояние, например, <code>is_ready</code>, <code>open_now</code>.</p>
<p><strong>Плохо</strong>: <code>"task.status": true</code> — неочевидно, что статус бинарен, к тому же такое API будет нерасширяемым.</p>
<p><strong>Плохо</strong>: <code>"task.status": true</code> — неочевидно, что статус бинарен, к тому же такой API будет нерасширяемым.</p>
<p><strong>Хорошо</strong>: <code>"task.is_finished": true</code>.</p>
<p>Отдельно следует оговорить, что на разных платформах эти правила следует дополнить по-своему с учетом специфики first-class citizen-типов. Например, объекты типа <code>Date</code>, если таковые имеются, разумно индицировать с помощью, например, постфикса <code>_at</code> (<code>created_at</code>, <code>occurred_at</code> и т.д.) или <code>_date</code>.</p>
<p>Если наименование сущности само по себе является каким-либо термином, способным смутить разработчика, лучше добавить лишний префикс или постфикс во избежание непонимания.</p>
@ -1812,7 +1813,7 @@ GET /v1/records?cursor=&#x3C;значение курсора>
// начиная с записи с номером offset
GET /records?sort_by=date_modified&#x26;sort_order=desc&#x26;limit=10&#x26;offset=100
</code></pre>
<p>Сортировка по дате модификации обычно означает, что данные могут меняться. Иными словами, между запросом первой порции данных и запросом второй порции данных какая-то запись может измениться; она просто пропадёт из перечисления, т.к. автоматически попадает на первую страницу. Клиент никогда не получит те записи, которые менялись во время перебора, и у него даже нет способа узнать о самом факте такого пропуска. Помимо этого отметим, что такое API нерасширяемо — невозможно добавить сортировку по двум или более полям.</p>
<p>Сортировка по дате модификации обычно означает, что данные могут меняться. Иными словами, между запросом первой порции данных и запросом второй порции данных какая-то запись может измениться; она просто пропадёт из перечисления, т.к. автоматически попадает на первую страницу. Клиент никогда не получит те записи, которые менялись во время перебора, и у него даже нет способа узнать о самом факте такого пропуска. Помимо этого отметим, что такой API нерасширяем — невозможно добавить сортировку по двум и более полям.</p>
<p><strong>Хорошо</strong>: в представленной постановке задача, собственно говоря, не решается. Список записей по дате изменения всегда будет непредсказуемо изменяться, поэтому необходимо изменить сам подход к формированию данных, одним из двух способов.</p>
<p><strong>Вариант 1</strong>: фиксировать порядок в момент обработки запроса; т.е. сервер формирует полный список и сохраняет его в неизменяемом виде:</p>
<pre><code>// Создаёт представление по указанным параметрам
@ -2004,10 +2005,10 @@ POST /v1/orders
<p>Все эндпойнты должны принимать на вход языковые параметры (например, в виде заголовка <code>Accept-Language</code>), даже если на текущем этапе нужды в локализации нет.</p>
<p>Важно понимать, что язык пользователя и юрисдикция, в которой пользователь находится — разные вещи. Цикл работы вашего API всегда должен хранить локацию пользователя. Либо она задаётся явно (в запросе указываются географические координаты), либо неявно (первый запрос с географическими координатами инициировал создание сессии, в которой сохранена локация) — но без локации корректная локализация невозможна. В большинстве случаев локацию допустимо редуцировать до кода страны.</p>
<p>Дело в том, что множество параметров, потенциально влияющих на работу API, зависят не от языка, а именно от расположения пользователя. В частности, правила форматирования чисел (разделители целой и дробной частей, разделители разрядов) и дат, первый день недели, раскладка клавиатуры, система единиц измерения (которая к тому же может оказаться не десятичной!) и так далее. В некоторых ситуациях необходимо хранить две локации: та, в которой пользователь находится, и та, которую пользователь сейчас просматривает. Например, если пользователь из США планирует туристическую поездку в Европу, то цены ему желательно показывать в местной валюте, но отформатированными согласно правилам американского письма.</p>
<p>Следует иметь в виду, что явной передачи локации может оказаться недостаточно, поскольку в мире существуют территориальные конфликты и спорные территории. Каким образом API должно себя вести при попадании координат пользователя на такие территории — вопрос, к сожалению, в первую очередь юридический. Автору этой книги приходилось как-то разрабатывать API, в котором пришлось вводить концепцию «территория государства A по мнению официальных органов государства Б».</p>
<p>Следует иметь в виду, что явной передачи локации может оказаться недостаточно, поскольку в мире существуют территориальные конфликты и спорные территории. Каким образом API должен себя вести при попадании координат пользователя на такие территории — вопрос, к сожалению, в первую очередь юридический. Автору этой книги приходилось как-то разрабатывать API, в котором пришлось вводить концепцию «территория государства A по мнению официальных органов государства Б».</p>
<p><strong>Важно</strong>: различайте локализацию для конечного пользователя и локализацию для разработчика. В примере из п. 19 сообщение <code>localized_message</code> адресовано пользователю — его должно показать приложение, если в коде обработка такой ошибки не предусмотрена. Это сообщение должно быть написано на указанном в запросе языке и отформатировано согласно правилам локации пользователя. А вот сообщение <code>details.checks_failed[].message</code> написано не для пользователя, а для разработчика, который будет разбираться с проблемой. Соответственно, написано и отформатировано оно должно быть понятным для разработчика образом — что, скорее всего, означает «на английском языке», т.к. английский де факто является стандартом в мире разработки программного обеспечения.</p>
<p>Следует отметить, что индикация, какие сообщения следует показать пользователю, а какие написаны для разработчика, должна, разумеется, быть явной конвенцией вашего API. В примере для этого используется префикс <code>localized_</code>.</p>
<p>И ещё одна вещь: все строки должны быть в кодировке UTF-8 и никакой другой.</p><div class="page-break"></div><h3><a href="#chapter-12" class="anchor" id="chapter-12">Глава 12. Приложение к разделу I. Модельное API</a></h3>
<p>И ещё одна вещь: все строки должны быть в кодировке UTF-8 и никакой другой.</p><div class="page-break"></div><h3><a href="#chapter-12" class="anchor" id="chapter-12">Глава 12. Приложение к разделу I. Модельный API</a></h3>
<p>Суммируем текущее состояние нашего учебного API.</p>
<h5><a href="#chapter-12-paragraph-1" id="chapter-12-paragraph-1" class="anchor">1. Поиск предложений</a></h5>
<pre><code>POST /v1/offers/search
@ -2164,7 +2165,7 @@ POST /v1/runtimes/{id}/terminate
<p>С нашей точки зрения длительность поддержания обратной совместимости следует увязывать с длительностью жизненных циклов приложений в соответствующей предметной области. Хороший ориентир в большинстве случаев — это LTS-периоды платформ. Так как приложение все равно будет переписано в связи с окончанием поддержки платформы, нормально предложить также и переход на новую версию API. В основных предметных областях (десктопные и мобильные операционные системы) этот срок исчисляется несколькими годами.</p>
</li>
</ol>
<p>Почему обратную совместимость необходимо поддерживать (в том числе предпринимать необходимые меры ещё на этапе проектирования API) — понятно из определения. Прекращение работы приложения (полное или частичное) по вине поставщика API — крайне неприятное событие, а то и катастрофа, для любого разработчика, особенно если он платит за это API деньги.</p>
<p>Почему обратную совместимость необходимо поддерживать (в том числе предпринимать необходимые меры ещё на этапе проектирования API) — понятно из определения. Прекращение работы приложения (полное или частичное) по вине поставщика API — крайне неприятное событие, а то и катастрофа, для любого разработчика, особенно если он платит за этот API деньги.</p>
<p>Но развернём теперь проблему в другую сторону: а почему вообще возникает проблема с поддержанием обратной совместимости? Почему мы можем <em>хотеть</em> её нарушить? Ответ на этот вопрос, при кажущейся простоте, намного сложнее, чем на предыдущий.</p>
<p>Мы могли бы сказать, что <em>обратную совместимость приходится нарушать для расширения функциональности API</em>. Но это лукавство: новая функциональность на то и <em>новая</em>, что она не может затронуть код приложений, который её не использует. Да, конечно, есть ряд сопутствующих проблем, приводящих к стремлению переписать <em>наш</em> код, код самого API, с выпуском новой мажорной версии:</p>
<ul>
@ -2196,11 +2197,11 @@ POST /v1/runtimes/{id}/terminate
<li>пользователь не хочет обновляться (в том числе потому, что, по мнению пользователя, разработчики приложения его «испортили» в новых версиях);</li>
<li>пользователь не может обновиться вообще, потому что его устройство больше не поддерживается.</li>
</ul>
<p>В современных реалиях все три категории в сумме легко могут составлять десятки процентов аудитории. Это означает, что прекращение поддержки любой версии API является весьма заметным событием — особенно если приложения разработчика поддерживают более широкий спектр версий платформы, нежели ваше API.</p>
<p>Вы можете не выпускать вообще никаких SDK, предоставляя только серверное API в виде, например, HTTP эндпойнтов. Вам может показаться, что таким образом, пусть ваше API и стало менее конкурентоспособным на рынке из-за отсутствия SDK, вы облегчили себе задачу поддержания обратной совместимости. На самом деле это совершенно не так: раз вы не предоставляете свой SDK — или разработчики возьмут неофициальный SDK (если кто-то его сделает), или просто каждый из них напишет по фреймворку. Стратегия «ваш фреймворк — ваша ответственность», к счастью или к сожалению, работает плохо: если на вашем API пишут некачественные приложения — значит, ваше API само некачественное. Уж точно по мнению разработчиков, а может и по мнению пользователей, если работа API внутри приложения пользователю видна.</p>
<p>В современных реалиях все три категории в сумме легко могут составлять десятки процентов аудитории. Это означает, что прекращение поддержки любой версии API является весьма заметным событием — особенно если приложения разработчика поддерживают более широкий спектр версий платформы, нежели ваш API.</p>
<p>Вы можете не выпускать вообще никаких SDK, предоставляя только серверный API в виде, например, HTTP эндпойнтов. Вам может показаться, что таким образом, пусть ваш API и стал менее конкурентоспособным на рынке из-за отсутствия SDK, вы облегчили себе задачу поддержания обратной совместимости. На самом деле это совершенно не так: раз вы не предоставляете свой SDK — или разработчики возьмут неофициальный SDK (если кто-то его сделает), или просто каждый из них напишет по фреймворку. Стратегия «ваш фреймворк — ваша ответственность», к счастью или к сожалению, работает плохо: если на вашем API пишут некачественные приложения — значит, ваш API сам некачественный. Уж точно по мнению разработчиков, а может и по мнению пользователей, если работа API внутри приложения пользователю видна.</p>
</li>
</ol>
<p>Конечно, если ваше API достаточно stateless и не требует клиентских SDK (или же можно обойтись просто автогенерацией SDK из спецификации), эти проблемы будут гораздо менее заметны, но избежать их полностью можно только одним способом — никогда не выпуская новых версий API. Во всех остальных случаях вы будете иметь дело с какой-то гребёнкой распределения количества пользователей по версиям API и версиям SDK.</p>
<p>Конечно, если ваш API достаточно stateless и не требует клиентских SDK (или же можно обойтись просто автогенерацией SDK из спецификации), эти проблемы будут гораздо менее заметны, но избежать их полностью можно только одним способом — никогда не выпуская новых версий API. Во всех остальных случаях вы будете иметь дело с какой-то гребёнкой распределения количества пользователей по версиям API и версиям SDK.</p>
<h4>Эволюция предметной области</h4>
<p>Другая сторона ущелья — та самая нижележащая функциональность, к которой вы предоставляете API. Она, разумеется, тоже не статична и развивается в какую-то сторону:</p>
<ul>
@ -2235,7 +2236,7 @@ POST /v1/runtimes/{id}/terminate
<p>Какое количество <em>минорных</em> версий (в рамках одной мажорной) поддерживать одновременно.</p>
<p>Для минорных версий возможны два варианта:</p>
<ul>
<li>если вы предоставляете только серверное API и компилируемые SDK, вы можете в принципе не поддерживать никакие минорные версии API, помимо актуальной: серверное API находится полностью под вашим контролем, и вы можете оперативно исправить любые проблемы с логикой;</li>
<li>если вы предоставляете только серверный API и компилируемые SDK, вы можете в принципе не поддерживать никакие минорные версии API, помимо актуальной: серверный API находится полностью под вашим контролем, и вы можете оперативно исправить любые проблемы с логикой;</li>
<li>если вы предоставляете code-on-demand SDK, то вот здесь хорошим тоном является поддержка предыдущих минорных версий SDK в работающем состоянии на срок, достаточный для того, чтобы разработчики могли протестировать своё приложение с новой версией и внести какие-то правки по необходимости. Так как полностью переписывать приложения при этом не надо, разумно ориентироваться на длину релизных циклов в вашей индустрии, обычно это несколько месяцев в худшем случае.</li>
</ul>
</li>
@ -2243,7 +2244,7 @@ POST /v1/runtimes/{id}/terminate
<p>Дополнительно в разделе III мы также обсудим, каким образом предупреждать потребителей о выходе новых версий и прекращении поддержки старых, и как стимулировать их переходить на новые версии API.</p><div class="page-break"></div><h3><a href="#chapter-14" class="anchor" id="chapter-14">Глава 14. О ватерлинии айсберга</a></h3>
<p>Прежде, чем начинать разговор о принципах проектирования расширяемого API, следует обсудить гигиенический минимум. Огромное количество проблем не случилось бы, если бы разработчики API чуть ответственнее подходили к обозначению зоны своей ответственности.</p>
<h5><a href="#chapter-14-paragraph-1" id="chapter-14-paragraph-1" class="anchor">1. Предоставляйте минимальный объём функциональности</a></h5>
<p>В любой момент времени ваше API подобно айсбергу: у него есть видимая (документированная) часть и невидимая — недокументированная. В хорошем API эти две части соотносятся друг с другом примерно как надводная и подводная часть настоящего айсберга, 1 к 10. Почему так? Из двух очевидных соображений.</p>
<p>В любой момент времени ваш API подобен айсбергу: у него есть видимая (документированная) часть и невидимая — недокументированная. В хорошем API эти две части соотносятся друг с другом примерно как надводная и подводная часть настоящего айсберга, 1 к 10. Почему так? Из двух очевидных соображений.</p>
<ul>
<li>
<p>Компьютеры существуют, чтобы сложные вещи делать просто, не наоборот. Код, который напишут разработчики поверх вашего API, должен в простых и лаконичных выражениях описывать решение сложной проблемы. Поэтому «внутри» ваш код, скорее всего, будет опираться на мощную номенклатуру непубличной функциональности.</p>
@ -2269,7 +2270,7 @@ let order = api.createOrder();
// Получает статус заказа
let status = api.getStatus(order.id);
</code></pre>
<p>Предположим, что в какой-то момент при масштабировании вашего сервиса вы пришли к асинхронной репликации базы данных и разрешили чтение из реплики. Это приведёт к тому, что после создания заказа следующее обращение к его статусу по id может вернуть <code>404</code>, если пришлось на асинхронную реплику, до которой ещё не дошли последние изменения из мастера. Фактически, вы сменили <a href="https://en.wikipedia.org/wiki/Consistency_model">политику консистентности</a> со strong на eventual.</p>
<p>Предположим, что в какой-то момент при масштабировании вашего сервиса вы пришли к асинхронной репликации базы данных и разрешили чтение из реплики. Это приведёт к тому, что после создания заказа следующее обращение к его статусу по id может вернуть <code>404</code>, если оно пришлось на асинхронную реплику, до которой ещё не дошли последние изменения из мастера. Фактически, вы сменили <a href="https://en.wikipedia.org/wiki/Consistency_model">политику консистентности</a> со strong на eventual.</p>
<p>К чему это приведёт? К тому, что код выше перестанет работать. Разработчик создал заказ, пытается получить его статус — и получает ошибку. Очень тяжело предсказать, какую реакцию на эту ошибку предусмотрят разработчики — вероятнее всего, никакую.</p>
<p>Вы можете сказать: «Позвольте, но мы нигде и не обещали строгую консистентность!» — и это будет, конечно, неправдой. Вы можете так сказать если, и только если, вы действительно в документации метода <code>createOrder</code> явно описали нестрогую консистентность, а все ваши примеры использования SDK написаны как-то так:</p>
<pre><code>let order = api.createOrder();
@ -2287,7 +2288,7 @@ if (status) {
}
</code></pre>
<p>Мы полагаем, что можно не уточнять, что писать код, подобный вышеприведённому, ни в коем случае нельзя. Уж если вы действительно предоставляете нестрого консистентное API, то либо операция <code>createOrder</code> в SDK должна быть асинхронной и возвращать результат только по готовности всех реплик, либо политика перезапросов должна быть скрыта внутри операции <code>getStatus</code>.</p>
<p>Мы полагаем, что можно не уточнять, что писать код, подобный вышеприведённому, ни в коем случае нельзя. Уж если вы действительно предоставляете нестрого консистентный API, то либо операция <code>createOrder</code> в SDK должна быть асинхронной и возвращать результат только по готовности всех реплик, либо политика перезапросов должна быть скрыта внутри операции <code>getStatus</code>.</p>
<p>Если же нестрогая консистентность не была описана с самого начала — вы не можете внести такие изменения в API. Это эффективный слом обратной совместимости, который к тому же приведёт к огромным проблемам ваших потребителей, поскольку проблема будет воспроизводиться случайным образом.</p>
<p><strong>Пример 2</strong>. Представьте себе следующий код:</p>
<pre><code>let resolve;
@ -2299,7 +2300,7 @@ let promise = new Promise(
resolve();
</code></pre>
<p>Этот код полагается на то, что callback-функция, переданная в <code>new Promise</code> будет выполнена <em>синхронно</em>, и переменная <code>resolve</code> будет инициализирована к моменту вызова <code>resolve()</code>. Однако это конвенция абсолютно ниоткуда не следует: ничто в сигнатуре конструктора <code>new Promise</code> не указывает на синхронный вызов callback-а.</p>
<p>Разработчики языка, конечно, могут позволить себе такие фокусы. Однако вы как разработчик API — не можете. Вы должны как минимум задокументировать это поведение и подобрать сигнатуры так, чтобы оно было очевидно; но вообще хорошим советом будет избегать таких конвенций, поскольку они банально неочевидны при прочтении кода, использующего ваше API. Ну и конечно же ни при каких обстоятельствах вы не можете изменить это поведение с синхронного на асинхронное.</p>
<p>Разработчики языка, конечно, могут позволить себе такие фокусы. Однако вы как разработчик API — не можете. Вы должны как минимум задокументировать это поведение и подобрать сигнатуры так, чтобы оно было очевидно; но вообще хорошим советом будет избегать таких конвенций, поскольку они банально неочевидны при прочтении кода, использующего ваш API. Ну и конечно же ни при каких обстоятельствах вы не можете изменить это поведение с синхронного на асинхронное.</p>
<p><strong>Пример 3</strong>. Представьте, что вы предоставляете API для анимаций, в котором есть две независимые функции:</p>
<pre><code>// Анимирует ширину некоторого объекта
// от первого значения до второго
@ -2344,7 +2345,7 @@ object.observe('widthchange', observerFunction);
<p>Представьте, что в один прекрасный день вы заводите специальный номер телефона, по которому клиент может позвонить в колл-центр и отменить заказ. Вы даже можете сделать это <em>технически</em> обратно-совместимым образом, добавив новых необязательных полей в сущность «заказ». Но конечный потребитель может просто <em>знать</em> нужный номер телефона, и позвонить по нему, даже если приложение его не показало. При этом код бизнес-аналитика партнера всё так же может сломаться или начать показывать погоду на Марсе, т.к. он был написан когда-то, ничего не зная о возможности отменить заказ, сделанный в приложении партнера, каким-то иным образом, не через самого партнёра же.</p>
<p><em>Технически</em> корректным решением в данной ситуации могло бы быть добавление параметра «разрешено отменять через колл-центр» в функцию создания заказа — и, соответственно, запрет операторам колл-центра отменять заказы, если флаг не был указан при их создании. Но это в свою очередь плохое решение <em>с точки зрения продукта</em>. «Хорошее» решение здесь только одно — изначально предусмотреть возможность внешних отмен в API; если же вы её не предвидели — остаётся воспользоваться «блокнотом душевного спокойствия», речь о котором пойдёт в последней главе настоящего раздела.</p><div class="page-break"></div><h3><a href="#chapter-15" class="anchor" id="chapter-15">Глава 15. Расширение через абстрагирование</a></h3>
<p>В предыдущих разделах мы старались приводить теоретические правила и иллюстрировать их на практических примерах. Однако понимание принципов проектирования API, устойчивого к изменениям, как ничто другое требует прежде всего практики. Знание о том, куда стоит «постелить соломку» — оно во многом «сын ошибок трудных». Нельзя предусмотреть всего — но можно выработать необходимый уровень технической интуиции.</p>
<p>Поэтому в этом разделе мы поступим следующим образом: возьмём наше <a href="#chapter-12">модельное API</a> из предыдущего раздела, и проверим его на устойчивость в каждой возможной точке — проведём некоторый «вариационный анализ» наших интерфейсов. Ещё более конкретно — к каждой сущности мы подойдём с вопросом «что, если?» — что, если нам потребуется предоставить партнерам возможность написать свою независимую реализацию этого фрагмента логики.</p>
<p>Поэтому в этом разделе мы поступим следующим образом: возьмём наше <a href="#chapter-12">модельный API</a> из предыдущего раздела, и проверим его на устойчивость в каждой возможной точке — проведём некоторый «вариационный анализ» наших интерфейсов. Ещё более конкретно — к каждой сущности мы подойдём с вопросом «что, если?» — что, если нам потребуется предоставить партнерам возможность написать свою независимую реализацию этого фрагмента логики.</p>
<p><strong>NB</strong>. В рассматриваемых нами примерах мы будем выстраивать интерфейсы так, чтобы связывание разных сущностей происходило динамически в реальном времени; на практике такие интеграции будут делаться на стороне сервера путём написания ad hoc кода и формирования конкретных договорённостей с конкретным клиентом, однако мы для целей обучения специально будем идти более сложным и абстрактным путём. Динамическое связывание в реальном времени применимо скорее к сложным программным конструктам типа API операционных систем или встраиваемых библиотек; приводить обучающие примеры на основе систем подобной сложности было бы, однако, чересчур затруднительно.</p>
<p>Начнём с базового интерфейса. Предположим, что мы пока что вообще не раскрывали никакой функциональности помимо поиска предложений и заказа, т.е. мы предоставляем API из двух методов — <code>POST /offers/search</code> и <code>POST /orders</code>.</p>
<p>Сделаем следующий логический шаг и предположим, что партнёры захотят динамически подключать к нашей платформе свои собственные кофе машины с каким-то новым API. Для этого нам будет необходимо договориться о формате обратного вызова, каким образом мы будем вызывать API партнёра, и предоставить два новых эндпойта для:</p>
@ -2383,7 +2384,7 @@ PUT /v1/partners/{partnerId}/coffee-machines
<li>перечислим все неявные предположения, которые мы допустили;</li>
<li>перечислим все неявные механизмы связывания, которые необходимы для функционирования платформы.</li>
</ul>
<p>Может показаться, что в нашем API нет ни того, ни другого, ведь оно очень просто и по сути просто сводится к вызову какого-то HTTP-метода — но это неправда.</p>
<p>Может показаться, что в нашем API нет ни того, ни другого, ведь он очень прост и по сути просто сводится к вызову какого-то HTTP-метода — но это неправда.</p>
<ol>
<li>Предполагается, что каждая кофе-машина поддерживает все возможные опции заказа (например, допустимый объём напитка).</li>
<li>Нет необходимости показывать пользователю какую-то дополнительную информацию о том, что заказ готовится на новых типах кофе-машин.</li>
@ -2393,7 +2394,7 @@ PUT /v1/partners/{partnerId}/coffee-machines
<p>Универсальный паттерн внесения подобных изменений таков: мы должны рассмотреть существующий интерфейс как частный случай некоторого более общего, в котором значения некоторых параметров приняты известными по умолчанию, а потому опущены. Таким образом, внесение изменений всегда происходит в три шага.</p>
<ol>
<li>Явная фиксация программного контракта <em>в том объёме, в котором она действует на текущий момент</em>.</li>
<li>Расширение функциональности: добавление нового метода, которые позволяют обойти ограничение, зафиксированное в п. 1.</li>
<li>Расширение функциональности: добавление нового метода, который позволяет обойти ограничение, зафиксированное в п. 1.</li>
<li>Объявление существующих вызовов (из п. 1) "хелперами" к новому формату (из п. 2), в которых значение новых опций считается равным значению по умолчанию.</li>
</ol>
<p>На нашем примере с изменением списка доступных опций заказа мы должны поступить следующим образом.</p>
@ -2428,7 +2429,7 @@ PUT /v1/partners/{partnerId}/coffee-machines
<p>Увы, здесь мы сталкиваемся с плохо разрешимым противоречием: мы хотим, с одной стороны, чтобы разработчик писал лаконичный код, следовательно, должны предоставлять хорошие хелперные методы и значения по умолчанию. С другой, знать наперёд какими будут самые частотные наборы опций через несколько лет развития API — очень сложно.</p>
<p><strong>NB</strong>. Замаскировать эту проблему можно так: в какой-то момент собрать все эти «странности» в одном месте и переопределить все значения по умолчанию скопом под одним параметром. Условно говоря, вызов одного метода, например, <code>POST /use-defaults {"version": "v2"}</code> переопределяет все значения по умолчанию на более разумные. Это упростит порог входа и уменьшит количество вопросов, но документация от этого станет выглядеть только хуже.</p>
<p>В реальной жизни как-то нивелировать проблему помогает лишь слабая связность объектов, речь о которой пойдёт в следующей главе.</p><div class="page-break"></div><h3><a href="#chapter-16" class="anchor" id="chapter-16">Глава 16. Сильная связность и сопутствующие проблемы</a></h3>
<p>Для демонстрации проблем сильной связности перейдём теперь к <em>действительно интересным</em> вещам. Продолжим наш «вариационный анализ»: что, если партнёры хотят не просто готовить кофе по стандартным рецептам, но и предлагать свои авторские напитки? Вопрос этот с подвохом: в том виде, как мы описали партнёрское API в предыдущей главе, факт существования партнерской сети никак не отражен в нашем API с точки зрения продукта, предлагаемого пользователю, а потому представляет собой довольно простой кейс. Если же мы пытаемся предоставить не какую-то дополнительную возможность, а модифицировать саму базовую функциональность API, то мы быстро столкнёмся с проблемами совсем другого порядка.</p>
<p>Для демонстрации проблем сильной связности перейдём теперь к <em>действительно интересным</em> вещам. Продолжим наш «вариационный анализ»: что, если партнёры хотят не просто готовить кофе по стандартным рецептам, но и предлагать свои авторские напитки? Вопрос этот с подвохом: в том виде, как мы описали партнёрскый API в предыдущей главе, факт существования партнерской сети никак не отражен в нашем API с точки зрения продукта, предлагаемого пользователю, а потому представляет собой довольно простой кейс. Если же мы пытаемся предоставить не какую-то дополнительную возможность, а модифицировать саму базовую функциональность API, то мы быстро столкнёмся с проблемами совсем другого порядка.</p>
<p>Итак, добавим ещё один эндпойнт — для регистрации собственного рецепта партнёра.</p>
<pre><code>// Добавляет новый рецепт
POST /v1/recipes
@ -2469,12 +2470,12 @@ POST /v1/recipes
<p>Проблемы, с которыми мы столкнулись — это проблемы <em>сильной связности</em>. Каждый раз, предлагая интерфейс, подобный вышеприведённому, мы фактически описываем имплементацию одной сущности (рецепта) через имплементации других (визуального макета, правил локализации). Этот подход противоречит самому принципу проектирования API «сверху вниз», поскольку <strong>низкоуровневые сущности не должны определять высокоуровневые</strong>.</p>
<h4>Правило контекстов</h4>
<p>Как бы парадоксально это ни звучало, обратное утверждение тоже верно: высокоуровневые сущности тоже не должны определять низкоуровневые. Это попросту не их ответственность. Выход из этого логического лабиринта таков: высокоуровневые сущности должны <em>определять контекст</em>, который другие объекты будут интерпретировать. Чтобы спроектировать добавление нового рецепта нам нужно не формат данных подобрать — нам нужно понять, какие (возможно, неявные, т.е. не представленные в виде API) контексты существуют в нашей предметной области.</p>
<p>Как уже понятно, существует контекст локализации. Есть какой-то набор языков и регионов, которые мы поддерживаем в нашем API, и есть требования — что конкретно необходимо предоставить партнёру, чтобы API заработало на новом языке в новом регионе. Конкретно в случае объёма кофе где-то в недрах нашего API есть функция форматирования строк для отображения объёма напитка:</p>
<p>Как уже понятно, существует контекст локализации. Есть какой-то набор языков и регионов, которые мы поддерживаем в нашем API, и есть требования — что конкретно необходимо предоставить партнёру, чтобы API заработал на новом языке в новом регионе. Конкретно в случае объёма кофе где-то в недрах нашего API есть функция форматирования строк для отображения объёма напитка:</p>
<pre><code>l10n.volume.format(value, language_code, country_code)
// l10n.formatVolume('300ml', 'en', 'UK') → '300 ml'
// l10n.formatVolume('300ml', 'en', 'US') → '10 fl oz'
</code></pre>
<p>Чтобы наше API корректно заработал с новым языком или регионом, партнер должен или задать эту функцию, или указать, какую из существующих локализаций необходимо использовать. Для этого мы абстрагируем-и-расширяем API, в соответствии с описанной в предыдущей главе процедурой, и добавляем новый эндпойнт — настройки форматирования:</p>
<p>Чтобы наш API корректно заработал с новым языком или регионом, партнер должен или задать эту функцию, или указать, какую из существующих локализаций необходимо использовать. Для этого мы абстрагируем-и-расширяем API, в соответствии с описанной в предыдущей главе процедурой, и добавляем новый эндпойнт — настройки форматирования:</p>
<pre><code>// Добавляем общее правило форматирования
// для русского языка
PUT /formatters/volume/ru
@ -2585,8 +2586,8 @@ PUT /formatters/volume/ru/US
"id": "my-coffee-company:lungo-customato"
}
</code></pre>
<p>Заметим, что в таком формате мы сразу закладываем важное допущение: различные партнёры могут иметь как полностью изолированные неймспейсы, так и разделять их. Более того, мы можем ввести специальные неймспейсы типа "common", которые позволят публиковать новые рецепты для всех. (Это, кстати говоря, хорошо ещё и тем, что такое API мы сможем использовать для организации нашей собственной панели управления контентом.)</p><div class="page-break"></div><h3><a href="#chapter-17" class="anchor" id="chapter-17">Глава 17. Слабая связность</a></h3>
<p>В предыдущей главе мы продемонстрировали, как разрыв сильной связанности приводит к декомпозиции сущностей и схлопыванию публичных интерфейсов до минимума. Внимательный читатель может подметить, что этот приём уже был продемонстрирован в нашем учебном API гораздо раньше <a href="#chapter-9">в главе 9</a> на примере сущностей «программа» и «запуск программы». В самом деле, мы могли бы обойтись без программ и без эндпойнта <code>program-matcher</code> и пойти вот таким путём:</p>
<p>Заметим, что в таком формате мы сразу закладываем важное допущение: различные партнёры могут иметь как полностью изолированные неймспейсы, так и разделять их. Более того, мы можем ввести специальные неймспейсы типа "common", которые позволят публиковать новые рецепты для всех. (Это, кстати говоря, хорошо ещё и тем, что такой API мы сможем использовать для организации нашей собственной панели управления контентом.)</p><div class="page-break"></div><h3><a href="#chapter-17" class="anchor" id="chapter-17">Глава 17. Слабая связность</a></h3>
<p>В предыдущей главе мы продемонстрировали, как разрыв сильной связности приводит к декомпозиции сущностей и схлопыванию публичных интерфейсов до минимума. Внимательный читатель может подметить, что этот приём уже был продемонстрирован в нашем учебном API гораздо раньше <a href="#chapter-9">в главе 9</a> на примере сущностей «программа» и «запуск программы». В самом деле, мы могли бы обойтись без программ и без эндпойнта <code>program-matcher</code> и пойти вот таким путём:</p>
<pre><code>GET /v1/recipes/{id}/run-data/{api_type}
{ /* описание способа запуска
@ -2610,7 +2611,7 @@ PUT /formatters/volume/ru/US
}
}
</code></pre>
<p>Исходя из общей логики мы можем предположить, что любое API так или иначе будет выполнять три функции: запускать программы с указанными параметрами, возвращать текущий статус запуска и завершать (отменять) заказ. Самый очевидный подход к реализации такого API — просто потребовать от партнёра имплементировать вызов этих трёх функций удалённо, например следующим образом:</p>
<p>Исходя из общей логики мы можем предположить, что любой API так или иначе будет выполнять три функции: запускать программы с указанными параметрами, возвращать текущий статус запуска и завершать (отменять) заказ. Самый очевидный подход к реализации такого API — просто потребовать от партнёра имплементировать вызов этих трёх функций удалённо, например следующим образом:</p>
<pre><code>// Эндпойнт добавления списка
// кофе-машин партнёра
PUT /v1/api-types/{api_type}
@ -2635,16 +2636,16 @@ PUT /v1/api-types/{api_type}
<li>Этот дизайн изначально основан на следующем принципе: любое приготовление заказа можно описать этими тремя императивными командами.</li>
</ol>
<p>Пункт 2 очень легко опровергнуть, что автоматически вскроет проблемы пункта 1. Предположим для начала, что в ходе развития функциональности мы решили дать пользователю возможность изменять свой заказ уже после того, как он создан — ну, например, попросить посыпать кофе корицей или выдать заказ бесконтактно. Это автоматически влечёт за собой добавление нового эндпойнта, ну скажем, <code>program_modify_endpoint</code>, и новых сложностей в формате обмена данными (нам нужно уметь понимать в реальном времени, можно ли этот конкретный кофе посыпать корицей). Что важно, и то, и другое (и эндпойнт, и новые поля данных) из соображений обратной совместимости будут необязательными.</p>
<p>Теперь попытаемся придумать какой-нибудь пример реального мира, который не описывается нашими тремя императивами. Это довольно легко: допустим, мы подключим через наше API не кофейню, а вендинговый автомат. Это, с одной стороны, означает, что эндпойнт <code>modify</code> и вся его обвязка для этого типа API бесполезны — автомат не умеет посыпать кофе корицей, а требование бесконтактной выдачи попросту ничего не значит. С другой, автомат, в отличие от оперируемой людьми кофейни, требует программного способа <em>подтверждения выдачи</em> напитка: пользователь делает заказ, находясь где-то в другом месте, потом доходит до автомата и нажимает в приложении кнопку «выдать заказ». Мы могли бы, конечно, потребовать, чтобы пользователь создавал заказ автомату, стоя прямо перед ним, но это, в свою очередь, противоречит нашей изначальной концепции, в которой пользователь выбирает и заказывает напиток, исходя из доступных опций, а потом идёт в указанную точку, чтобы его забрать.</p>
<p>Теперь попытаемся придумать какой-нибудь пример реального мира, который не описывается нашими тремя императивами. Это довольно легко: допустим, мы подключим через наш API не кофейню, а вендинговый автомат. Это, с одной стороны, означает, что эндпойнт <code>modify</code> и вся его обвязка для этого типа API бесполезны — автомат не умеет посыпать кофе корицей, а требование бесконтактной выдачи попросту ничего не значит. С другой, автомат, в отличие от оперируемой людьми кофейни, требует программного способа <em>подтверждения выдачи</em> напитка: пользователь делает заказ, находясь где-то в другом месте, потом доходит до автомата и нажимает в приложении кнопку «выдать заказ». Мы могли бы, конечно, потребовать, чтобы пользователь создавал заказ автомату, стоя прямо перед ним, но это, в свою очередь, противоречит нашей изначальной концепции, в которой пользователь выбирает и заказывает напиток, исходя из доступных опций, а потом идёт в указанную точку, чтобы его забрать.</p>
<p>Программная выдача напитка потребует добавления ещё одного эндпойнта, ну скажем, <code>program_takeout_endpoint</code>. И вот мы уже запутались в лесу из трёх эндпойнтов:</p>
<ul>
<li>для работы вендинговых автоматов нужно реализовать эндпойнт <code>program_takeout_endpoint</code>, но не нужно реализовывать <code>program_modify_endpoint</code>;</li>
<li>для работы обычных кофеен нужно реализовать эндпойнт <code>program_modify_endpoint</code>, но не нужно реализовывать <code>program_takeout_endpoint</code>.</li>
</ul>
<p>При этом в документации интерфейса мы опишем и тот, и другой эндпойнт. Как несложно заметить, интерфейс <code>takeout</code> весьма специфичен. Если посыпку корицей мы как-то скрыли за общим <code>modify</code>, то на вот такие операции типа подтверждения выдачи нам каждый раз придётся заводить новый метод с уникальным названием. Несложно представить себе, как через несколько итераций интерфейс превратится в свалку из визуально похожих методов, притом формально необязательных — но для подключения своего API нужно будет прочитать документацию каждого и разобраться в том, нужен ли он в конкретной ситуации или нет.</p>
<p>Мы не знаем, правда ли в реальном мире API кофемашин возникнет проблема, подобная описанной. Но мы можем сказать со всей уверенностью, что <em>всегда</em>, когда речь идёт об интеграции «железного» уровня, происходят именно те процессы, которые мы описали: меняется нижележащая технология, и вроде бы понятное и ясное API превращается в свалку из легаси-методов, половина из которых не несёт в себе никакого практического смысла в рамках конкретной интеграции. Если мы добавим к проблеме ещё и технический прогресс — представим, например, что со временем все кофейни станут автоматическими — то мы быстро придём к ситуации, когда половина методов <em>вообще не нужна</em>, как метод запроса бесконтактной выдачи напитка.</p>
<p>Мы не знаем, правда ли в реальном мире API кофемашин возникнет проблема, подобная описанной. Но мы можем сказать со всей уверенностью, что <em>всегда</em>, когда речь идёт об интеграции «железного» уровня, происходят именно те процессы, которые мы описали: меняется нижележащая технология, и вроде бы понятный и ясный API превращается в свалку из легаси-методов, половина из которых не несёт в себе никакого практического смысла в рамках конкретной интеграции. Если мы добавим к проблеме ещё и технический прогресс — представим, например, что со временем все кофейни станут автоматическими — то мы быстро придём к ситуации, когда половина методов <em>вообще не нужна</em>, как метод запроса бесконтактной выдачи напитка.</p>
<p>Заметим также, что мы невольно начали нарушать принцип изоляции уровней абстракции. На уровне API вендингового автомата вообще не существует понятия «бесконтактная выдача», это по сути продуктовый термин.</p>
<p>Каким же образом мы можем решить эту проблему? Одним из двух способов: или досконально изучить предметную область и тренды её развития на несколько лет вперёд, или перейти от сильной связанности к слабой. Как выглядит идеальное решение с точки зрения обеих взаимодействующих сторон? Как-то так:</p>
<p>Каким же образом мы можем решить эту проблему? Одним из двух способов: или досконально изучить предметную область и тренды её развития на несколько лет вперёд, или перейти от сильной связности к слабой. Как выглядит идеальное решение с точки зрения обеих взаимодействующих сторон? Как-то так:</p>
<ul>
<li>вышестоящий API программ не знает, как устроен уровень исполнения его команд; он формулирует задание так, как понимает на своём уровне: сварить такой-то кофе такого-то объёма, с корицей, выдать такому-то пользователю;</li>
<li>нижележащий API исполнения программ не заботится о том, какие ещё вокруг бывают API того же уровня; он трактует только ту часть задания, которая имеет для него смысл.</li>
@ -2687,7 +2688,7 @@ registerProgramRunHandler(apiType, (context) => {
<p>Это замечание совершенно верно. Изменение формата API само по себе не решает проблем, связанных с эволюцией функциональности и нижележащей технологии. Формат API решает другую проблему: как оставить при этом код читаемым и поддерживаемым. Почему в примере с интеграцией через методы код становится нечитаемым? Потому что обе стороны <em>вынуждены</em> имплементировать функциональность, которая в их контексте бессмысленна; и эта имплементация будет состоять из какого-то (хорошо если явного!) способа ответить, что данная функциональность не поддерживается (или, наоборот, поддерживается всегда и безусловно).</p>
<p>Разница между жёстким связыванием и слабым в данном случае состоит в том, что механизм полей и событий <em>не является обязывающим</em>. Вспомним, чего мы добивались:</p>
<ul>
<li>верхнеуровневый контекст не знает, как устроено низкоуровневое API — и он действительно не знает; он описывает те изменения, которые происходят <em>в нём самом</em> и реагирует только на те события, которые имеют смысл <em>для него самого</em>;</li>
<li>верхнеуровневый контекст не знает, как устроен низкоуровневый API — и он действительно не знает; он описывает те изменения, которые происходят <em>в нём самом</em> и реагирует только на те события, которые имеют смысл <em>для него самого</em>;</li>
<li>низкоуровневый контекст не знает ничего об альтернативных реализациях — он обрабатывает только те события, которые имеют смысл на его уровне, и оповещает только о тех событиях, которые могут происходить в его конкретной реализации.</li>
</ul>
<p>В пределе может вообще оказаться так, что обе стороны вообще ничего не знают друг о друге и никак не взаимодействуют — не исключаем, что на каком-то этапе развития технологии именно так и произойдёт.</p>
@ -2826,11 +2827,11 @@ ProgramContext.dispatch = (action) => {
</li>
</ol>
<p>Замена конкретных имплементаций интерфейсами позволяет не только точнее ответить на многие вопросы, которые должны были у вас возникнуть в ходе проектирования API, но и наметить множество возможных векторов развития API, что поможет избежать проблем с неконсистентностью API в ходе дальнейшей эволюции программного продукта.</p><div class="page-break"></div><h3><a href="#chapter-19" class="anchor" id="chapter-19">Глава 19. Блокнот душевного покоя</a></h3>
<p>Помимо вышеперечисленных абстрактных принципов хотелось бы также привести набор вполне конкретных рекомендаций по внесению изменений в существующее API с поддержанием обратной совместимости.</p>
<p>Помимо вышеперечисленных абстрактных принципов хотелось бы также привести набор вполне конкретных рекомендаций по внесению изменений в существующий API с поддержанием обратной совместимости.</p>
<h5><a href="#chapter-19-paragraph-1" id="chapter-19-paragraph-1" class="anchor">1. Помните о подводной части айсберга</a></h5>
<p>То, что вы не давали формальных гарантий и обязательств, совершенно не означает, что эти неформальные гарантии и обязательства можно нарушать. Зачастую даже исправление ошибок в API может привести к неработоспособности чьего-то кода. Можно привести следующий пример из реальной жизни, с которым столкнулся автор этой книги:</p>
<ul>
<li>существовало некоторое API размещения кнопок в визуальном контейнере; по контракту оно принимало позицию размещаемой кнопки (отступы от углов контейнера) в качестве обязательного параметра;</li>
<li>существовал некоторый API размещения кнопок в визуальном контейнере; по контракту оно принимало позицию размещаемой кнопки (отступы от углов контейнера) в качестве обязательного параметра;</li>
<li>в реализации была допущена ошибка: если позицию не передать, то исключения не происходило — добавленные таким образом кнопки размещались в левом верхнем углу контейнера одна за другой;</li>
<li>в день, когда ошибка была исправлена, в техническую поддержку пришло множество обращений от разработчиков, чей код перестал работать; как оказалось, клиенты использовали эту ошибку для того, чтобы последовательно размещать кнопки в левом верхнем углу контейнера.</li>
</ul>
@ -2844,7 +2845,7 @@ ProgramContext.dispatch = (action) => {
<h5><a href="#chapter-19-paragraph-3" id="chapter-19-paragraph-3" class="anchor">3. Реализуйте функциональность своего API поверх публичных интерфейсов</a></h5>
<p>Часто можно увидеть антипаттерн: разработчики API используют внутренние непубличные реализации тех или иных методов взамен существующих в их API публичных. Это происходит по двум причинам:</p>
<ul>
<li>часто публичное API является лишь дополнением к более специализированному внутреннему ПО компании, и наработки, представленные в публичном API, не портируются обратно в непубличную часть проекта, или же разработчики публичного API попросту не знают о существовании аналогичных непубличных функций;</li>
<li>часто публичный API является лишь дополнением к более специализированному внутреннему ПО компании, и наработки, представленные в публичном API, не портируются обратно в непубличную часть проекта, или же разработчики публичного API попросту не знают о существовании аналогичных непубличных функций;</li>
<li>в ходе развития API некоторые интерфейсы абстрагируются, но имплементация уже существующих интерфейсов при этом по разным причинам не затрагивается; например, можно представить себе, что при реализации интерфейса <code>PUT /formatters</code>, описанного в <a href="#chapter16">главе 16</a>, разработчики сделали отдельную, более общую, версию функции форматирования объёма для пользовательских языков в API, но не переписали существующую функцию форматирования для известных языков поверх неё.</li>
</ul>
<p>Помимо очевидных частных проблем, вытекающих из такого подхода (неконсистентность поведения разных функций в API, не найденные при тестировании ошибки), здесь есть и одна глобальная: легко может оказаться, что вашим API попросту невозможно будет пользоваться, если сделать хоть один «шаг в сторону» — попытка воспользоваться любой нестандартной функциональностью может привести к проблемам производительности, многочисленным ошибкам, нестабильной работе и так далее.</p>

Binary file not shown.

View File

@ -14,14 +14,15 @@
<a href="https://www.patreon.com/yatwirl">www.patreon.com/yatwirl</a>
</p>
<p>
The API-first development is one of the hottest technical topics in
2020, since many companies started to realize that API serves as a
The API-first development is one of the hottest technical topics
nowadays, since many companies started to realize that API serves as a
multiplicator to their opportunities—but it also amplifies the design
mistakes as well.
</p>
<p>
The book is dedicated to designing APIs: how to build the architecture
properly, from a high-level planning down to final interfaces.
properly, from a high-level planning down to final interfaces, and to
extend API in a backwards-compatible manner.
</p>
<p>
Illustrations by Maria Konstantinova<br /><a

View File

@ -1,17 +1,17 @@
### Критерии качества API
Прежде чем излагать рекомендации, нам следует определиться с тем, что мы считаем «хорошим» API, и какую пользу мы получаем от того, что наше API «хорошее».
Прежде чем излагать рекомендации, нам следует определиться с тем, что мы считаем «хорошим» API, и какую пользу мы получаем от того, что наш API «хороший».
Начнём со второго вопроса. Очевидно, «хорошесть» API определяется в первую очередь тем, насколько он помогает разработчикам решать стоящие перед ними задачи. (Можно резонно возразить, что решение задач, стоящих перед разработчиками, не обязательно влечёт за собой выполнение целей, которые мы ставим перед собой, предлагая разработчикам API. Однако манипуляция общественным мнением не входит в область интересов автора этой книги: здесь и далее предполагается, что API существует в первую очередь для того, чтобы разработчики решали с его помощью свои задачи, а не ради каких-то завуалированных целей).
Как же дизайн API может помочь разработчику? Очень просто: API должно решать задачи *максимально удобно и понятно*. Путь разработчика от формулирования своей задачи до написания работающего кода должен быть максимально коротким. Это, в том числе, означает, что:
Как же дизайн API может помочь разработчику? Очень просто: API должен решать задачи *максимально удобно и понятно*. Путь разработчика от формулирования своей задачи до написания работающего кода должен быть максимально коротким. Это, в том числе, означает, что:
* из структуры вашего API должно быть максимально очевидно, как решить ту или иную задачу; в идеале разработчику должно быть достаточно одного взгляда на документацию, чтобы понять, с помощью каких сущностей следует решать поставленную задачу;
* API должно быть читаемым: в идеале разработчик, просто глядя в номенклатуру методов, сразу пишет правильный код, не углубляясь в детали (особенно — детали реализации!); немаловажно уточнить, что из интерфейсов объектов должно быть понятно не только решение задачи, но и возможные ошибки и исключения;
* API должно быть консистентно: при разработке новой функциональности, т.е. при обращении к каким-то незнакомым сущностям в API, разработчик может действовать по аналогии с уже известными ему концепциями API, и его код будет работать.
* API должен быть читаемым: в идеале разработчик, просто глядя в номенклатуру методов, сразу пишет правильный код, не углубляясь в детали (особенно — детали реализации!); немаловажно уточнить, что из интерфейсов объектов должно быть понятно не только решение задачи, но и возможные ошибки и исключения;
* API должен быть консистентен: при разработке новой функциональности, т.е. при обращении к каким-то незнакомым сущностям в API, разработчик может действовать по аналогии с уже известными ему концепциями API, и его код будет работать.
Однако статическое удобство и понятность API — это простая часть. В конце концов, никто не стремится специально сделать API нелогичным и нечитаемым — всегда при разработке мы начинаем с каких-то понятных базовых концепций. При минимальном опыте проектирования сложно сделать ядро API, не удовлетворяющее критериям очевидности, читаемости и консистентности.
Проблемы возникают, когда мы начинаем API развивать. Добавление новой функциональности рано или поздно приводит к тому, что некогда простое и понятное API становится наслоением разных концепций, а попытки сохранить обратную совместимость приводят к нелогичным, неочевидным и попросту плохим решениям. Отчасти это связано так же и с тем, что невозможно обладать полным знанием о будущем: ваше понимание о «правильном» API тоже будет меняться со временем, как в объективной части (какие задачи и каким образом решает API), так и в субъективной — что такое очевидность, читабельность и консистентность для вашего API.
Проблемы возникают, когда мы начинаем API развивать. Добавление новой функциональности рано или поздно приводит к тому, что некогда простой и понятный API становится наслоением разных концепций, а попытки сохранить обратную совместимость приводят к нелогичным, неочевидным и попросту плохим решениям. Отчасти это связано так же и с тем, что невозможно обладать полным знанием о будущем: ваше понимание о «правильном» API тоже будет меняться со временем, как в объективной части (какие задачи и каким образом решает API), так и в субъективной — что такое очевидность, читабельность и консистентность для вашего API.
Принципы, которые мы будем излагать ниже, во многом ориентированы именно на то, чтобы API правильно развивалось во времени и не превращалось в нагромождение разнородных неконсистентных интерфейсов. Важно понимать, что такой подход тоже не бесплатен: необходимость держать в голове варианты развития событий и закладывать возможность изменений в API означает избыточность интерфейсов и возможно излишнее абстрагирование. И то, и другое, помимо прочего, усложняет и работу программиста, пользующегося вашим API. **Закладывание перспектив «на будущее» имеет смысл, только если это будущее у API есть, иначе это попросту оверинжиниринг**.
Принципы, которые мы будем излагать ниже, во многом ориентированы именно на то, чтобы API правильно развивался во времени и не превращался в нагромождение разнородных неконсистентных интерфейсов. Важно понимать, что такой подход тоже не бесплатен: необходимость держать в голове варианты развития событий и закладывать возможность изменений в API означает избыточность интерфейсов и возможно излишнее абстрагирование. И то, и другое, помимо прочего, усложняет и работу программиста, пользующегося вашим API. **Закладывание перспектив «на будущее» имеет смысл, только если это будущее у API есть, иначе это попросту оверинжиниринг**.

View File

@ -8,6 +8,6 @@
С нашей точки зрения, подобное поведение ничем не может быть оправдано. Избегайте скрытых налогов на своих пользователей. Если вы можете не ломать обратную совместимость — не ломайте её.
Да, безусловно, поддержка старых версий API — это тоже своего рода налог. Технологии меняются, и, как бы хорошо ни было спроектировано ваше API, всего предусмотреть невозможно. В какой-то момент ценой поддержки старых версий становится невозможность предоставлять новую функциональность и поддерживать новые платформы, и выпустить новую версию всё равно придётся. Однако вы по крайней мере сможете убедить своих потребителей в необходимости перехода.
Да, безусловно, поддержка старых версий API — это тоже своего рода налог. Технологии меняются, и, как бы хорошо ни был спроектирован ваш API, всего предусмотреть невозможно. В какой-то момент ценой поддержки старых версий становится невозможность предоставлять новую функциональность и поддерживать новые платформы, и выпустить новую версию всё равно придётся. Однако вы по крайней мере сможете убедить своих потребителей в необходимости перехода.
Более подробно о жизненном цикле API и политиках выпуска новых версий будет рассказано в разделе II.

View File

@ -35,7 +35,7 @@ Cache-Control: no-cache
* в ответе также могут находиться дополнительные заголовки, на которые мы обращаем внимание;
* телом ответа является JSON, состоящий из единственного поля `error_message`; отсутствие значения поля означает, что его значением является именно то, что в этом поле и ожидается — в данном случае какое-то сообщение об ошибке.
Здесь термин «клиент» означает «приложение, установленное на устройстве пользователя, использующее рассматриваемое API». Приложение может быть как нативным, так и веб-приложением. Термины «агент» и «юзер-агент» являются синонимами термина «клиент».
Здесь термин «клиент» означает «приложение, установленное на устройстве пользователя, использующее рассматриваемый API». Приложение может быть как нативным, так и веб-приложением. Термины «агент» и «юзер-агент» являются синонимами термина «клиент».
Ответ (частично или целиком) и тело запроса могут быть опущены, если в контексте обсуждаемого вопроса их содержание не имеет значения.

View File

@ -6,7 +6,7 @@
* разграничение областей ответственности;
* описание конечных интерфейсов.
Этот алгоритм строит API сверху вниз, от общих требований и сценариев использования до конкретной номенклатуры сущностей; фактически, двигаясь этим путем, вы получите на выходе готовое API — чем этот подход и ценен.
Этот алгоритм строит API сверху вниз, от общих требований и сценариев использования до конкретной номенклатуры сущностей; фактически, двигаясь этим путем, вы получите на выходе готовый API — чем этот подход и ценен.
Может показаться, что наиболее полезные советы приведены в последнем разделе, однако это не так; цена ошибки, допущенной на разных уровнях весьма различна. Если исправить плохое именование довольно просто, то исправить неверное понимание того, зачем вообще нужен API, практически невозможно.

View File

@ -2,7 +2,7 @@
Ключевой вопрос, который вы должны задать себе четыре раза, выглядит так: какую проблему мы решаем? Задать его следует четыре раза с ударением на каждом из четырёх слов.
1. _Какую_ проблему мы решаем? Можем ли мы чётко описать, в какой ситуации гипотетическим потребителям-разработчикам нужно наше API?
1. _Какую_ проблему мы решаем? Можем ли мы чётко описать, в какой ситуации гипотетическим потребителям-разработчикам нужен наш API?
2. Какую _проблему_ мы решаем? А мы правда уверены, что описанная выше ситуация — проблема? Действительно ли кто-то готов платить (в прямом и переносном смысле) за то, что ситуация будет как-то автоматизирована?
@ -36,7 +36,7 @@
Иными словами, должна иметься веская причина, по которой два домена разработки ПО должны быть разделены: есть оператор(ы), предоставляющий API; есть оператор(ы), предоставляющий сервисы пользователям. Их интересы в чем-то различны настолько, что объединение этих двух ролей в одном лице нежелательно. Более подробно мы изложим причины и мотивации делать именно API в разделе III.
Заметим также следующее: вы должны браться делать API тогда и только тогда, когда в ответе на второй вопрос написали «потому что в этом состоит наша экспертиза». Разрабатывая API вы занимаетесь некоторой мета-разработкой: вы пишете ПО для того, чтобы другие могли разрабатывать ПО для решения задачи пользователя. Не обладая экспертизой в обоих этих доменах (API и конечные продукты) написать хорошее API сложно.
Заметим также следующее: вы должны браться делать API тогда и только тогда, когда в ответе на второй вопрос написали «потому что в этом состоит наша экспертиза». Разрабатывая API, вы занимаетесь некоторой мета-разработкой: вы пишете ПО для того, чтобы другие могли разрабатывать ПО для решения задачи пользователя. Не обладая экспертизой в обоих этих доменах (API и конечные продукты) написать хороший API сложно.
Для нашего умозрительного примера предположим, что в недалеком будущем произошло разделение рынка кофе на две группы игроков: одни предоставляют само железо, кофейные аппараты, а другие имеют доступ к потребителю — примерно как это произошло, например, с рынком авиабилетов, где есть собственно авиакомпании, осуществляющие перевозку, и сервисы планирования путешествий, где люди выбирают варианты перелётов. Мы хотим агрегировать доступ к железу, чтобы владельцы приложений могли встраивать заказ кофе.

View File

@ -16,7 +16,7 @@
2. Возможность поддерживать обратную совместимость; правильно подобранные уровни абстракции позволят нам в дальнейшем добавлять новую функциональность, не меняя интерфейс.
3. Поддержание интероперабельности. Правильно выделенные низкоуровневые абстракции позволят нам адаптировать наше API к другим платформам, не меняя высокоуровневый интерфейс.
3. Поддержание интероперабельности. Правильно выделенные низкоуровневые абстракции позволят нам адаптировать наш API к другим платформам, не меняя высокоуровневый интерфейс.
Допустим, мы имеем следующий интерфейс:
@ -72,7 +72,7 @@ POST /v1/orders
3. Наконец, от структур данных, в которых удобно оперировать пользователю к структурам данных, максимально приближенных к «сырым» - в нашем случае от «лунго» и «сети кофеен "Ромашка"» - к сырым байтовый данным, описывающим состояние кофе-машины марки «Доброе утро» в процессе приготовления напитка.
Чем дальше находятся друг от друга программные контексты, которые соединяет наше API - тем более глубокая иерархия сущностей должна получиться у нас в итоге.
Чем дальше находятся друг от друга программные контексты, которые соединяет наш API - тем более глубокая иерархия сущностей должна получиться у нас в итоге.
В нашем примере с определением готовности кофе мы явно пришли к тому, что нам требуется промежуточный уровень абстракции:
@ -98,7 +98,7 @@ POST /v1/orders
}
```
Мы называем этот подход «наивным» не потому, что он неправильный; напротив, это вполне логичное решение «по умолчанию», если вы на данном этапе ещё не знаете или не понимаете, как будет выглядеть ваше API. Проблема его в том, что он умозрительный: он не добавляет понимания того, как устроена предметная область.
Мы называем этот подход «наивным» не потому, что он неправильный; напротив, это вполне логичное решение «по умолчанию», если вы на данном этапе ещё не знаете или не понимаете, как будет выглядеть ваш API. Проблема его в том, что он умозрительный: он не добавляет понимания того, как устроена предметная область.
Хороший разработчик в нашем примере должен спросить: хорошо, а какие вообще говоря существуют варианты? Как можно определять готовность напитка? Если вдруг окажется, что сравнение объёмов — единственный способ определения готовности во всех без исключения кофе-машинах, то почти все рассуждения выше — неверны: можно совершенно спокойно включать в интерфейсы определение готовности кофе по объёму, т.к. никакого другого и не существует. Прежде, чем что-то абстрагировать — надо представлять, *что* мы, собственно, абстрагируем.
@ -148,7 +148,7 @@ POST /v1/orders
GET /execution/status
```
**NB**. На всякий случай отметим, что данное API нарушает множество описанных нами принципов проектирования, начиная с отсутствия версионирования; оно приведено в таком виде по двум причинам: (1) чтобы мы могли показать, как спроектировать API более удачно; (2) скорее всего, в реальной жизни вы получите именно такое API от производителей кофе-машин, и это ещё довольно вменяемый вариант.
**NB**. На всякий случай отметим, что данный API нарушает множество описанных нами принципов проектирования, начиная с отсутствия версионирования; он приведен в таком виде по двум причинам: (1) чтобы мы могли показать, как спроектировать API более удачно; (2) скорее всего, в реальной жизни вы получите именно такой API от производителей кофе-машин, и это ещё довольно вменяемый вариант.
* Машины с предустановленными функциями:
```
@ -366,7 +366,7 @@ POST /v1/runtimes
2. С точки зрения верхнеуровневого API отмена заказа является терминальным действием, т.е. никаких последующих операций уже быть не может; а с точки зрения низкоуровневого API обработка заказа продолжается, т.к. нужно дождаться, когда стакан будет утилизирован, и после этого освободить кофе-машину (т.е. разрешить создание новых рантаймов на ней). Это вторая задача для уровня исполнения: связывать оба статуса, внешний (заказ отменён) и внутренний (исполнение продолжается).
Может показаться, что соблюдение правила изоляции уровней абстракции является избыточным и заставляет усложнять интерфейс. И это в действительности так: важно понимать, что никакая гибкость, логичность, читабельность и расширяемость не бывает бесплатной. Можно построить API так, чтобы оно выполняло свою функцию с минимальными накладными расходами, по сути — дать интерфейс к микроконтроллерам кофе-машины. Однако пользоваться им будет крайне неудобно, и расширяемость такого API будет нулевой.
Может показаться, что соблюдение правила изоляции уровней абстракции является избыточным и заставляет усложнять интерфейс. И это в действительности так: важно понимать, что никакая гибкость, логичность, читабельность и расширяемость не бывает бесплатной. Можно построить API так, чтобы он выполнял свою функцию с минимальными накладными расходами, по сути — дать интерфейс к микроконтроллерам кофе-машины. Однако пользоваться им будет крайне неудобно, и расширяемость такого API будет нулевой.
Выделение уровней абстракции, прежде всего, _логическая_ процедура: как мы объясняем себе и разработчику, из чего состоит наш API. **Абстрагируемая дистанция между сущностями существует объективно**, каким бы образом мы ни написали конкретные интерфейсы. Наша задача состоит только лишь в том, чтобы эта дистанция была разделена на уровни _явно_. Чем более неявно разведены (или, хуже того, перемешаны) уровни абстракции, тем сложнее будет разобраться в вашем API, и тем хуже будет написан использующий его код.

View File

@ -33,11 +33,11 @@
* проверять статус;
* терминировать.
Если внимательно посмотреть на каждый объект, то мы увидим, что, в итоге, каждый объект оказался в смысле своей ответственности составным. Например, `program` будет оперировать данными высшего уровня (рецепт и кофе-машина), дополняя их терминами своего уровня (идентификатор запуска). Это совершенно нормально: API должно связывать контексты.
Если внимательно посмотреть на каждый объект, то мы увидим, что, в итоге, каждый объект оказался в смысле своей ответственности составным. Например, `program` будет оперировать данными высшего уровня (рецепт и кофе-машина), дополняя их терминами своего уровня (идентификатор запуска). Это совершенно нормально: API должен связывать контексты.
#### Сценарии использования
На этом уровне, когда наше API уже в целом понятно устроено и спроектировано, мы должны поставить себя на место разработчика и попробовать написать код. Наша задача: взглянуть на номенклатуру сущностей и понять, как ими будут пользоваться.
На этом уровне, когда наш API уже в целом понятно устроен и спроектирован, мы должны поставить себя на место разработчика и попробовать написать код. Наша задача: взглянуть на номенклатуру сущностей и понять, как ими будут пользоваться.
Представим, что нам поставили задачу, пользуясь нашим кофейным API, разработать приложение для заказа кофе. Какой код мы напишем?
@ -281,7 +281,7 @@ POST /v1/orders
}, …]
}
```
Такое API читать и воспринимать гораздо удобнее, нежели сплошную простыню различных атрибутов. Более того, возможно, стоит на будущее сразу дополнительно сгруппировать, например, `place` и `route` в одну структуру `location`, или `offer` и `pricing` в одну более общую структуру.
Такой API читать и воспринимать гораздо удобнее, нежели сплошную простыню различных атрибутов. Более того, возможно, стоит на будущее сразу дополнительно сгруппировать, например, `place` и `route` в одну структуру `location`, или `offer` и `pricing` в одну более общую структуру.
Важно, что читабельность достигается не просто снижением количества сущностей на одном уровне. Декомпозиция должна производиться таким образом, чтобы разработчик при чтении интерфейса сразу понимал: так, вот здесь находится описание заведения, оно мне пока неинтересно и углубляться в эту ветку я пока не буду. Если перемешать данные, которые нужны в моменте одновременно для выполнения действия по разным композитам — это только ухудшит читабельность, а не улучшит.

View File

@ -10,7 +10,7 @@
Например, требование консистентности номенклатуры существует затем, чтобы разработчик тратил меньше времени на чтение документации; если вам _необходимо_, чтобы разработчик обязательно прочитал документацию по какому-то методу, вполне разумно сделать его сигнатуру нарочито неконсистентно.
Это соображение применимо ко всем принципам ниже. Если из-за следования правилам у вас получается неудобное, громоздкое, неочевидное API — это повод пересмотреть правила (или API).
Это соображение применимо ко всем принципам ниже. Если из-за следования правилам у вас получается неудобный, громоздкий, неочевидный API — это повод пересмотреть правила (или API).
Важно понимать, что вы вольны вводить свои собственные конвенции. Например, в некоторых фреймворках сознательно отказываются от парных методов `set_entity` / `get_entity` в пользу одного метода `entity` с опциональным параметром. Важно только проявить последовательность в её применении — если такая конвенция вводится, то абсолютно все методы API должны иметь подобную полиморфную сигнатуру, или по крайней мере должен существовать принцип именования, отличающий такие комбинированные методы от обычных вызовов.
@ -121,7 +121,7 @@ strpbrk (str1, str2)
Аналогично, если ожидается булево значение, то это должно быть очевидно из названия, т.е. именование должно описывать некоторое качественное состояние, например, `is_ready`, `open_now`.
**Плохо**: `"task.status": true` — неочевидно, что статус бинарен, к тому же такое API будет нерасширяемым.
**Плохо**: `"task.status": true` — неочевидно, что статус бинарен, к тому же такой API будет нерасширяемым.
**Хорошо**: `"task.is_finished": true`.
@ -788,7 +788,7 @@ GET /v1/records?cursor=<значение курсора>
// начиная с записи с номером offset
GET /records?sort_by=date_modified&sort_order=desc&limit=10&offset=100
```
Сортировка по дате модификации обычно означает, что данные могут меняться. Иными словами, между запросом первой порции данных и запросом второй порции данных какая-то запись может измениться; она просто пропадёт из перечисления, т.к. автоматически попадает на первую страницу. Клиент никогда не получит те записи, которые менялись во время перебора, и у него даже нет способа узнать о самом факте такого пропуска. Помимо этого отметим, что такое API нерасширяемо — невозможно добавить сортировку по двум или более полям.
Сортировка по дате модификации обычно означает, что данные могут меняться. Иными словами, между запросом первой порции данных и запросом второй порции данных какая-то запись может измениться; она просто пропадёт из перечисления, т.к. автоматически попадает на первую страницу. Клиент никогда не получит те записи, которые менялись во время перебора, и у него даже нет способа узнать о самом факте такого пропуска. Помимо этого отметим, что такой API нерасширяем — невозможно добавить сортировку по двум и более полям.
**Хорошо**: в представленной постановке задача, собственно говоря, не решается. Список записей по дате изменения всегда будет непредсказуемо изменяться, поэтому необходимо изменить сам подход к формированию данных, одним из двух способов.
@ -1019,7 +1019,7 @@ POST /search
Дело в том, что множество параметров, потенциально влияющих на работу API, зависят не от языка, а именно от расположения пользователя. В частности, правила форматирования чисел (разделители целой и дробной частей, разделители разрядов) и дат, первый день недели, раскладка клавиатуры, система единиц измерения (которая к тому же может оказаться не десятичной!) и так далее. В некоторых ситуациях необходимо хранить две локации: та, в которой пользователь находится, и та, которую пользователь сейчас просматривает. Например, если пользователь из США планирует туристическую поездку в Европу, то цены ему желательно показывать в местной валюте, но отформатированными согласно правилам американского письма.
Следует иметь в виду, что явной передачи локации может оказаться недостаточно, поскольку в мире существуют территориальные конфликты и спорные территории. Каким образом API должно себя вести при попадании координат пользователя на такие территории — вопрос, к сожалению, в первую очередь юридический. Автору этой книги приходилось как-то разрабатывать API, в котором пришлось вводить концепцию «территория государства A по мнению официальных органов государства Б».
Следует иметь в виду, что явной передачи локации может оказаться недостаточно, поскольку в мире существуют территориальные конфликты и спорные территории. Каким образом API должен себя вести при попадании координат пользователя на такие территории — вопрос, к сожалению, в первую очередь юридический. Автору этой книги приходилось как-то разрабатывать API, в котором пришлось вводить концепцию «территория государства A по мнению официальных органов государства Б».
**Важно**: различайте локализацию для конечного пользователя и локализацию для разработчика. В примере из п. 19 сообщение `localized_message` адресовано пользователю — его должно показать приложение, если в коде обработка такой ошибки не предусмотрена. Это сообщение должно быть написано на указанном в запросе языке и отформатировано согласно правилам локации пользователя. А вот сообщение `details.checks_failed[].message` написано не для пользователя, а для разработчика, который будет разбираться с проблемой. Соответственно, написано и отформатировано оно должно быть понятным для разработчика образом — что, скорее всего, означает «на английском языке», т.к. английский де факто является стандартом в мире разработки программного обеспечения.

View File

@ -1,4 +1,4 @@
### Приложение к разделу I. Модельное API
### Приложение к разделу I. Модельный API
Суммируем текущее состояние нашего учебного API.

View File

@ -12,7 +12,7 @@
С нашей точки зрения длительность поддержания обратной совместимости следует увязывать с длительностью жизненных циклов приложений в соответствующей предметной области. Хороший ориентир в большинстве случаев — это LTS-периоды платформ. Так как приложение все равно будет переписано в связи с окончанием поддержки платформы, нормально предложить также и переход на новую версию API. В основных предметных областях (десктопные и мобильные операционные системы) этот срок исчисляется несколькими годами.
Почему обратную совместимость необходимо поддерживать (в том числе предпринимать необходимые меры ещё на этапе проектирования API) — понятно из определения. Прекращение работы приложения (полное или частичное) по вине поставщика API — крайне неприятное событие, а то и катастрофа, для любого разработчика, особенно если он платит за это API деньги.
Почему обратную совместимость необходимо поддерживать (в том числе предпринимать необходимые меры ещё на этапе проектирования API) — понятно из определения. Прекращение работы приложения (полное или частичное) по вине поставщика API — крайне неприятное событие, а то и катастрофа, для любого разработчика, особенно если он платит за этот API деньги.
Но развернём теперь проблему в другую сторону: а почему вообще возникает проблема с поддержанием обратной совместимости? Почему мы можем *хотеть* её нарушить? Ответ на этот вопрос, при кажущейся простоте, намного сложнее, чем на предыдущий.
@ -46,11 +46,11 @@
* пользователь не хочет обновляться (в том числе потому, что, по мнению пользователя, разработчики приложения его «испортили» в новых версиях);
* пользователь не может обновиться вообще, потому что его устройство больше не поддерживается.
В современных реалиях все три категории в сумме легко могут составлять десятки процентов аудитории. Это означает, что прекращение поддержки любой версии API является весьма заметным событием — особенно если приложения разработчика поддерживают более широкий спектр версий платформы, нежели ваше API.
В современных реалиях все три категории в сумме легко могут составлять десятки процентов аудитории. Это означает, что прекращение поддержки любой версии API является весьма заметным событием — особенно если приложения разработчика поддерживают более широкий спектр версий платформы, нежели ваш API.
Вы можете не выпускать вообще никаких SDK, предоставляя только серверное API в виде, например, HTTP эндпойнтов. Вам может показаться, что таким образом, пусть ваше API и стало менее конкурентоспособным на рынке из-за отсутствия SDK, вы облегчили себе задачу поддержания обратной совместимости. На самом деле это совершенно не так: раз вы не предоставляете свой SDK — или разработчики возьмут неофициальный SDK (если кто-то его сделает), или просто каждый из них напишет по фреймворку. Стратегия «ваш фреймворк — ваша ответственность», к счастью или к сожалению, работает плохо: если на вашем API пишут некачественные приложения — значит, ваше API само некачественное. Уж точно по мнению разработчиков, а может и по мнению пользователей, если работа API внутри приложения пользователю видна.
Вы можете не выпускать вообще никаких SDK, предоставляя только серверный API в виде, например, HTTP эндпойнтов. Вам может показаться, что таким образом, пусть ваш API и стал менее конкурентоспособным на рынке из-за отсутствия SDK, вы облегчили себе задачу поддержания обратной совместимости. На самом деле это совершенно не так: раз вы не предоставляете свой SDK — или разработчики возьмут неофициальный SDK (если кто-то его сделает), или просто каждый из них напишет по фреймворку. Стратегия «ваш фреймворк — ваша ответственность», к счастью или к сожалению, работает плохо: если на вашем API пишут некачественные приложения — значит, ваш API сам некачественный. Уж точно по мнению разработчиков, а может и по мнению пользователей, если работа API внутри приложения пользователю видна.
Конечно, если ваше API достаточно stateless и не требует клиентских SDK (или же можно обойтись просто автогенерацией SDK из спецификации), эти проблемы будут гораздо менее заметны, но избежать их полностью можно только одним способом — никогда не выпуская новых версий API. Во всех остальных случаях вы будете иметь дело с какой-то гребёнкой распределения количества пользователей по версиям API и версиям SDK.
Конечно, если ваш API достаточно stateless и не требует клиентских SDK (или же можно обойтись просто автогенерацией SDK из спецификации), эти проблемы будут гораздо менее заметны, но избежать их полностью можно только одним способом — никогда не выпуская новых версий API. Во всех остальных случаях вы будете иметь дело с какой-то гребёнкой распределения количества пользователей по версиям API и версиям SDK.
#### Эволюция предметной области
@ -93,7 +93,7 @@
Для минорных версий возможны два варианта:
* если вы предоставляете только серверное API и компилируемые SDK, вы можете в принципе не поддерживать никакие минорные версии API, помимо актуальной: серверное API находится полностью под вашим контролем, и вы можете оперативно исправить любые проблемы с логикой;
* если вы предоставляете только серверный API и компилируемые SDK, вы можете в принципе не поддерживать никакие минорные версии API, помимо актуальной: серверный API находится полностью под вашим контролем, и вы можете оперативно исправить любые проблемы с логикой;
* если вы предоставляете code-on-demand SDK, то вот здесь хорошим тоном является поддержка предыдущих минорных версий SDK в работающем состоянии на срок, достаточный для того, чтобы разработчики могли протестировать своё приложение с новой версией и внести какие-то правки по необходимости. Так как полностью переписывать приложения при этом не надо, разумно ориентироваться на длину релизных циклов в вашей индустрии, обычно это несколько месяцев в худшем случае.
Дополнительно в разделе III мы также обсудим, каким образом предупреждать потребителей о выходе новых версий и прекращении поддержки старых, и как стимулировать их переходить на новые версии API.

View File

@ -4,7 +4,7 @@
##### Предоставляйте минимальный объём функциональности
В любой момент времени ваше API подобно айсбергу: у него есть видимая (документированная) часть и невидимая — недокументированная. В хорошем API эти две части соотносятся друг с другом примерно как надводная и подводная часть настоящего айсберга, 1 к 10. Почему так? Из двух очевидных соображений.
В любой момент времени ваш API подобен айсбергу: у него есть видимая (документированная) часть и невидимая — недокументированная. В хорошем API эти две части соотносятся друг с другом примерно как надводная и подводная часть настоящего айсберга, 1 к 10. Почему так? Из двух очевидных соображений.
* Компьютеры существуют, чтобы сложные вещи делать просто, не наоборот. Код, который напишут разработчики поверх вашего API, должен в простых и лаконичных выражениях описывать решение сложной проблемы. Поэтому «внутри» ваш код, скорее всего, будет опираться на мощную номенклатуру непубличной функциональности.
@ -58,7 +58,7 @@ if (status) {
}
```
Мы полагаем, что можно не уточнять, что писать код, подобный вышеприведённому, ни в коем случае нельзя. Уж если вы действительно предоставляете нестрого консистентное API, то либо операция `createOrder` в SDK должна быть асинхронной и возвращать результат только по готовности всех реплик, либо политика перезапросов должна быть скрыта внутри операции `getStatus`.
Мы полагаем, что можно не уточнять, что писать код, подобный вышеприведённому, ни в коем случае нельзя. Уж если вы действительно предоставляете нестрого консистентный API, то либо операция `createOrder` в SDK должна быть асинхронной и возвращать результат только по готовности всех реплик, либо политика перезапросов должна быть скрыта внутри операции `getStatus`.
Если же нестрогая консистентность не была описана с самого начала — вы не можете внести такие изменения в API. Это эффективный слом обратной совместимости, который к тому же приведёт к огромным проблемам ваших потребителей, поскольку проблема будет воспроизводиться случайным образом.
@ -76,7 +76,7 @@ resolve();
Этот код полагается на то, что callback-функция, переданная в `new Promise` будет выполнена *синхронно*, и переменная `resolve` будет инициализирована к моменту вызова `resolve()`. Однако это конвенция абсолютно ниоткуда не следует: ничто в сигнатуре конструктора `new Promise` не указывает на синхронный вызов callback-а.
Разработчики языка, конечно, могут позволить себе такие фокусы. Однако вы как разработчик API — не можете. Вы должны как минимум задокументировать это поведение и подобрать сигнатуры так, чтобы оно было очевидно; но вообще хорошим советом будет избегать таких конвенций, поскольку они банально неочевидны при прочтении кода, использующего ваше API. Ну и конечно же ни при каких обстоятельствах вы не можете изменить это поведение с синхронного на асинхронное.
Разработчики языка, конечно, могут позволить себе такие фокусы. Однако вы как разработчик API — не можете. Вы должны как минимум задокументировать это поведение и подобрать сигнатуры так, чтобы оно было очевидно; но вообще хорошим советом будет избегать таких конвенций, поскольку они банально неочевидны при прочтении кода, использующего ваш API. Ну и конечно же ни при каких обстоятельствах вы не можете изменить это поведение с синхронного на асинхронное.
**Пример 3**. Представьте, что вы предоставляете API для анимаций, в котором есть две независимые функции:

View File

@ -2,7 +2,7 @@
В предыдущих разделах мы старались приводить теоретические правила и иллюстрировать их на практических примерах. Однако понимание принципов проектирования API, устойчивого к изменениям, как ничто другое требует прежде всего практики. Знание о том, куда стоит «постелить соломку» — оно во многом «сын ошибок трудных». Нельзя предусмотреть всего — но можно выработать необходимый уровень технической интуиции.
Поэтому в этом разделе мы поступим следующим образом: возьмём наше [модельное API](#chapter-12) из предыдущего раздела, и проверим его на устойчивость в каждой возможной точке — проведём некоторый «вариационный анализ» наших интерфейсов. Ещё более конкретно — к каждой сущности мы подойдём с вопросом «что, если?» — что, если нам потребуется предоставить партнерам возможность написать свою независимую реализацию этого фрагмента логики.
Поэтому в этом разделе мы поступим следующим образом: возьмём наше [модельный API](#chapter-12) из предыдущего раздела, и проверим его на устойчивость в каждой возможной точке — проведём некоторый «вариационный анализ» наших интерфейсов. Ещё более конкретно — к каждой сущности мы подойдём с вопросом «что, если?» — что, если нам потребуется предоставить партнерам возможность написать свою независимую реализацию этого фрагмента логики.
**NB**. В рассматриваемых нами примерах мы будем выстраивать интерфейсы так, чтобы связывание разных сущностей происходило динамически в реальном времени; на практике такие интеграции будут делаться на стороне сервера путём написания ad hoc кода и формирования конкретных договорённостей с конкретным клиентом, однако мы для целей обучения специально будем идти более сложным и абстрактным путём. Динамическое связывание в реальном времени применимо скорее к сложным программным конструктам типа API операционных систем или встраиваемых библиотек; приводить обучающие примеры на основе систем подобной сложности было бы, однако, чересчур затруднительно.
@ -46,7 +46,7 @@ PUT /v1/partners/{partnerId}/coffee-machines
* перечислим все неявные предположения, которые мы допустили;
* перечислим все неявные механизмы связывания, которые необходимы для функционирования платформы.
Может показаться, что в нашем API нет ни того, ни другого, ведь оно очень просто и по сути просто сводится к вызову какого-то HTTP-метода — но это неправда.
Может показаться, что в нашем API нет ни того, ни другого, ведь он очень прост и по сути просто сводится к вызову какого-то HTTP-метода — но это неправда.
1. Предполагается, что каждая кофе-машина поддерживает все возможные опции заказа (например, допустимый объём напитка).
2. Нет необходимости показывать пользователю какую-то дополнительную информацию о том, что заказ готовится на новых типах кофе-машин.
3. Цена напитка не зависит ни от партнёра, ни от типа кофе-машины.

View File

@ -1,6 +1,6 @@
### Сильная связность и сопутствующие проблемы
Для демонстрации проблем сильной связности перейдём теперь к *действительно интересным* вещам. Продолжим наш «вариационный анализ»: что, если партнёры хотят не просто готовить кофе по стандартным рецептам, но и предлагать свои авторские напитки? Вопрос этот с подвохом: в том виде, как мы описали партнёрское API в предыдущей главе, факт существования партнерской сети никак не отражен в нашем API с точки зрения продукта, предлагаемого пользователю, а потому представляет собой довольно простой кейс. Если же мы пытаемся предоставить не какую-то дополнительную возможность, а модифицировать саму базовую функциональность API, то мы быстро столкнёмся с проблемами совсем другого порядка.
Для демонстрации проблем сильной связности перейдём теперь к *действительно интересным* вещам. Продолжим наш «вариационный анализ»: что, если партнёры хотят не просто готовить кофе по стандартным рецептам, но и предлагать свои авторские напитки? Вопрос этот с подвохом: в том виде, как мы описали партнёрскый API в предыдущей главе, факт существования партнерской сети никак не отражен в нашем API с точки зрения продукта, предлагаемого пользователю, а потому представляет собой довольно простой кейс. Если же мы пытаемся предоставить не какую-то дополнительную возможность, а модифицировать саму базовую функциональность API, то мы быстро столкнёмся с проблемами совсем другого порядка.
Итак, добавим ещё один эндпойнт — для регистрации собственного рецепта партнёра.
@ -56,7 +56,7 @@ POST /v1/recipes
Как бы парадоксально это ни звучало, обратное утверждение тоже верно: высокоуровневые сущности тоже не должны определять низкоуровневые. Это попросту не их ответственность. Выход из этого логического лабиринта таков: высокоуровневые сущности должны *определять контекст*, который другие объекты будут интерпретировать. Чтобы спроектировать добавление нового рецепта нам нужно не формат данных подобрать — нам нужно понять, какие (возможно, неявные, т.е. не представленные в виде API) контексты существуют в нашей предметной области.
Как уже понятно, существует контекст локализации. Есть какой-то набор языков и регионов, которые мы поддерживаем в нашем API, и есть требования — что конкретно необходимо предоставить партнёру, чтобы API заработало на новом языке в новом регионе. Конкретно в случае объёма кофе где-то в недрах нашего API есть функция форматирования строк для отображения объёма напитка:
Как уже понятно, существует контекст локализации. Есть какой-то набор языков и регионов, которые мы поддерживаем в нашем API, и есть требования — что конкретно необходимо предоставить партнёру, чтобы API заработал на новом языке в новом регионе. Конкретно в случае объёма кофе где-то в недрах нашего API есть функция форматирования строк для отображения объёма напитка:
```
l10n.volume.format(value, language_code, country_code)
@ -64,7 +64,7 @@ l10n.volume.format(value, language_code, country_code)
// l10n.formatVolume('300ml', 'en', 'US') → '10 fl oz'
```
Чтобы наше API корректно заработало с новым языком или регионом, партнер должен или задать эту функцию, или указать, какую из существующих локализаций необходимо использовать. Для этого мы абстрагируем-и-расширяем API, в соответствии с описанной в предыдущей главе процедурой, и добавляем новый эндпойнт — настройки форматирования:
Чтобы наш API корректно заработал с новым языком или регионом, партнер должен или задать эту функцию, или указать, какую из существующих локализаций необходимо использовать. Для этого мы абстрагируем-и-расширяем API, в соответствии с описанной в предыдущей главе процедурой, и добавляем новый эндпойнт — настройки форматирования:
```
// Добавляем общее правило форматирования
@ -198,4 +198,4 @@ POST /v1/recipes/custom
}
```
Заметим, что в таком формате мы сразу закладываем важное допущение: различные партнёры могут иметь как полностью изолированные неймспейсы, так и разделять их. Более того, мы можем ввести специальные неймспейсы типа "common", которые позволят публиковать новые рецепты для всех. (Это, кстати говоря, хорошо ещё и тем, что такое API мы сможем использовать для организации нашей собственной панели управления контентом.)
Заметим, что в таком формате мы сразу закладываем важное допущение: различные партнёры могут иметь как полностью изолированные неймспейсы, так и разделять их. Более того, мы можем ввести специальные неймспейсы типа "common", которые позволят публиковать новые рецепты для всех. (Это, кстати говоря, хорошо ещё и тем, что такой API мы сможем использовать для организации нашей собственной панели управления контентом.)

View File

@ -31,7 +31,7 @@ PUT /v1/api-types/{api_type}
}
```
Исходя из общей логики мы можем предположить, что любое API так или иначе будет выполнять три функции: запускать программы с указанными параметрами, возвращать текущий статус запуска и завершать (отменять) заказ. Самый очевидный подход к реализации такого API — просто потребовать от партнёра имплементировать вызов этих трёх функций удалённо, например следующим образом:
Исходя из общей логики мы можем предположить, что любой API так или иначе будет выполнять три функции: запускать программы с указанными параметрами, возвращать текущий статус запуска и завершать (отменять) заказ. Самый очевидный подход к реализации такого API — просто потребовать от партнёра имплементировать вызов этих трёх функций удалённо, например следующим образом:
```
// Эндпойнт добавления списка
@ -61,7 +61,7 @@ PUT /v1/api-types/{api_type}
Пункт 2 очень легко опровергнуть, что автоматически вскроет проблемы пункта 1. Предположим для начала, что в ходе развития функциональности мы решили дать пользователю возможность изменять свой заказ уже после того, как он создан — ну, например, попросить посыпать кофе корицей или выдать заказ бесконтактно. Это автоматически влечёт за собой добавление нового эндпойнта, ну скажем, `program_modify_endpoint`, и новых сложностей в формате обмена данными (нам нужно уметь понимать в реальном времени, можно ли этот конкретный кофе посыпать корицей). Что важно, и то, и другое (и эндпойнт, и новые поля данных) из соображений обратной совместимости будут необязательными.
Теперь попытаемся придумать какой-нибудь пример реального мира, который не описывается нашими тремя императивами. Это довольно легко: допустим, мы подключим через наше API не кофейню, а вендинговый автомат. Это, с одной стороны, означает, что эндпойнт `modify` и вся его обвязка для этого типа API бесполезны — автомат не умеет посыпать кофе корицей, а требование бесконтактной выдачи попросту ничего не значит. С другой, автомат, в отличие от оперируемой людьми кофейни, требует программного способа *подтверждения выдачи* напитка: пользователь делает заказ, находясь где-то в другом месте, потом доходит до автомата и нажимает в приложении кнопку «выдать заказ». Мы могли бы, конечно, потребовать, чтобы пользователь создавал заказ автомату, стоя прямо перед ним, но это, в свою очередь, противоречит нашей изначальной концепции, в которой пользователь выбирает и заказывает напиток, исходя из доступных опций, а потом идёт в указанную точку, чтобы его забрать.
Теперь попытаемся придумать какой-нибудь пример реального мира, который не описывается нашими тремя императивами. Это довольно легко: допустим, мы подключим через наш API не кофейню, а вендинговый автомат. Это, с одной стороны, означает, что эндпойнт `modify` и вся его обвязка для этого типа API бесполезны — автомат не умеет посыпать кофе корицей, а требование бесконтактной выдачи попросту ничего не значит. С другой, автомат, в отличие от оперируемой людьми кофейни, требует программного способа *подтверждения выдачи* напитка: пользователь делает заказ, находясь где-то в другом месте, потом доходит до автомата и нажимает в приложении кнопку «выдать заказ». Мы могли бы, конечно, потребовать, чтобы пользователь создавал заказ автомату, стоя прямо перед ним, но это, в свою очередь, противоречит нашей изначальной концепции, в которой пользователь выбирает и заказывает напиток, исходя из доступных опций, а потом идёт в указанную точку, чтобы его забрать.
Программная выдача напитка потребует добавления ещё одного эндпойнта, ну скажем, `program_takeout_endpoint`. И вот мы уже запутались в лесу из трёх эндпойнтов:
* для работы вендинговых автоматов нужно реализовать эндпойнт `program_takeout_endpoint`, но не нужно реализовывать `program_modify_endpoint`;
@ -69,7 +69,7 @@ PUT /v1/api-types/{api_type}
При этом в документации интерфейса мы опишем и тот, и другой эндпойнт. Как несложно заметить, интерфейс `takeout` весьма специфичен. Если посыпку корицей мы как-то скрыли за общим `modify`, то на вот такие операции типа подтверждения выдачи нам каждый раз придётся заводить новый метод с уникальным названием. Несложно представить себе, как через несколько итераций интерфейс превратится в свалку из визуально похожих методов, притом формально необязательных — но для подключения своего API нужно будет прочитать документацию каждого и разобраться в том, нужен ли он в конкретной ситуации или нет.
Мы не знаем, правда ли в реальном мире API кофемашин возникнет проблема, подобная описанной. Но мы можем сказать со всей уверенностью, что *всегда*, когда речь идёт об интеграции «железного» уровня, происходят именно те процессы, которые мы описали: меняется нижележащая технология, и вроде бы понятное и ясное API превращается в свалку из легаси-методов, половина из которых не несёт в себе никакого практического смысла в рамках конкретной интеграции. Если мы добавим к проблеме ещё и технический прогресс — представим, например, что со временем все кофейни станут автоматическими — то мы быстро придём к ситуации, когда половина методов *вообще не нужна*, как метод запроса бесконтактной выдачи напитка.
Мы не знаем, правда ли в реальном мире API кофемашин возникнет проблема, подобная описанной. Но мы можем сказать со всей уверенностью, что *всегда*, когда речь идёт об интеграции «железного» уровня, происходят именно те процессы, которые мы описали: меняется нижележащая технология, и вроде бы понятный и ясный API превращается в свалку из легаси-методов, половина из которых не несёт в себе никакого практического смысла в рамках конкретной интеграции. Если мы добавим к проблеме ещё и технический прогресс — представим, например, что со временем все кофейни станут автоматическими — то мы быстро придём к ситуации, когда половина методов *вообще не нужна*, как метод запроса бесконтактной выдачи напитка.
Заметим также, что мы невольно начали нарушать принцип изоляции уровней абстракции. На уровне API вендингового автомата вообще не существует понятия «бесконтактная выдача», это по сути продуктовый термин.
@ -118,7 +118,7 @@ registerProgramRunHandler(apiType, (context) => {
Это замечание совершенно верно. Изменение формата API само по себе не решает проблем, связанных с эволюцией функциональности и нижележащей технологии. Формат API решает другую проблему: как оставить при этом код читаемым и поддерживаемым. Почему в примере с интеграцией через методы код становится нечитаемым? Потому что обе стороны *вынуждены* имплементировать функциональность, которая в их контексте бессмысленна; и эта имплементация будет состоять из какого-то (хорошо если явного!) способа ответить, что данная функциональность не поддерживается (или, наоборот, поддерживается всегда и безусловно).
Разница между жёстким связыванием и слабым в данном случае состоит в том, что механизм полей и событий *не является обязывающим*. Вспомним, чего мы добивались:
* верхнеуровневый контекст не знает, как устроено низкоуровневое API — и он действительно не знает; он описывает те изменения, которые происходят *в нём самом* и реагирует только на те события, которые имеют смысл *для него самого*;
* верхнеуровневый контекст не знает, как устроен низкоуровневый API — и он действительно не знает; он описывает те изменения, которые происходят *в нём самом* и реагирует только на те события, которые имеют смысл *для него самого*;
* низкоуровневый контекст не знает ничего об альтернативных реализациях — он обрабатывает только те события, которые имеют смысл на его уровне, и оповещает только о тех событиях, которые могут происходить в его конкретной реализации.
В пределе может вообще оказаться так, что обе стороны вообще ничего не знают друг о друге и никак не взаимодействуют — не исключаем, что на каком-то этапе развития технологии именно так и произойдёт.

View File

@ -1,11 +1,11 @@
### Блокнот душевного покоя
Помимо вышеперечисленных абстрактных принципов хотелось бы также привести набор вполне конкретных рекомендаций по внесению изменений в существующее API с поддержанием обратной совместимости.
Помимо вышеперечисленных абстрактных принципов хотелось бы также привести набор вполне конкретных рекомендаций по внесению изменений в существующий API с поддержанием обратной совместимости.
##### Помните о подводной части айсберга
То, что вы не давали формальных гарантий и обязательств, совершенно не означает, что эти неформальные гарантии и обязательства можно нарушать. Зачастую даже исправление ошибок в API может привести к неработоспособности чьего-то кода. Можно привести следующий пример из реальной жизни, с которым столкнулся автор этой книги:
* существовало некоторое API размещения кнопок в визуальном контейнере; по контракту оно принимало позицию размещаемой кнопки (отступы от углов контейнера) в качестве обязательного параметра;
* существовал некоторый API размещения кнопок в визуальном контейнере; по контракту оно принимало позицию размещаемой кнопки (отступы от углов контейнера) в качестве обязательного параметра;
* в реализации была допущена ошибка: если позицию не передать, то исключения не происходило — добавленные таким образом кнопки размещались в левом верхнем углу контейнера одна за другой;
* в день, когда ошибка была исправлена, в техническую поддержку пришло множество обращений от разработчиков, чей код перестал работать; как оказалось, клиенты использовали эту ошибку для того, чтобы последовательно размещать кнопки в левом верхнем углу контейнера.
@ -21,7 +21,7 @@
##### Реализуйте функциональность своего API поверх публичных интерфейсов
Часто можно увидеть антипаттерн: разработчики API используют внутренние непубличные реализации тех или иных методов взамен существующих в их API публичных. Это происходит по двум причинам:
* часто публичное API является лишь дополнением к более специализированному внутреннему ПО компании, и наработки, представленные в публичном API, не портируются обратно в непубличную часть проекта, или же разработчики публичного API попросту не знают о существовании аналогичных непубличных функций;
* часто публичный API является лишь дополнением к более специализированному внутреннему ПО компании, и наработки, представленные в публичном API, не портируются обратно в непубличную часть проекта, или же разработчики публичного API попросту не знают о существовании аналогичных непубличных функций;
* в ходе развития API некоторые интерфейсы абстрагируются, но имплементация уже существующих интерфейсов при этом по разным причинам не затрагивается; например, можно представить себе, что при реализации интерфейса `PUT /formatters`, описанного в [главе 16](#chapter16), разработчики сделали отдельную, более общую, версию функции форматирования объёма для пользовательских языков в API, но не переписали существующую функцию форматирования для известных языков поверх неё.
Помимо очевидных частных проблем, вытекающих из такого подхода (неконсистентность поведения разных функций в API, не найденные при тестировании ошибки), здесь есть и одна глобальная: легко может оказаться, что вашим API попросту невозможно будет пользоваться, если сделать хоть один «шаг в сторону» — попытка воспользоваться любой нестандартной функциональностью может привести к проблемам производительности, многочисленным ошибкам, нестабильной работе и так далее.

View File

@ -15,14 +15,15 @@
</p>
<p>
«API-first» подход — одна из самых горячих горячих тем в разработке
программного обеспечения в 2020. Многие компании начали понимать, что
API выступает мультипликатором их возможностей — но также умножает и
программного обеспечения в наше время. Многие компании начали понимать,
что API выступает мультипликатором их возможностей — но также умножает и
допущенные ошибки.
</p>
<p>
Эта книга посвящена проектированию API: как правильно выстроить
архитектуру, начиная с высокоуровневого планирования и заканчивая
деталями реализации конкретных интерфейсов.
деталями реализации конкретных интерфейсов, и как развивать API, не
нарушая обратную совместимость.
</p>
<p>
Иллюстрации: Мария Константинова<br /><a

View File

@ -4,7 +4,7 @@
"chapter": "Глава",
"toc": "Содержание",
"frontPage": "Титульный лист",
"description": "Разработка API — особый навык: API является как мультипликатором ваших возможностей, так и мультипликатором ваших ошибок. Эта книга написана для того, чтобы поделиться опытом и изложить лучшие практики проектирования API. Книга состоит из трёх больших разделов. В первом разделе мы поговорим о проектировании API на стадии разработки концепции — как грамотно выстроить архитектуру, от крупноблочного планирования до конечных интерфейсов. Второй раздел посвящён развитию существующих API с сохранением обратной совместимости",
"description": "Разработка API — особый навык: API является как мультипликатором ваших возможностей, так и мультипликатором ваших ошибок. Эта книга написана для того, чтобы поделиться опытом и изложить лучшие практики проектирования API. Книга состоит из двух больших разделов. В первом разделе мы поговорим о проектировании API на стадии разработки концепции — как грамотно выстроить архитектуру, от крупноблочного планирования до конечных интерфейсов. Второй раздел посвящён развитию существующих API с сохранением обратной совместимости",
"locale": "ru_RU",
"file": "API",
"url": "https://twirl.github.io/The-API-Book/docs/API.ru.html",