1
0
mirror of https://github.com/twirl/The-API-Book.git synced 2025-07-12 22:50:21 +02:00

Описание конечных интерфейсов - начало

This commit is contained in:
Sergey Konstantinov
2020-11-06 17:36:36 +03:00
parent 5075c9e72d
commit 9edb3ff27d
2 changed files with 59 additions and 5 deletions

64
dist/API.ru.html vendored
View File

@ -50,7 +50,7 @@ h2 {
</head>
<body>
<article id="percollate-page-24ecbb90-1f6e-11eb-a6a0-eb3dafefce9d" class="article">
<article id="percollate-page-5065e100-203d-11eb-bef9-4551dbb4656b" class="article">
<header class="article__header">
<h1 class="article__title">
Сергей Константинов. API
@ -118,8 +118,10 @@ h2 {
<li>Третья цифра (патч) увеличивается при выпуске новых версий, содержащих только исправление ошибок</li>
</ol>
<p>Выражения «мажорная версия API» и «версия API, содержащая обратно несовместимые изменения функциональности» тем самым следует считать эквивалентными.</p>
<h3 id="-5">Замечание о терминологии</h3>
<h3 id="-5">Условные обозначения и терминология</h3>
<p>Разработка программного обеспечения характеризуется, помимо прочего, существованием множества различных парадигм разработки, адепты которых зачастую настроены весьма воинственно по отношению к адептам других парадигм. Поэтому при написании этой книги мы намеренно избегаем слов «метод», «объект», «функция» и так далее, используя нейтральный термин «сущность». Под «сущностью» понимается некоторая атомарная единица функциональности — класс, метод, объект, монада, прототип (нужное подчеркнуть).</p>
<p>Для составных частей сущности, к сожалению, достаточно нейтрального термина нам придумать не удалось, поэтому мы используем слова «поля» и «методы».</p>
<p>Большинство примеров API в общих разделах будут даны в виде JSON-over-HTTP-эндпойтов. Это некоторая условность, которая помогает описать концепции, как нам кажется, максимально понятно. Вместо <code>GET /orders</code> вполне может быть вызов метода <code>orders.get()</code>, локальный или удалённый; вместо JSON может быть любой другой формат данных. Смысл утверждений от этого не меняется.</p>
<h2 id="iapi">I. Проектирование API</h2>
<h3 id="api-3">Пирамида контекстов API</h3>
<p>Подход, который мы используем для проектирования, состоит из четырёх шагов:</p>
@ -281,10 +283,10 @@ h2 {
<p>Обратите также внимание, что содержание операции «отменить заказ» изменяется на каждом из уровней. На пользовательском уровне заказ отменён, когда решены все важные для пользователя вопросы. То, что отменённый заказ какое-то время продолжает исполняться (например, ждёт утилизации) — пользователю неважно. На уровне исполнения же нужно связать оба контекста:</p>
<ul>
<li><code>GET /tasks/{id}/status</code><br>
<code>{"status":"canceled","operations":{"status":"canceling","items":[…]}</code><br>
<code>{"status":"canceled","operation_state":{"status":"canceling","operations":[…]}</code><br>
— с т.з. высокоуровневого кода задача завершена (<code>canceled</code>), но с точки зрения низкоуровневого кода список исполняемых операций непуст, т.е. задача продолжает работать.</li>
</ul>
<p>NB: так как <code>task</code> связывает два разных уровня абстракции, то и статусов у неё два: внешний <code>canceled</code> и внутренний <code>canceling</code>. Мы могли бы опустить второй статус и предложить ориентироваться на содержание <code>operations</code>, но это вновь (а) неявно, (б) предполагает необходимость разбираться в более низкоуровневом интерфейсе <code>operations</code>, что, быть может, разработчику вовсе и не нужно.</p>
<p>NB: так как <code>task</code> связывает два разных уровня абстракции, то и статусов у неё два: внешний <code>canceled</code> и внутренний <code>canceling</code>. Мы могли бы опустить второй статус и предложить ориентироваться на содержание <code>operations</code>, но это вновь (а) неявно, (б) предполагает необходимость разбираться в более низкоуровневом интерфейсе <code>operation_state</code>, что, быть может, разработчику вовсе и не нужно.</p>
<p>Может показаться, что соблюдение правила изоляции уровней абстракции является избыточным и заставляет усложнять интерфейс. И это в действительности так: важно понимать, что никакая гибкость, логичность, читабельность и расширяемость не бывает бесплатной. Можно построить API так, чтобы оно выполняло свою функцию с минимальными накладными расходами, по сути — дать интерфейс к микроконтроллерам кофе-машины. Однако пользоваться им будет крайне неудобно, и расширяемость такого API будет нулевой.</p>
<p>Дублирование функций на каждом уровне абстракций позволяет добиться важной вещи: возможности сменить нижележащие уровни без необходимости переписывать верхнеуровневый код. Мы можем добавить другие виды кофе-машин с принципиально другими физическими способами определения готовности напитка, и наш метод <code>GET /orders?order_id={id}</code> продолжит работать, как работал.</p>
<p>Да, код, который работал с физическим уровнем, придётся переписать. Но, во-первых, это неизбежно: изменение принципов работы физического уровня автоматически означает необходимость переписать код. Во-вторых, такое разделение ставит перед нами четкий вопрос: до какого момента API должно предоставлять публичный доступ? Стоило ли предоставлять пользователю методы физического уровня?</p>
@ -333,7 +335,59 @@ h2 {
<li>каждый объект в иерархии абстракций должен оперировать данными согласно своему уровню иерархии;</li>
<li>преобразованием данных имеют право заниматься только те объекты, в чьи непосредственные обязанности это входит.</li>
</ul>
<p>Дерево информационных контекстов (какой объект обладает какой информацией, и кто является транслятором из одного контекста в другой), по сути, представляет собой «срез» нашего дерева иерархии интерфейсов; выделение такого среза позволяет проще и удобнее удерживать в голове всю архитектуру проекта.</p></article></div>
<p>Дерево информационных контекстов (какой объект обладает какой информацией, и кто является транслятором из одного контекста в другой), по сути, представляет собой «срез» нашего дерева иерархии интерфейсов; выделение такого среза позволяет проще и удобнее удерживать в голове всю архитектуру проекта.</p>
<h3 id="-14">Описание конечных интерфейсов</h3>
<p>Определив все сущности, их ответственность и отношения друг с другом, мы переходим непосредственно к разработке API: нам осталось прописать номенклатуру всех объектов, полей, методов и функций в деталях. В этой главе мы дадим сугубо практические советы, как сделать API удобным и понятным.</p>
<p>Важное уточнение под номером ноль:</p>
<ol start="0">
<li>Правила — это всего лишь обобщения. Они не действуют безусловно и не означают, что можно не думать головой. У каждого правила есть какая-то рациональная причина его существования. Если в вашей ситуации нет причин следовать правилу — значит, следовать ему не надо.<ul>
<li>например, требование консистентности номенклатуры существует затем, чтобы разработчик тратил меньше времени на чтение документации; если вам <em>необходимо</em>, чтобы разработчик обязательно прочитал документацию по какому-то методу, вполне разумно сделать его сигнатуру нарочито неконсистентно;</li>
<li>важно понимать, что вы вольны вводить свои собственные конвенции. Например, в некоторых фреймворках сознательно отказываются от парных методов <code>set_entity</code> / <code>get_entity</code> в пользу одного метода <code>entity</code> с опциональным параметром. Важно только проявить последовательность в её применении — если такая конвенция вводится, то абсолютно все методы API должны иметь подобную полиморфную сигнатуру, или по крайней мере должен существовать принцип именования, отличающий такие комбинированные методы от обычных вызовов.</li></ul></li>
</ol>
<p>Это соображение применимо ко всем принципам ниже. Если из-за следования правилам у вас получается неудобное, громоздкое, неочевидное API — это повод пересмотреть правила (или API).</p>
<ol>
<li><p>Явное лучше неявного. Из названия любой сущности должно быть очевидно, что она делает и к каким сайд-эффектам может привести её использование.</p>
<ul>
<li>плохо: <code>GET /orders/cancellation</code> отменяет заказ<br>
— неочевидно, что достаточно просто обращения к сущности <code>cancellation</code> (что это?), тем более немодифицирующим методом <code>GET</code>, чтобы отменить заказ;<br>
хорошо: <code>POST /orders/cancellation</code>;</li>
<li>плохо: <code>GET /orders/statistics</code> агрегирует статистику заказов за всё время<br>
— даже если операция немодифицирующая, но вычислительно дорогая — следует об этом явно индицировать, особенно если вычислительные ресурсы тарифицируются для пользователя; тем более не стоит подбирать значения по умолчанию так, чтобы операция без параметров максимально расходовала ресурсы;<br>
хорошо: <code>POST /orders/statistics/aggregate</code> с обязательным указанием периода агрегации в запросе.</li></ul></li>
<li><p>Избегайте слов-«амёб» без определённой семантики, таких как get, apply, make. Сущности должны именоваться конкретно:</p>
<ul>
<li>плохо: <code>user.get()</code>
— неочевидно, что конкретно будет возвращено;<br>
хорошо: user.<code>get_id()</code>;</li></ul></li>
<li><p>Не экономьте буквы. В XXI веке давно уже нет нужды называть переменные покороче:</p>
<ul>
<li>плохо: <code>order.time()</code><br>
— неясно, о каком времени идёт речь: время создания заказа, время готовности заказа, время ожидания заказа?…<br>
хорошо: <code>order.get_estimated_delivery_time()</code></li>
<li>плохо: <code>strpbrk</code> ищет вхождение любого из списка символов в строке<br>
— возможно, автору этого API казалось, что аббревиатура <code>pbrk</code> что-то значит для читателя, но он явно ошибся;<br>
хорошо: <code>string_search_for_characters</code>; однако необходимость существования такого метода вообще вызывает сомнения, достаточно было бы иметь удобную функцию поиска подстроки с нужными параметрами.</li></ul></li>
<li><p>Сущности-массивы должны именоваться во множественном числе или собирательными выражениями — <code>objects</code>, <code>children</code>; если это невозможно (термин неисчисляемый), следует добавить префикс или постфикс, не оставляющий сомнений:</p>
<ul>
<li>плохо: <code>GET /news</code>
— неясно, будет ли получена какая-то конкретная новость или массив новостей;
хорошо: <code>GET /news-list</code>.</li></ul></li>
<li><p>Аналогично, если ожидается булево значение, то из названия это должно быть очевидно, т.е. именование должно описывать некоторое качественное состояние, например, <code>is_ready</code>, <code>open_now</code>:</p>
<ul>
<li>плохо: <code>"task.status": true</code>
— неочевидно, что статус бинарен, плюс такое API будет нерасширяемым
хорошо: <code>"task.is_finished": true</code></li></ul></li>
<li><p>Сущности, выполняющие подобные функции, должны называться подобно и вести себя подобным образом.</p>
<ul>
<li>плохо: <code>begin_transition</code> / <code>stop_transition</code><br>
<code>begin</code> и <code>stop</code> — непарные термины; разработчик будет вынужден рыться в документации;<br>
хорошо: <code>begin_transition</code> / <code>end_transition</code> либо <code>start_transition</code> / <code>stop_transition</code>;</li>
<li>плохо:<br>
<code>strpos(haystack, needle)</code> — ищет позицию строки <code>needle</code> внутри строки <code>haystack</code>;<br>
<code>str_replace(needle, replace, haystack)</code> — заменяет вхождения строки <code>needle</code> внутри строки <code>haystack</code> на строку <code>replace</code>;<br>
— здесь нарушены сразу несколько правил: написание неконсистентно в части знака подчеркивания; близкие по смыслу методы имеют разный порядок аргументов <code>needle</code>/<code>haystack</code>; наконец, один из методов находит первое вхождение, а другой — все вхождения, и это никак не отражено в именовании.</li></ul></li>
</ol>
<p>// TODO: единицы измерения, различные стандарты, консистентность</p></article></div>
</div>
</article>

BIN
dist/API.ru.pdf vendored

Binary file not shown.