mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-01-23 17:53:04 +02:00
WIP: глава разделение уровней абстракции перерабатывается
This commit is contained in:
parent
272d792aaf
commit
3f78ca456d
3
build.js
3
build.js
@ -42,7 +42,8 @@ function getParts ({ path, l10n: { chapter }, pageBreak}) {
|
||||
const md = fs.readFileSync(`${subdir}${file}`, 'utf-8');
|
||||
parts.push(
|
||||
mdHtml.makeHtml(
|
||||
md.trim().replace(/^### /, `### ${chapter} ${counter++}. `)
|
||||
md.trim()
|
||||
.replace(/^### /, `### ${chapter} ${counter++}. `)
|
||||
) + pageBreak
|
||||
);
|
||||
});
|
||||
|
272
docs/API.ru.html
272
docs/API.ru.html
@ -27,11 +27,21 @@
|
||||
padding-left: 92px;
|
||||
}
|
||||
|
||||
code {
|
||||
code, pre {
|
||||
font-family: Inconsolata, sans-serif;
|
||||
font-size: 12pt;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 12pt 0;
|
||||
padding: 12pt;
|
||||
border-radius: .25em;
|
||||
border-top: 1px solid rgba(0,0,0,.45);
|
||||
border-left: 1px solid rgba(0,0,0,.45);
|
||||
box-shadow: .1em .1em .1em rgba(0,0,0,.45);
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
.page-break {
|
||||
page-break-after: always;
|
||||
}
|
||||
@ -106,14 +116,14 @@ h4, h5 {
|
||||
<li>API должно быть консистентно: при разработке новой функциональности, т.е. при обращении к каким-то незнакомым сущностям в API, разработчик может действовать по аналогии с уже известными ему концепциями API, и его код будет работать.</li>
|
||||
</ul>
|
||||
<p>Однако статическое удобство и понятность API — это простая часть. В конце концов, никто не стремится специально сделать API нелогичным и нечитаемым — всегда при разработке мы начинаем с каких-то понятных базовых концепций. При минимальном опыте проектирования сложно сделать ядро API, не удовлетворяющее критериям очевидности, читаемости и консистентности.</p>
|
||||
<p>Проблемы начинаются, когда мы начинаем API развивать. Добавление новой фунциональности рано или поздно приводит к тому, что некогда простое и понятное API становится наслоением разных концепций, а попытки сохранить обратную совместимость приводят к нелогичным, неочевидным и попросту плохим решениям. Отчасти это связано так же и с тем, что невозможно обладать полным знанием о будущем: ваше понимание о «правильном» 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 id="4">Глава 4. Обратная совместимость</h3>
|
||||
<p>Обратная совместимость — это некоторая <em>временна́я</em> характеристика качества вашего API. Именно необходимость поддержания обратной совместимости отличает разработку API от разработки программного обеспечения вообще.</p>
|
||||
<p>Разумеется, обратная совместимость не абсолютна. В некоторых предметных областях выпуск новых обратно несовместимых версий API является вполне рутинной процедурой. Тем не менее, каждый раз, когда выпускается новая обратно несовместимая версия API, всем разработчикам приходится инвестировать какое-то ненулевое количество усилий, чтобы адаптировать свой код к новой версии. В этом плане выпуск новых версий API является некоторого рода «налогом» на потребителей — им нужно тратить вполне осязаемые деньги только для того, чтобы их продукт продолжал работать.</p>
|
||||
<p>Конечно, крупные компании с прочным положением на рынке могут позволить себе такой налог взымать. Более того, они могут вводить какие-то санкции за отказ от перехода на новые версии API, вплоть до отключения приложений.</p>
|
||||
<p>С нашей точки зрения, подобное поведение ничем не может быть оправдано. Избегайте скрытых налогов на своих пользователей. Если вы можете не ломать обратную совсемстимость — не ломайте её.</p>
|
||||
<p>С нашей точки зрения, подобное поведение ничем не может быть оправдано. Избегайте скрытых налогов на своих пользователей. Если вы можете не ломать обратную совместимость — не ломайте её.</p>
|
||||
<p>Да, безусловно, поддержка старых версий API — это тоже своего рода налог. Технологии меняются, и, как бы хорошо ни было спроектировано ваше API, всего предусмотреть невозможно. В какой-то момент ценой поддержки старых версий становится невозможность предоставлять новую функциональность и поддерживать новые платформы, и выпустить новую версию всё равно придётся. Однако вы по крайней мере сможете убедить своих потребителей в необходимости перехода.</p>
|
||||
<p>Более подробно о политиках версионирования будет рассказано в разделе II.</p><div class="page-break"></div><h3 id="5">Глава 5. О версионировании</h3>
|
||||
<p>Более подробно о жизненном цикле API и политиках выпуска новых версий будет рассказано в разделе II.</p><div class="page-break"></div><h3 id="5">Глава 5. О версионировании</h3>
|
||||
<p>Здесь и далее мы будем придерживаться принципов версионирования <a href="https://semver.org/">semver</a>:</p>
|
||||
<ol>
|
||||
<li>Версия API задаётся тремя цифрами, вида <code>1.2.3</code></li>
|
||||
@ -121,10 +131,11 @@ h4, h5 {
|
||||
<li>Вторая цифра (минорная версия) увеличивается при добавлении новой функциональности с сохранением обратной совместимости</li>
|
||||
<li>Третья цифра (патч) увеличивается при выпуске новых версий, содержащих только исправление ошибок</li>
|
||||
</ol>
|
||||
<p>Выражения «мажорная версия API» и «версия API, содержащая обратно несовместимые изменения функциональности» тем самым следует считать эквивалентными.</p><div class="page-break"></div><h3 id="6">Глава 6. Условные обозначения и терминология</h3>
|
||||
<p>Выражения «мажорная версия API» и «версия API, содержащая обратно несовместимые изменения функциональности» тем самым следует считать эквивалентными.</p>
|
||||
<p>Более подробно о политиках версионирования будет рассказано в разделе II. В разделе I мы ограничимся лишь указанием версии API в формате <code>v1</code>, <code>v2</code>, etc.</p><div class="page-break"></div><h3 id="6">Глава 6. Условные обозначения и терминология</h3>
|
||||
<p>Разработка программного обеспечения характеризуется, помимо прочего, существованием множества различных парадигм разработки, адепты которых зачастую настроены весьма воинственно по отношению к адептам других парадигм. Поэтому при написании этой книги мы намеренно избегаем слов «метод», «объект», «функция» и так далее, используя нейтральный термин «сущность». Под «сущностью» понимается некоторая атомарная единица функциональности — класс, метод, объект, монада, прототип (нужное подчеркнуть).</p>
|
||||
<p>Для составных частей сущности, к сожалению, достаточно нейтрального термина нам придумать не удалось, поэтому мы используем слова «поля» и «методы».</p>
|
||||
<p>Большинство примеров API в общих разделах будут даны в виде JSON-over-HTTP-эндпойтов. Это некоторая условность, которая помогает описать концепции, как нам кажется, максимально понятно. Вместо <code>GET /orders</code> вполне может быть вызов метода <code>orders.get()</code>, локальный или удалённый; вместо JSON может быть любой другой формат данных. Смысл утверждений от этого не меняется.</p>
|
||||
<p>Большинство примеров API в общих разделах будут даны в виде JSON-over-HTTP-эндпойтов. Это некоторая условность, которая помогает описать концепции, как нам кажется, максимально понятно. Вместо <code>GET /v1/orders</code> вполне может быть вызов метода <code>orders.get()</code>, локальный или удалённый; вместо JSON может быть любой другой формат данных. Смысл утверждений от этого не меняется.</p>
|
||||
<p>Также в примерах часто применяется следующая конвенция. Запись <code>{ "begin_date" }</code> (т.е. отсутствие значения у поля в JSON-объекте) означает, что в поле находится именно то, что ожидается — т.е. в данном примере какая-то дата начала.</p><div class="page-break"></div><h2>I. Проектирование API</h2><h3 id="7api">Глава 7. Пирамида контекстов API</h3>
|
||||
<p>Подход, который мы используем для проектирования, состоит из четырёх шагов:</p>
|
||||
<ul>
|
||||
@ -134,14 +145,14 @@ h4, h5 {
|
||||
<li>описание конечных интерфейсов.</li>
|
||||
</ul>
|
||||
<p>Этот алгоритм строит API сверху вниз, от общих требований и сценариев использования до конкретной номенклатуры сущностей; фактически, двигаясь этим путем, вы получите на выходе готовое API — чем этот подход и ценен.</p>
|
||||
<p>Может показаться, что наиболее полезные советы и best practice приведены в последнем разделе, однако это не так; цена ошибки, допущенной на разных уровнях весьма различна. Если исправить плохое именование довольно просто, то исправить неверное понимание того, зачем вообще нужно API, практически невозможно.</p>
|
||||
<p>Может показаться, что наиболее полезные советы приведены в последнем разделе, однако это не так; цена ошибки, допущенной на разных уровнях весьма различна. Если исправить плохое именование довольно просто, то исправить неверное понимание того, зачем вообще нужно API, практически невозможно.</p>
|
||||
<p><em>NB</em>. Здесь и далее мы будем рассматривать концепции разработки API на примере некоторого гипотетического API заказа кофе в городских кофейнях. На всякий случай сразу уточним, что пример является синтетическим; в реальной ситуации, если бы такое API пришлось проектировать, оно вероятно было бы совсем не похоже на наш выдуманный пример.</p><div class="page-break"></div><h3 id="8">Глава 8. Определение области применения</h3>
|
||||
<p>Ключевой вопрос, который вы должны задать себе четыре раза, выглядит так: какую проблему мы решаем? Задать его следует четыре раза с ударением на каждом из четырёх слов.</p>
|
||||
<ol>
|
||||
<li><p><em>Какую</em> проблему мы решаем? Можем ли мы чётко описать, в какой ситуации гипотетическим потребителям-разработчикам нужно наше API?</p></li>
|
||||
<li><p>Какую <em>проблему</em> мы решаем? А мы правда уверены, что описанная выше ситуация — проблема? Действительно ли кто-то готов платить (в прямом и переносном смысле) за то, что ситуация будет как-то автоматизирована?</p></li>
|
||||
<li><p>Какую проблему <em>мы</em> решаем? Действительно ли решение этой проблемы находится в нашей компетенции? Действительно ли мы находимся в той позиции, чтобы решить эту проблему?</p></li>
|
||||
<li><p>Какую проблему мы <em>решаем</em>? Правда ли, что решение, которое мы предлагаем, действильно решает проблему? Не создаём ли мы на её месте другую проблему, более сложную?</p></li>
|
||||
<li><p>Какую проблему мы <em>решаем</em>? Правда ли, что решение, которое мы предлагаем, действительно решает проблему? Не создаём ли мы на её месте другую проблему, более сложную?</p></li>
|
||||
</ol>
|
||||
<p>Итак, предположим, что мы хотим предоставить API автоматического заказа кофе в городских кофейнях. Попробуем применить к ней этот принцип.</p>
|
||||
<ol>
|
||||
@ -149,14 +160,14 @@ h4, h5 {
|
||||
<ul>
|
||||
<li>Возможно, мы хотим решить проблему выбора и знания? Чтобы человек наиболее полно знал о доступных ему здесь и сейчас опциях.</li>
|
||||
<li>Возможно, мы оптимизируем время ожидания? Чтобы человеку не пришлось ждать, пока его заказ готовится.</li>
|
||||
<li>Возможно, мы хотим минимизировать ошибки? Чтобы человек получил именно то, что хотел заказть, не потеряв информацию при разговорном общении либо при настройке незнакомого интерфейса кофе-машины.</li></ul>
|
||||
<li>Возможно, мы хотим минимизировать ошибки? Чтобы человек получил именно то, что хотел заказать, не потеряв информацию при разговорном общении либо при настройке незнакомого интерфейса кофе-машины.</li></ul>
|
||||
<p>Вопрос «зачем» — самый важный из тех вопросов, которые вы должны задавать себе. Не только глобально в отношении целей всего проекта, но и локально в отношении каждого кусочка функциональности. <strong>Если вы не можете коротко и понятно ответить на вопрос «зачем эта сущность нужна» — значит, она не нужна</strong>.</p>
|
||||
<p>Здесь и далее предположим (в целях придания нашему примеру глубины и некоторой упоротости), что мы оптимизируем все три фактора в порядке убывания важности.</p></li>
|
||||
<li><p>Правда ли решаемая проблема существует? Дейсвительно ли мы наблюдаем неравномерную загрузку кофейных автоматов по утрам? Правда ли люди страдают от того, что не могут найти поблизости нужный им латте с ореховым сиропом? Действительно ли людям важны те минуты, которые они теряют, стоя в очередях?</p></li>
|
||||
<li><p>Действительно ли мы обладаем достаточным ресурсом, чтобы решить эту проблему? Есть ли у нас доступ к достаточному количеству кофемашин и клиентов, чтобы обеспечить работоспособность системы?</p></li>
|
||||
<li><p>Правда ли решаемая проблема существует? Действительно ли мы наблюдаем неравномерную загрузку кофейных автоматов по утрам? Правда ли люди страдают от того, что не могут найти поблизости нужный им латте с ореховым сиропом? Действительно ли людям важны те минуты, которые они теряют, стоя в очередях?</p></li>
|
||||
<li><p>Действительно ли мы обладаем достаточным ресурсом, чтобы решить эту проблему? Есть ли у нас доступ к достаточному количеству кофе-машин и клиентов, чтобы обеспечить работоспособность системы?</p></li>
|
||||
<li><p>Наконец, правда ли мы решим проблему? Как мы поймём, что оптимизировали перечисленные факторы?</p></li>
|
||||
</ol>
|
||||
<p>На все эти вопросы, в общем случае, простого ответа нет. В идеале ответы на эти вопросы должны даваться с цифрами в руках. Сколько конкретно времени тратится неоптимально, и какого значения мы рассчитываем добиться, располагая какой плотностью кофемашин? Заметим также, что в реальной жизни просчитать такого рода цифры можно в основном для проектов, которые пытаются влезть на уже устоявшийся рынок; если вы пытаетесь сделать что-то новое, то, вероятно, вам придётся ориентироваться в основном на свою интуицию.</p>
|
||||
<p>На все эти вопросы, в общем случае, простого ответа нет. В идеале ответы на эти вопросы должны даваться с цифрами в руках. Сколько конкретно времени тратится неоптимально, и какого значения мы рассчитываем добиться, располагая какой плотностью кофе-машин? Заметим также, что в реальной жизни просчитать такого рода цифры можно в основном для проектов, которые пытаются влезть на уже устоявшийся рынок; если вы пытаетесь сделать что-то новое, то, вероятно, вам придётся ориентироваться в основном на свою интуицию.</p>
|
||||
<h4 id="api">Почему API?</h4>
|
||||
<p>Т.к. наша книга посвящена не просто разработке программного обеспечения, а разработке API, то на все эти вопросы мы должны взглянуть под другим ракурсом: а почему для решения этих задач требуется именно API, а не просто программное обеспечение? В нашем вымышленном примере мы должны спросить себя: зачем нам нужно предоставлять сервис для других разработчиков, чтобы они могли готовить кофе своим клиентам, а не сделать своё приложение для конечного потребителя?</p>
|
||||
<p>Иными словами, должна иметься веская причина, по которой два домена разработки ПО должны быть разделены: есть оператор(ы), предоставляющий API; есть оператор(ы), предоставляющий сервисы пользователям. Их интересы в чем-то различны настолько, что объединение этих двух ролей в одном лице нежелательно. Более подробно мы изложим причины и мотивации делать именно API в разделе II.</p>
|
||||
@ -168,17 +179,17 @@ h4, h5 {
|
||||
<li>Что конкретно мы делаем</li>
|
||||
<li>Как мы это делаем</li>
|
||||
</ol>
|
||||
<p>В случае нашего кофепримера мы:</p>
|
||||
<p>В случае нашего кофе-примера мы:</p>
|
||||
<ol>
|
||||
<li>Предоставляем сервисам с большой пользовательской аудиторией API для того, чтобы их потребители могли максимально удобно для себя заказать кофе.</li>
|
||||
<li>Для этого мы абстрагируем за нашим HTTP API доступ к «железу» и предоставим методы для выбора вида напитка и места его приготовления и для непосредственно исполнения заказа.</li>
|
||||
</ol><div class="page-break"></div><h3 id="9">Глава 9. Разделение уровней абстракции</h3>
|
||||
<p>«Разделите свой код на уровни абстракции» - пожалуй, самый общий совет для разработчиков программного обеспечения. Однако будет вовсе не преувеличением сказать, что изоляция уровней абстрации — самая сложная задача, стоящая перед разработчиком API.</p>
|
||||
<p>«Разделите свой код на уровни абстракции» - пожалуй, самый общий совет для разработчиков программного обеспечения. Однако будет вовсе не преувеличением сказать, что изоляция уровней абстракции — самая сложная задача, стоящая перед разработчиком API.</p>
|
||||
<p>Прежде чем переходить к теории, следует чётко сформулировать, <em>зачем</em> нужны уровни абстракции и каких целей мы хотим достичь их выделением.</p>
|
||||
<p>Вспомним, что программный продукт - это средство связи контекстов, средство преобразования терминов и операций одной предметной области в другую. Чем дальше друг от друга эти области отстоят - тем большее число промежуточных передаточных звеньев нам придётся ввести. Вернёмся к нашему примеру с кофейнями. Какие уровни сущностей мы видим?</p>
|
||||
<ol>
|
||||
<li>Непосредственно состояние кофе-машины и шаги приготовления кофе. Температура, давление, объём воды.</li>
|
||||
<li>У кофе есть мета-характерстики: сорт, вкус, вид напитка.</li>
|
||||
<li>У кофе есть мета-характеристики: сорт, вкус, вид напитка.</li>
|
||||
<li>Мы готовим с помощью нашего API <em>заказ</em> — один или несколько стаканов кофе с определенной стоимостью.</li>
|
||||
<li>Наши кофе-машины как-то распределены в пространстве (и времени).</li>
|
||||
<li>Кофе-машина принадлежит какой-то сети кофеен, каждая из которых обладает какой-то айдентикой и специальными возможностями.</li>
|
||||
@ -191,12 +202,12 @@ h4, h5 {
|
||||
</ol>
|
||||
<p>Допустим, мы имеем следующий интерфейс:</p>
|
||||
<ul>
|
||||
<li><code>GET /recipes/lungo</code><br />
|
||||
<li><code>GET /v1/recipes/lungo</code><br />
|
||||
— возвращает рецепт лунго;</li>
|
||||
<li><code>POST /coffee-machines/orders?machine_id={id}</code><br />
|
||||
<li><code>POST /v1/coffee-machines/orders?machine_id={id}</code><br />
|
||||
<code>{recipe:"lungo"}</code><br />
|
||||
— размещает на указанной кофе-машине заказ на приготовление лунго и возвращает идентификатор заказа;</li>
|
||||
<li><code>GET /orders?order_id={id}</code><br />
|
||||
<li><code>GET /v1/orders?order_id={id}</code><br />
|
||||
— возвращает состояние заказа;</li>
|
||||
</ul>
|
||||
<p>И зададимся вопросом, каким образом разработчик определит, что заказ клиента готов. Допустим, мы сделаем так: добавим в рецепт лунго эталонный объём, а в состояние заказа — количество уже налитого кофе. Тогда разработчику нужно будет проверить совпадение этих двух цифр, чтобы убедиться, что кофе готов.</p>
|
||||
@ -207,17 +218,16 @@ h4, h5 {
|
||||
<ul>
|
||||
<li>или мы фиксируем список допустимых объёмов и заводим фиктивные рецепты типа <code>/recipes/small-lungo</code>, <code>recipes/large-lungo</code>. Почему фиктивные? Потому что рецепт один и тот же, меняется только объём. Нам придётся либо тиражировать одинаковые рецепты, отличающиеся только объёмом, либо вводить какое-то «наследование» рецептов, чтобы можно было указать базовый рецепт и только переопределить объём;</li>
|
||||
<li>или мы модифицируем интерфейс, объявляя объём кофе, указанный в рецепте, значением по умолчанию; при размещении заказа мы разрешаем указать объём, отличный от эталонного:<br />
|
||||
<code>POST /coffee-machines/orders?machine_id={id}</code><br />
|
||||
<code>POST /v1/coffee-machines/orders?machine_id={id}</code><br />
|
||||
<code>{recipe:"lungo","volume":"800ml"}</code><br />
|
||||
Для таких кофе произвольного объёма нужно будет получать требуемый объём не из <code>GET /recipes</code>, а из <code>GET /orders</code>. Сделав так, мы сразу получаем клубок из связанных проблем:</li>
|
||||
<li>разработчик, которому придётся поддержать эту функциональность, имеет высокие шансы сделать ошибку: добавив поддержку произвольного объёма кофе в код, работающий с <code>POST /coffee-machines/orders</code> нужно не забыть переписать код проверки готовности заказа;</li>
|
||||
<li>мы получим классическую ситуацию, когда одно и то же поле (объём кофе) значит разные вещи в разных интерфейсах. В <code>GET /recipes</code> поле «объём» теперь значит «объём, который будет запрошен, если не передать его явно в <code>POST /coffee-machines/orders</code>»; переименовать его в «объём по умолчанию» уже не получиться, с этой проблемой теперь придётся жить.</li></ul></li>
|
||||
Для таких кофе произвольного объёма нужно будет получать требуемый объём не из <code>GET /v1/recipes</code>, а из <code>GET /v1/orders</code>. Сделав так, мы сразу получаем клубок из связанных проблем:</li>
|
||||
<li>разработчик, которому придётся поддержать эту функциональность, имеет высокие шансы сделать ошибку: добавив поддержку произвольного объёма кофе в код, работающий с <code>POST /v1/coffee-machines/orders</code> нужно не забыть переписать код проверки готовности заказа;</li>
|
||||
<li>мы получим классическую ситуацию, когда одно и то же поле (объём кофе) значит разные вещи в разных интерфейсах. В <code>GET /v1/recipes</code> поле «объём» теперь значит «объём, который будет запрошен, если не передать его явно в <code>POST /v1/coffee-machines/orders</code>»; переименовать его в «объём по умолчанию» уже не получится, с этой проблемой теперь придётся жить.</li></ul></li>
|
||||
<li><p>Вся эта схема полностью неработоспособна, если разные модели кофе-машин производят лунго разного объёма. Для решения задачи «объём лунго зависит от вида машины» нам придётся сделать совсем неприятную вещь: сделать рецепт зависимым от id машины. Тем самым мы начнём активно смешивать уровни абстракции: одной частью нашего API (рецептов) станет невозможно пользоваться без другой части (информации о кофе-машинах). Что немаловажно, от разработчиков потребуется изменить логику своего приложения: если раньше они могли предлагать сначала выбрать объём, а потом кофе-машину, то теперь им придётся полностью изменить этот шаг.</p></li>
|
||||
</ol>
|
||||
<p>Хорошо, допустим, мы поняли, как сделать плохо. Но как же тогда сделать <em>хорошо</em>? Разделение уровней абстракции должно происходить вдоль трёх направлений:</p>
|
||||
<ol>
|
||||
<li><p>От сценариев использования к их внутренней реализации: высокоуровневые сущности и номенклатура их методов должны напрямую отражать сценарии использования API; низкоуровневый - отражать декомпозицию сценариев на составные части.</p>
|
||||
<p>Здесь мы должны явно обратиться к выписанному нами ранее «что» и «как». В идеальном мире высший уровень абстракции вашего API должен быть просто переводом записанной человекочитаемой фразы на машинный язык. Если нужно узнать, готов ли заказ — значит, должен быть метод <code>is-order-ready</code> (если мы считаем эту операцию действительно важной и частотной) или хотя бы <code>GET /orders/{id}/status</code> для того, чтобы явно узнать статус заказа. Эту логику требуется прорастить вниз до самых мелких и частных сценариев типа определения температуры напитка или наличия у исполнителя картонного держателя нужного размера.</p></li>
|
||||
<li><p>От сценариев использования к их внутренней реализации: высокоуровневые сущности и номенклатура их методов должны напрямую отражать сценарии использования API; низкоуровневый - отражать декомпозицию сценариев на составные части.</p></li>
|
||||
<li><p>От терминов предметной области пользователя к терминам предметной области исходных данных — в нашем случае от высокоуровневых понятий «рецепт», «заказ», «бренд», «кофейня» к низкоуровневым «температура напитка» и «координаты кофе-машины»</p></li>
|
||||
<li><p>Наконец, от структур данных, в которых удобно оперировать пользователю к структурам данных, максимально приближенных к «сырым» - в нашем случае от «лунго» и «сети кофеен "Ромашка"» - к сырым байтовый данным, описывающим состояние кофе-машины марки «Доброе утро» в процессе приготовления напитка.</p></li>
|
||||
</ol>
|
||||
@ -227,43 +237,141 @@ h4, h5 {
|
||||
<li>с одной стороны, «заказ» не должен содержать информацию о датчиках и сенсорах кофе-машины;</li>
|
||||
<li>с другой стороны, кофе-машина не должна хранить информацию о свойствах заказа (да и вероятно её API такой возможности и не предоставляет).</li>
|
||||
</ul>
|
||||
<p>Введём промежуточный уровень: нам нужно звено, которое одновременно знает о заказе, рецепте и кофе-машине. Назовём его «уровнем исполнения»: его ответственностью является интерпретация заказа, превращение его в набор команд кофе-машине. Самый простой вариант — ввести абстрактную сущность «задание» <code>task</code>:</p>
|
||||
<p>Наивный подход в такой ситуации — искусственно ввести некий промежуточный уровень абстракции, «передаточное звено», который переформулирует задачи одного уровня абстракции в другой. Например, введём сущность <code>task</code> вида:</p>
|
||||
<pre><code>{
|
||||
…
|
||||
"volume_requested": "800ml",
|
||||
"volume_prepared": "200ml",
|
||||
"readiness_policy": "check_volume",
|
||||
"ready": false,
|
||||
"operation_state": {
|
||||
"status": "executing",
|
||||
"operations": [
|
||||
// описание операций, запущенных на
|
||||
// физической кофе-машине
|
||||
]
|
||||
}
|
||||
…
|
||||
}
|
||||
</code></pre>
|
||||
<p>Я называю этот подход «наивным» не потому, что он неправильный; напротив, это вполне логичное решение «по умолчанию», если вы на данном этапе ещё не знаете или не понимаете, как будет выглядеть ваше API. Проблема его в том, что он умозрительный: он не добавляет понимания того, как устроена предметная область.</p>
|
||||
<p>Хороший разработчик в нашем примере должен спросить: хорошо, а какие вообще говоря существуют варианты? Как можно определять готовность напитка? Если вдруг окажется, что сравнение объёмов — единственный способ определения готовности во всех без исключения кофе-машинах, то почти все рассуждения выше — неверны: можно совершенно спокойно включать в интерфейсы определение готовности кофе по объёму, т.к. никакого другого и не существует. Прежде, чем что-то абстрагировать — надо представлять, <em>что</em> мы, собственно, абстрагируем.</p>
|
||||
<p>Для нашего примера допустим, что мы сели изучать спецификации API кофе-машин и выяснили, что существует принципиально два класса устройств:</p>
|
||||
<ul>
|
||||
<li>заказ порождает одно или несколько заданий, указывая для задания конкретный рецепт и кофе-машину;</li>
|
||||
<li>задание в свою очередь оперирует командами кофе-машины и отвечает за интерпретацию состояния датчиков.</li>
|
||||
<li>кофе-машины с предустановленными программами, которые умеют готовить заранее прошитые N видов напитков, и мы можем управлять только какими-то параметрами напитка (скажем, объёмом напитка, вкусом сиропа и видом молока); у таких машин отсутствует доступ к внутренним функциям и датчикам, но зато машина умеет через API сама отдавать статус приготовления напитка;</li>
|
||||
<li>кофе-машины с предустановленными функциями типа «смолоть такой-то объём кофе», «пролить N миллилитров воды», «взбить молочную пену» и т.д.: у таких машин отсутствует понятие «программа приготовления», но есть доступ к микрокомандам и датчикам.</li>
|
||||
</ul>
|
||||
<p>Таким образом, наше API будет выглядеть примерно так:</p>
|
||||
<p>Предположим, для большей конкретности, что эти два класса устройств поставляются вот с таким физическим API:</p>
|
||||
<ul>
|
||||
<li><code>POST /orders</code> — создаёт заказ;</li>
|
||||
<li><code>GET /tasks?order_id={order_id}</code> — позволяет получить список заданий по заказу.</li>
|
||||
<li><p>Машины с предустановленными программами:</p>
|
||||
<pre><code>GET /programs
|
||||
// Возвращает список предустановленных программ
|
||||
{
|
||||
// идентификатор программы
|
||||
"program": "01",
|
||||
// вид кофе
|
||||
"type": "lungo"
|
||||
}
|
||||
</code></pre>
|
||||
<pre><code>POST /execute
|
||||
{
|
||||
"program": 1,
|
||||
"volume": "200ml"
|
||||
}
|
||||
// Запускает указанную программу на исполнение
|
||||
// и возвращает статус исполнения
|
||||
{
|
||||
// Уникальный идентификатор задания
|
||||
"execution_id": "01-01",
|
||||
// Идентификатор исполняемой программы
|
||||
"program": 1,
|
||||
// Запрошенный объём напитка
|
||||
"volume": "200ml",
|
||||
// Ожидаемое время приготовления
|
||||
"preparation_time": "20s",
|
||||
// Готовность
|
||||
"ready": false
|
||||
}
|
||||
</code></pre>
|
||||
<pre><code>POST /cancel
|
||||
// Отменяет текущую программу
|
||||
</code></pre>
|
||||
<pre><code>GET /execution/{execution_id}/status
|
||||
// Возвращает статус исполнения
|
||||
// Формат аналогичен формату ответа `POST /execute`
|
||||
</code></pre>
|
||||
<p><em>NB</em>. На всякий случай отметим, что данное API нарушает множество описанных нами принципов проектирования, начиная с отсутствия версионирования; оно приведено в таком виде по двум причинам: (1) чтобы мы могли показать, как спроектировать API более удачно; (2) скорее всего, в реальной жизни вы получите именно такое API от производителей кофе-машин, и это ещё довольно вменяемый вариант.</p></li>
|
||||
<li><p>Машины с предустановленными функциями:</p>
|
||||
<pre><code>GET /functions
|
||||
// Возвращает список доступных функций
|
||||
{
|
||||
"functions": [
|
||||
{
|
||||
// Тип операции
|
||||
// * set_cup — поставить стакан
|
||||
// * grind_coffee — смолоть кофе
|
||||
// * shed_water — пролить воду
|
||||
// * discard_cup — утилизировать стакан
|
||||
"type": "set_cup",
|
||||
// Допустимые аргументы для каждой операции
|
||||
// Для простоты ограничимся одним аргументом:
|
||||
// * volume — объём стакана, кофе или воды
|
||||
"arguments": ["volume"]
|
||||
},
|
||||
…
|
||||
]
|
||||
}
|
||||
</code></pre>
|
||||
<pre><code>POST /functions
|
||||
{
|
||||
"type": "set_cup",
|
||||
"arguments": [{ "name": "volume", "value": "300ml" }]
|
||||
}
|
||||
// Запускает на исполнение функцию
|
||||
// с передачей указанных значений аргументов
|
||||
</code></pre>
|
||||
<pre><code>GET /sensors
|
||||
// Возвращает статусы датчиков
|
||||
{
|
||||
"sensors": [
|
||||
{
|
||||
// Допустимые значения
|
||||
// * cup_volume — объём установленного стакана
|
||||
// * ground_coffee_volume — объём смолотого кофе
|
||||
// * cup_filled_volume — объём напитка в стакане
|
||||
"type": "cup_volume",
|
||||
"value": "200ml"
|
||||
},
|
||||
…
|
||||
]
|
||||
}
|
||||
</code></pre>
|
||||
<p><em>NB</em>. Пример нарочно сделан умозрительным для моделирования ситуации, описанной в начале главы: для определения готовности напитка нужно сличить объём налитого с эталоном.</p></li>
|
||||
</ul>
|
||||
<p>Внимательный читатель может здесь поинтересоваться, а в чём, собственно разница по сравнению с наивным подходом? Напомню, мы рассмотрели выше примерно такой вариант:</p>
|
||||
<p>Теперь картина становится более явной: нам нужно абстрагировать работу с кофе-машиной так, чтобы наш «уровень исполнения» в API предоставлял общие функции (такие, как определение готовности напитка) в унифицированном виде. Важно отметить, что с точки зрения разделения абстракций два этих вида кофе-машин сами находятся на разных уровнях: первые предоставляют API более высокого уровня, нежели вторые; следовательно, и «ветка» нашего API, работающая со вторым видом машин, будет более «развесистой».</p>
|
||||
<p>Следующий шаг, необходимый для отделения уровней абстракции — необходимо понять, какую функциональность нам, собственно, необходимо абстрагировать. Для этого нам необходимо обратиться к задачам, которые решает разработчик на уровне работы с заказами, и понять, какие проблемы у него возникнут в случае отсутствия нашего слоя абстракции.</p>
|
||||
<ol>
|
||||
<li>Очевидно, что разработчику хочется создавать заказ унифицированным образом — перечислить высокоуровневые параметры заказа (вид напитка, объём и специальные требования, такие как вид сиропа или молока) — и не думать о том, как на конкретной машине исполнить этот заказ.</li>
|
||||
<li>Разработчику надо понимать состояние исполнения — готов ли заказ или нет; если не готов — когда ожидать готовность (и надо ли её ожидать вообще в случае ошибки исполнения).</li>
|
||||
<li>Разработчику нужно уметь соотносить заказ с его положением в пространстве и времени — чтобы показать потребителю, когда и как нужно заказ забрать.</li>
|
||||
<li>Наконец, разработчику нужно выполнять атомарные операции — прежде всего, отменять заказ.</li>
|
||||
</ol>
|
||||
<p>Таким образом, наш промежуточный уровень абстракции должен:</p>
|
||||
<ul>
|
||||
<li><code>POST /coffee-machines/orders?machine_id={id}</code><br />
|
||||
<code>{recipe:"lungo","volume":"800ml"}</code><br />
|
||||
— создаёт заказ указанного объёма</li>
|
||||
<li><code>GET /orders/{id}</code><br />
|
||||
<code>{…"volume_requested":"800ml","volume_prepared":"120ml"…}</code><br />
|
||||
— состояние исполнения заказа (налито 120 мл из запрошенных 800).</li>
|
||||
<li>скрывать параметры создания задания на приготовление напитка — их вычисление должно производиться в нашем коде;</li>
|
||||
<li>обобщать состояние исполнения в единый набор статусов, не зависящий от физического уровня приготовления напитка;</li>
|
||||
<li>связывать параметры заказа (его уникальный идентификатор) с конкретным процессом приготовления напитка;</li>
|
||||
<li>предоставлять атомарный интерфейс логических операций, скрывая за фасадом различия в физической имплементации этих операций.</li>
|
||||
</ul>
|
||||
<p>По сути пара <code>volume_requested</code> / <code>volume_prepared</code> и является аналогом дополнительной сущности <code>task</code>, зачем мы тогда усложняли?</p>
|
||||
<p>Во-первых, в схеме с дополнительным уровнем абстракции мы скрываем конструирование самого объекта <code>task</code>. Если от <code>GET /orders/{id}</code> ожидается, что он вернёт хотя бы логически те же параметры заказа, что были переданы в <code>POST /coffee-machines/orders</code>, то при конструировании <code>task</code> сформировать нужный набор параметров — уже наша ответственность, спрятанная внутри обработчика создания заказа. Мы можем переформулировать параметры заказа в более удобные для исполнения на кофе-машине термины — например, возвращаясь к вопросу проверки готовности, явно сформулировать политику определения готовности кофе:</p>
|
||||
<ul>
|
||||
<li><code>POST /tasks/?order_id={order_id}</code><br />
|
||||
<code>{…"volume_requested":"800ml","readiness_policy":"check_volume"…}</code><br />
|
||||
— внутри обработчика создания заказа мы обратились к спецификации кофе-машины и поставили задачу в соответствии с ней. (Здесь мы предполагаем, что <code>POST /tasks</code> — внутренний метод создания задач; он может и не существовать в виде API.)</li>
|
||||
<li><code>GET /tasks/{id}/status</code><br />
|
||||
<code>{…"volume_prepared":"200ml","ready":false}</code><br />
|
||||
— в публичном интерфейсе </li>
|
||||
</ul>
|
||||
<p>На это (совершенно верное!) замечаниемы ответим, что выделение уровней абстракции — прежде всего <em>логическая</em> процедура: как мы объясняем себе и разработчику, из чего состоит наш API. Мы могли бы просто ограничиться выделением секции <code>task</code> в ответе <code>GET /orders/{id}</code> — или вовсе сказать, что <code>task</code> — это просто четверка полей (<code>ready</code>, <code>volume_requested</code>, <code>volume_prepared</code>, <code>readiness_policy</code>) и есть. <strong>Абстрагируемая дистанция между сущностями существует объективно</strong>, каким бы образом мы ни написали конкретные интерфейсы. Наша задача состоит только лишь в том, чтобы эта дистанция была разделена на уровни <em>явно</em>. Чем неявнее разведены уровни абстракции (или хуже того, перемешаны) уровни абстракции, тем сложнее будет разобраться в вашем API и тем хуже будет написан использующий его код.</p>
|
||||
<p>Первая из проблем, которую мы видим на данном этапе — это отсутствие на физическом уровне машин второго типа самого понятия «заказ» или «напиток»: машина выполняет какой-то набор операций</p>
|
||||
<p>Выделение уровней абстракции — прежде всего <em>логическая</em> процедура: как мы объясняем себе и разработчику, из чего состоит наш API. Мы могли бы просто ограничиться выделением секции <code>task</code> в ответе <code>GET /orders/{id}</code> — или вовсе сказать, что <code>task</code> — это просто четверка полей (<code>ready</code>, <code>volume_requested</code>, <code>volume_prepared</code>, <code>readiness_policy</code>) и есть. <strong>Абстрагируемая дистанция между сущностями существует объективно</strong>, каким бы образом мы ни написали конкретные интерфейсы. Наша задача состоит только лишь в том, чтобы эта дистанция была разделена на уровни <em>явно</em>. Чем неявнее разведены уровни абстракции (или хуже того, перемешаны) уровни абстракции, тем сложнее будет разобраться в вашем API и тем хуже будет написан использующий его код.</p>
|
||||
<p>NB: важно заметить, что с дальнейшей проработкой уровень исполнения, скорее всего, сам должен будет разделиться на два и более уровня, т.к. «задача» по сути — просто сущность-зонтик, связывающая в рамках заказа несколько высокоуровневых сущностей. Идея определения параметров кофе-машины на этапе создания заказов не очень удобна, да и до манипуляции командами кофе-машины и состоянием сенсоров всё ещё далеко с точки зрения абстрагирования. Но мы пока оставим в таком виде, для удобства дальнейшего изложения.</p>
|
||||
<h4 id="">Изоляция уровней абстракции</h4>
|
||||
<p>Важное свойство правильно подобранных уровней абстракции, и отсюда требование к их проектированию — это требование изоляции: <strong>взамодействие возможно только между сущностями соседних уровней абстракции</strong>. Если при проектировании выясняется, что для выполнения того или иного действия требуется «перепрыгнуть» уровень абстракции, это явный признак того, что в проекте допущены ошибки.</p>
|
||||
<p>Возвращаясь к нашему примеру с готовностью кофе: проблемы с определением готовности кофе исходя из объёма возникают именно потому, что мы не можем ожидать от пользователя, создающего заказ, знания о необходимости проверки объёма налитого реальной кофе-машиной объёма кофе. Мы вводим дополнительный уровень абстракции именно для того, чтобы на нём переформулировать, что такое «заказ готов».</p>
|
||||
<p>Важным следствием этого принципа является то, что информацию о готовности заказа нам придётся «прорастить» через все уровни абстракции:</p>
|
||||
<ol>
|
||||
<li>На физическом уровне мы будем оперировать состоянием кофе-машины, её сенсоров;</li>
|
||||
<li>На физическом уровне мы будем оперировать состоянием кофе-машины, её сенсоров; с точки зрения физических сенсоров нет никакой «готовности заказа», есть только состояние выполнения команд;</li>
|
||||
<li>На уровне исполнения статус готовности означает, что состояние сенсоров приведено к эталонному (в случае политики "check_volume" — что налит именно тот объём кофе, который был запрошен);</li>
|
||||
<li>На пользовательском уровне статус готовности заказа означает, что все ассоциированные задачи выполнены.</li>
|
||||
</ol>
|
||||
@ -272,8 +380,8 @@ h4, h5 {
|
||||
<ul>
|
||||
<li><code>POST /orders/{id}/cancel</code> работает с высокоуровневыми данными о заказе:<ul>
|
||||
<li>проверяет авторизацию, т.е. имеет ли право этот пользователь отменять этот заказ;</li>
|
||||
<li>решает денежные вопросы — нужно ли делать рефанд</li>
|
||||
<li>находит все незавершённые задачи и отменяет их</li></ul></li>
|
||||
<li>решает денежные вопросы — нужно ли делать рефанд;</li>
|
||||
<li>находит все незавершённые задачи и отменяет их;</li></ul></li>
|
||||
<li><code>POST /tasks/{id}/cancel</code> работает с исполнением заказа:<ul>
|
||||
<li>определяет, возможно ли физически отменить исполнение, есть ли такая функция у кофе-машины;</li>
|
||||
<li>генерирует последовательность действий отмены (возможно, не только непосредственно для самой машины — вполне вероятно, необходимо будет поставить задание сотруднику кофейни утилизировать невостребованный напиток);</li></ul></li>
|
||||
@ -285,10 +393,8 @@ h4, h5 {
|
||||
<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>operation_state</code>, что, быть может, разработчику вовсе и не нужно.</p>
|
||||
<p>Может показаться, что соблюдение правила изоляции уровней абстракции является избыточным и заставляет усложнять интерфейс. И это в действительности так: важно понимать, что никакая гибкость, логичность, читабельность и расширяемость не бывает бесплатной. Можно построить API так, чтобы оно выполняло свою функцию с минимальными накладными расходами, по сути — дать интерфейс к микроконтроллерам кофе-машины. Однако пользоваться им будет крайне неудобно, и расширяемость такого API будет нулевой.</p>
|
||||
<p>Дублирование функций на каждом уровне абстракций позволяет добиться важной вещи: возможности сменить нижележащие уровни без необходимости переписывать верхнеуровневый код. Мы можем добавить другие виды кофе-машин с принципиально другими физическими способами определения готовности напитка, и наш метод <code>GET /orders?order_id={id}</code> продолжит работать, как работал.</p>
|
||||
<p>Да, код, который работал с физическим уровнем, придётся переписать. Но, во-первых, это неизбежно: изменение принципов работы физического уровня автоматически означает необходимость переписать код. Во-вторых, такое разделение ставит перед нами четкий вопрос: до какого момента API должно предоставлять публичный доступ? Стоило ли предоставлять пользователю методы физического уровня?</p><div class="page-break"></div><h3 id="10">Глава 10. Разграничение областей ответственности</h3>
|
||||
<p><strong>NB</strong>: так как <code>task</code> связывает два разных уровня абстракции, то и статусов у неё два: внешний <code>canceled</code> и внутренний <code>canceling</code>. Мы могли бы опустить второй статус и предложить ориентироваться на содержание <code>operations</code>, но это (а) неявно, (б) предполагает необходимость разбираться в более низкоуровневом интерфейсе <code>operation_state</code>, что, быть может, разработчику вовсе и не нужно.</p>
|
||||
<p>Может показаться, что соблюдение правила изоляции уровней абстракции является избыточным и заставляет усложнять интерфейс. И это в действительности так: важно понимать, что никакая гибкость, логичность, читабельность и расширяемость не бывает бесплатной. Можно построить API так, чтобы оно выполняло свою функцию с минимальными накладными расходами, по сути — дать интерфейс к микроконтроллерам кофе-машины. Однако пользоваться им будет крайне неудобно, и расширяемость такого API будет нулевой.</p><div class="page-break"></div><h3 id="10">Глава 10. Разграничение областей ответственности</h3>
|
||||
<p>Исходя из описанного в предыдущей главе, мы понимаем, что иерархия абстракций в нашем гипотетическом проекте должна выглядеть примерно так:</p>
|
||||
<ul>
|
||||
<li>пользовательский уровень (те сущности, с которыми непосредственно взаимодействует пользователь и сформулированы в понятных для него терминах; например, заказы и виды кофе);</li>
|
||||
@ -311,10 +417,8 @@ h4, h5 {
|
||||
</ol>
|
||||
<p>Если внимательно посмотреть на каждый объект, то мы увидим, что, в итоге, каждый объект оказался в смысле своей ответственности составным. Например, <code>coffee-machine</code> будет частично оперировать реальными командами кофе-машины <em>и</em> представлять их состояние в каком-то машиночитаемом виде.</p>
|
||||
<h4 id="">Декомпозиция интерфейсов</h4>
|
||||
<p>// TODO</p>
|
||||
<p>NB. Во многих языках программирования нет поддержки интерфейсов, абстрактных классов и/или множественного наследования. Однако выделять интерфейсы нам это не мешает, поскольку мы всегда можем "договориться", что объект source имеет право пользоваться только вот этим набором свойств и методов. Конечно, контролировать соблюдение этой договоренности в достаточно развесистом API довольно сложно, но, поверьте автору, вполне возможно, тем более, что тестами и/или статическим анализом кода соблюдение договоренностей об интерфейсах можно проверить почти всегда.</p>
|
||||
<p>Разделение контекстов - не единственная причина, по которой выделение интерфейсов критически важно при проектировании API. Предъявление к входящим параметрам требования только удовлетворять интерфейсу существенно упрощает создание альтернативных реализаций ваших объектов, в том числе в целях тестирования. Теперь чтобы протестировать объект source достаточно написать mock на IGeoContext, а не весь класс map целиком. Аналогично, если мы захотим использовать наши оверлеи для показа их, скажем, в качестве какой-то инфографики или на абстрактном плане местности, нам не придётся переделывать для этого класс Map - достаточно будет альтернативной реализации IGraphicalContext.</p>
|
||||
<p>При выделении интерфейсов важно также понимать, что интерфейс, в отличие от его реализации, должен быть минимально достаточным и не должен включать в себя вспомогательные методы. Например, если класс map имеет как метод для получения всей области картографирования в виде четырехугольника getBBox, так и методы получения каждого из углов по отдельности - getLeftBottom, getRightTop, например, — то интерфейс IGeoContext должен содержать что-то одно. Нет никакого смысла загромождать интерфейс альтернативными реализациями одной и той же функциональности — это затрудняет чтение и усложняет написание собственных реализаций. Если только нет каких-то показаний с точки зрения производительности, следует отдать предпочтение максимально общему методу - в нашем случае getBBox.</p>
|
||||
<p>На этапе разделения уровней абстракции мы упомянули, что одна из целей такого разделения — добиться возможности безболезненно сменить нижележащие уровни абстракции при добавлении новых технологий или изменении нижележащих протоколов. Декомпозиция интерфейсов — основной инструмент, позволяющий добиться такой гибкости.</p>
|
||||
<p>На этом этапе нам предстоит отделить частное от общего: понять, какие свойства сущностей фиксированы в нашей модели, а какие будут зависеть от реализации. В нашем кофе-примере таким сильно зависящим от имплементации объектом, очевидно, будут сами кофе-машины. Попробуем ещё немного развить наше API: каким образом нам нужно описать модели кофе-машин для того, чтобы предоставить интерфейсы для работы с задачами заказа?</p>
|
||||
<h4 id="-1">Интерфейсы как универсальный паттерн</h4>
|
||||
<p>Как мы убедились в предыдущей главе, выделение интерфейсов крайне важно с точки зрения удобства написания кода. Однако, интерфейсы играют и другую важную роль в проектировании: они позволяют уложить в голове архитектуру API целиком.</p>
|
||||
<p>Любой сколько-нибудь крупный API рано или поздно обрастает разнообразной номенклатурой сущностей, их свойст и методов, как в силу того, что в одном объекте «сходятся» несколько предметных областей, так и в силу появления со временем разнообразной вспомогательной и дополнительной функциональности. Особенно сложной номенклатура объектов и их методов становится в случае появления альтернативных реализаций одного и того же интерфейса.</p>
|
||||
@ -334,7 +438,8 @@ h4, h5 {
|
||||
<li>преобразованием данных имеют право заниматься только те объекты, в чьи непосредственные обязанности это входит.</li>
|
||||
</ul>
|
||||
<p>Дерево информационных контекстов (какой объект обладает какой информацией, и кто является транслятором из одного контекста в другой), по сути, представляет собой «срез» нашего дерева иерархии интерфейсов; выделение такого среза позволяет проще и удобнее удерживать в голове всю архитектуру проекта.</p>
|
||||
<p>// TODO: простые вещи делаются просто без бойлерплейта</p><div class="page-break"></div><h3 id="11">Глава 11. Описание конечных интерфейсов</h3>
|
||||
<p>// TODO
|
||||
// Хелперы, бойлерплейт</p><div class="page-break"></div><h3 id="11">Глава 11. Описание конечных интерфейсов</h3>
|
||||
<p>Определив все сущности, их ответственность и отношения друг с другом, мы переходим непосредственно к разработке API: нам осталось прописать номенклатуру всех объектов, полей, методов и функций в деталях. В этой главе мы дадим сугубо практические советы, как сделать API удобным и понятным.</p>
|
||||
<p>Важное уточнение под номером ноль:</p>
|
||||
<h4 id="0">0. Правила — это всего лишь обобщения</h4>
|
||||
@ -347,25 +452,29 @@ h4, h5 {
|
||||
<ul>
|
||||
<li><p>плохо: </p>
|
||||
<pre><code>GET /orders/cancellation
|
||||
отменяет заказ
|
||||
// отменяет заказ
|
||||
</code></pre>
|
||||
<p>Неочевидно, что достаточно просто обращения к сущности <code>cancellation</code> (что это?), тем более немодифицирующим методом <code>GET</code>, чтобы отменить заказ; </p>
|
||||
<p>Хорошо: </p>
|
||||
<pre><code> POST /orders/cancel
|
||||
отменяет заказ
|
||||
</code></pre></li>
|
||||
<li><p>плохо:</p>
|
||||
<pre><code>POST /orders/cancel
|
||||
отменяет заказ
|
||||
</code></pre>
|
||||
<ul>
|
||||
<li>плохо:</li></ul>
|
||||
<pre><code>GET /orders/statistics
|
||||
Возвращает агрегированную статистику заказов за всё время
|
||||
// Возвращает агрегированную статистику заказов за всё время
|
||||
</code></pre>
|
||||
<p>Даже если операция немодифицирующая, но вычислительно дорогая — следует об этом явно индицировать, особенно если вычислительные ресурсы тарифицируются для пользователя; тем более не стоит подбирать значения по умолчанию так, чтобы вызов операции без параметров максимально расходовал ресурсы.</p>
|
||||
<p>Хорошо:</p>
|
||||
<pre><code>POST /orders/statistics/aggregate
|
||||
{ "start_date", "end_date" }
|
||||
Возвращает агрегированную статистику заказов за указанный период
|
||||
// Возвращает агрегированную статистику заказов за указанный период
|
||||
</code></pre></li>
|
||||
</ul>
|
||||
<p><strong>Стремитесь к тому, чтобы из сигнатуры функции было абсолютно ясно, что она делает, что принимает на вход и что возвращает</strong>. Вообще, при прочтении кода, работающего с вашим API, должно быть сразу понятно, что, собственно, он делает — без подглядывания в документацию.</p>
|
||||
<p>Два важных следствия:</p>
|
||||
<p><strong>1.1.</strong> Если операция модифицирующая, это должно быть очевидно из сигнатуры. В частности, не может быть модифицирующих операций за <code>GET</code>.</p>
|
||||
<p><strong>1.2.</strong> Если операция асинхронная, это должно быть очевидно из сигнатуры, <strong>либо</strong> должна существовать конвенция именования, позволяющая отличаться синхронные операции от асинхронных.</p>
|
||||
<h4 id="2">2. Сущности должны именоваться конкретно</h4>
|
||||
<p>Избегайте слов-«амёб» без определённой семантики, таких как get, apply, make. Сущности должны именоваться конкретно:</p>
|
||||
<ul>
|
||||
@ -380,13 +489,11 @@ h4, h5 {
|
||||
<li>плохо:
|
||||
<code>
|
||||
strpbrk (str1, str2)
|
||||
возвращает положение первого вхождения в строку str2 любого символа из строки str2
|
||||
// возвращает положение первого вхождения в строку str2
|
||||
// любого символа из строки str2
|
||||
</code>
|
||||
Возможно, автору этого API казалось, что аббревиатура <code>pbrk</code> что-то значит для читателя, но он явно ошибся. К тому же, невозможно сходу понять, какая из строк <code>str1</code>, <code>str2</code> является набором символов для поиска.
|
||||
Хорошо:
|
||||
<code>
|
||||
str_search_for_characters(lookup_character_set, str)
|
||||
</code>
|
||||
Хорошо: <code>str_search_for_characters(lookup_character_set, str)</code><br />
|
||||
Однако необходимость существования такого метода вообще вызывает сомнения, достаточно было бы иметь удобную функцию поиска подстроки с нужными параметрами. Аналогично сокращение <code>string</code> до <code>str</code> выглядит совершенно бессмысленным, но, увы, является устоявшимся для большого количества предметных областей.</li>
|
||||
</ul>
|
||||
<h4 id="4">4. Тип поля должен быть ясен из его названия</h4>
|
||||
@ -401,7 +508,17 @@ str_search_for_characters(lookup_character_set, str)
|
||||
<li>плохо: <code>"task.status": true</code> — неочевидно, что статус бинарен, плюс такое API будет нерасширяемым;<br />
|
||||
хорошо: <code>"task.is_finished": true</code></li>
|
||||
</ul>
|
||||
<p>Отдельно следует оговорить, что на разных платформах эти правила следует дополнить по-своему с учетом специфики first-class citizen-типов. Например, объекты типа <code>Date</code>, если таковые имеются, разумно индицировать с помощью, например, постфикса <code>_at</code> (<code>created_at</code>, <code>occurred_at</code>, etc).</p>
|
||||
<p>Отдельно следует оговорить, что на разных платформах эти правила следует дополнить по-своему с учетом специфики first-class citizen-типов. Например, объекты типа <code>Date</code>, если таковые имеются, разумно индицировать с помощью, например, постфикса <code>_at</code> (<code>created_at</code>, <code>occurred_at</code>, etc) или <code>_date</code>.</p>
|
||||
<p>Если наименование сущности само по себе является каким-либо термином, способным смутить разработчика, лучше добавить лишний префикс или постфикс, чтобы избежать непонимания.</p>
|
||||
<ul>
|
||||
<li>Плохо:
|
||||
<code>
|
||||
GET /coffee-machines/functions
|
||||
// Возвращает список встроенных функций кофе-машины
|
||||
</code>
|
||||
Слово "functions" многозначное; может означать и встроенные функции, и написанный код, и состояние (функционирует-не функционирует).<br />
|
||||
Хорошо: <code>GET /coffee-machines/builtin-functions-list</code></li>
|
||||
</ul>
|
||||
<h4 id="5">5. Подобные сущности должны называться подобно и вести себя подобным образом</h4>
|
||||
<ul>
|
||||
<li><p>плохо: <code>begin_transition</code> / <code>stop_transition</code><br />
|
||||
@ -409,10 +526,12 @@ str_search_for_characters(lookup_character_set, str)
|
||||
хорошо: <code>begin_transition</code> / <code>end_transition</code> либо <code>start_transition</code> / <code>stop_transition</code>;</p></li>
|
||||
<li><p>плохо: </p>
|
||||
<pre><code>strpos(haystack, needle)
|
||||
Находит первую позицию позицию строки `needle` внутри строки `haystack`
|
||||
// Находит первую позицию позицию строки `needle`
|
||||
// внутри строки `haystack`
|
||||
</code></pre>
|
||||
<pre><code>str_replace(needle, replace, haystack)
|
||||
Находит и заменяет все вхождения строки `needle` внутри строки `haystack` на строку `replace`
|
||||
// Находит и заменяет все вхождения строки `needle`
|
||||
// внутри строки `haystack` на строку `replace`
|
||||
</code></pre>
|
||||
<p>Здесь нарушены сразу несколько правил:</p>
|
||||
<ul>
|
||||
@ -437,7 +556,8 @@ str_search_for_characters(lookup_character_set, str)
|
||||
</ul>
|
||||
<p>Отдельное следствие из этого правила — денежные величины <em>всегда</em> должны сопровождаться указанием кода валюты.</p>
|
||||
<p>Также следует отметить, что в некоторых областях ситуация со стандартами настолько плоха, что как ни сделай — кто-то останется недовольным. Классический пример такого рода — порядок геокоординат ("широта-долгота" против "долгота-широта"). Здесь, увы, вам остаётся только смириться и проявлять выдержку при нападках на ваше API.</p>
|
||||
<h4 id="7">7. Сохранение точности дробных чисел</h4>
|
||||
<p>// TODO: блокнот душевного спокойствия</p>
|
||||
<h4 id="7">7. Сохраняйте точность дробных чисел</h4>
|
||||
<p>Там, где это позволено протоколом, дробные числа с фиксированной запятой — такие, как денежные суммы, например — должны передаваться в виде специально предназначенных для этого объектов, например, Decimal или аналогичных.</p>
|
||||
<p>Если в протоколе нет Decimal-типов (в частности, в JSON нет чисел с фиксированной запятой), следует либо привести к целому, либо использовать строковый тип.</p><div class="page-break"></div></article>
|
||||
</body></html>
|
BIN
docs/API.ru.pdf
BIN
docs/API.ru.pdf
Binary file not shown.
@ -12,6 +12,6 @@
|
||||
|
||||
Однако статическое удобство и понятность API — это простая часть. В конце концов, никто не стремится специально сделать API нелогичным и нечитаемым — всегда при разработке мы начинаем с каких-то понятных базовых концепций. При минимальном опыте проектирования сложно сделать ядро API, не удовлетворяющее критериям очевидности, читаемости и консистентности.
|
||||
|
||||
Проблемы начинаются, когда мы начинаем API развивать. Добавление новой фунциональности рано или поздно приводит к тому, что некогда простое и понятное API становится наслоением разных концепций, а попытки сохранить обратную совместимость приводят к нелогичным, неочевидным и попросту плохим решениям. Отчасти это связано так же и с тем, что невозможно обладать полным знанием о будущем: ваше понимание о «правильном» API тоже будет меняться со временем, как в объективной части (какие задачи решает API и как лучше это сделать), так и в субъективной — что такое очевидность, читабельность и консистентность для вашего API.
|
||||
Проблемы начинаются, когда мы начинаем API развивать. Добавление новой функциональности рано или поздно приводит к тому, что некогда простое и понятное API становится наслоением разных концепций, а попытки сохранить обратную совместимость приводят к нелогичным, неочевидным и попросту плохим решениям. Отчасти это связано так же и с тем, что невозможно обладать полным знанием о будущем: ваше понимание о «правильном» API тоже будет меняться со временем, как в объективной части (какие задачи решает API и как лучше это сделать), так и в субъективной — что такое очевидность, читабельность и консистентность для вашего API.
|
||||
|
||||
Принципы, которые я буду излагать ниже, во многом ориентированы именно на то, чтобы API правильно развивалось во времени и не превращалось в нагромождение разнородных неконсистентных интерфейсов. Важно понимать, что такой подход тоже небесплатен: необходимость держать в голове варианты развития событий и закладывать возможность изменений в API означает избыточность интерфейсов и возможно излишнее абстрагирование. И то, и другое, помимо прочего, усложняет и работу программиста, пользующегося вашим API. **Закладывание перспектив «на будущее» имеет смысл, только если это будущее у API есть, иначе это попросту оверинжиниринг**.
|
||||
|
@ -6,8 +6,8 @@
|
||||
|
||||
Конечно, крупные компании с прочным положением на рынке могут позволить себе такой налог взымать. Более того, они могут вводить какие-то санкции за отказ от перехода на новые версии API, вплоть до отключения приложений.
|
||||
|
||||
С нашей точки зрения, подобное поведение ничем не может быть оправдано. Избегайте скрытых налогов на своих пользователей. Если вы можете не ломать обратную совсемстимость — не ломайте её.
|
||||
С нашей точки зрения, подобное поведение ничем не может быть оправдано. Избегайте скрытых налогов на своих пользователей. Если вы можете не ломать обратную совместимость — не ломайте её.
|
||||
|
||||
Да, безусловно, поддержка старых версий API — это тоже своего рода налог. Технологии меняются, и, как бы хорошо ни было спроектировано ваше API, всего предусмотреть невозможно. В какой-то момент ценой поддержки старых версий становится невозможность предоставлять новую функциональность и поддерживать новые платформы, и выпустить новую версию всё равно придётся. Однако вы по крайней мере сможете убедить своих потребителей в необходимости перехода.
|
||||
|
||||
Более подробно о политиках версионирования будет рассказано в разделе II.
|
||||
Более подробно о жизненном цикле API и политиках выпуска новых версий будет рассказано в разделе II.
|
@ -7,4 +7,6 @@
|
||||
3. Вторая цифра (минорная версия) увеличивается при добавлении новой функциональности с сохранением обратной совместимости
|
||||
4. Третья цифра (патч) увеличивается при выпуске новых версий, содержащих только исправление ошибок
|
||||
|
||||
Выражения «мажорная версия API» и «версия API, содержащая обратно несовместимые изменения функциональности» тем самым следует считать эквивалентными.
|
||||
Выражения «мажорная версия API» и «версия API, содержащая обратно несовместимые изменения функциональности» тем самым следует считать эквивалентными.
|
||||
|
||||
Более подробно о политиках версионирования будет рассказано в разделе II. В разделе I мы ограничимся лишь указанием версии API в формате `v1`, `v2`, etc.
|
@ -4,6 +4,6 @@
|
||||
|
||||
Для составных частей сущности, к сожалению, достаточно нейтрального термина нам придумать не удалось, поэтому мы используем слова «поля» и «методы».
|
||||
|
||||
Большинство примеров API в общих разделах будут даны в виде JSON-over-HTTP-эндпойтов. Это некоторая условность, которая помогает описать концепции, как нам кажется, максимально понятно. Вместо `GET /orders` вполне может быть вызов метода `orders.get()`, локальный или удалённый; вместо JSON может быть любой другой формат данных. Смысл утверждений от этого не меняется.
|
||||
Большинство примеров API в общих разделах будут даны в виде JSON-over-HTTP-эндпойтов. Это некоторая условность, которая помогает описать концепции, как нам кажется, максимально понятно. Вместо `GET /v1/orders` вполне может быть вызов метода `orders.get()`, локальный или удалённый; вместо JSON может быть любой другой формат данных. Смысл утверждений от этого не меняется.
|
||||
|
||||
Также в примерах часто применяется следующая конвенция. Запись `{ "begin_date" }` (т.е. отсутствие значения у поля в JSON-объекте) означает, что в поле находится именно то, что ожидается — т.е. в данном примере какая-то дата начала.
|
@ -8,6 +8,6 @@
|
||||
|
||||
Этот алгоритм строит API сверху вниз, от общих требований и сценариев использования до конкретной номенклатуры сущностей; фактически, двигаясь этим путем, вы получите на выходе готовое API — чем этот подход и ценен.
|
||||
|
||||
Может показаться, что наиболее полезные советы и best practice приведены в последнем разделе, однако это не так; цена ошибки, допущенной на разных уровнях весьма различна. Если исправить плохое именование довольно просто, то исправить неверное понимание того, зачем вообще нужно API, практически невозможно.
|
||||
Может показаться, что наиболее полезные советы приведены в последнем разделе, однако это не так; цена ошибки, допущенной на разных уровнях весьма различна. Если исправить плохое именование довольно просто, то исправить неверное понимание того, зачем вообще нужно API, практически невозможно.
|
||||
|
||||
_NB_. Здесь и далее мы будем рассматривать концепции разработки API на примере некоторого гипотетического API заказа кофе в городских кофейнях. На всякий случай сразу уточним, что пример является синтетическим; в реальной ситуации, если бы такое API пришлось проектировать, оно вероятно было бы совсем не похоже на наш выдуманный пример.
|
@ -8,26 +8,26 @@
|
||||
|
||||
3. Какую проблему _мы_ решаем? Действительно ли решение этой проблемы находится в нашей компетенции? Действительно ли мы находимся в той позиции, чтобы решить эту проблему?
|
||||
|
||||
4. Какую проблему мы _решаем_? Правда ли, что решение, которое мы предлагаем, действильно решает проблему? Не создаём ли мы на её месте другую проблему, более сложную?
|
||||
4. Какую проблему мы _решаем_? Правда ли, что решение, которое мы предлагаем, действительно решает проблему? Не создаём ли мы на её месте другую проблему, более сложную?
|
||||
|
||||
Итак, предположим, что мы хотим предоставить API автоматического заказа кофе в городских кофейнях. Попробуем применить к ней этот принцип.
|
||||
|
||||
1. Зачем кому-то может потребоваться API для приготовления кофе? В чем неудобство заказа кофе через интерфейс, человек-человек или человек-машина? Зачем нужна возможность заказа машина-машина?
|
||||
* Возможно, мы хотим решить проблему выбора и знания? Чтобы человек наиболее полно знал о доступных ему здесь и сейчас опциях.
|
||||
* Возможно, мы оптимизируем время ожидания? Чтобы человеку не пришлось ждать, пока его заказ готовится.
|
||||
* Возможно, мы хотим минимизировать ошибки? Чтобы человек получил именно то, что хотел заказть, не потеряв информацию при разговорном общении либо при настройке незнакомого интерфейса кофе-машины.
|
||||
* Возможно, мы хотим минимизировать ошибки? Чтобы человек получил именно то, что хотел заказать, не потеряв информацию при разговорном общении либо при настройке незнакомого интерфейса кофе-машины.
|
||||
|
||||
Вопрос «зачем» — самый важный из тех вопросов, которые вы должны задавать себе. Не только глобально в отношении целей всего проекта, но и локально в отношении каждого кусочка функциональности. **Если вы не можете коротко и понятно ответить на вопрос «зачем эта сущность нужна» — значит, она не нужна**.
|
||||
|
||||
Здесь и далее предположим (в целях придания нашему примеру глубины и некоторой упоротости), что мы оптимизируем все три фактора в порядке убывания важности.
|
||||
|
||||
2. Правда ли решаемая проблема существует? Дейсвительно ли мы наблюдаем неравномерную загрузку кофейных автоматов по утрам? Правда ли люди страдают от того, что не могут найти поблизости нужный им латте с ореховым сиропом? Действительно ли людям важны те минуты, которые они теряют, стоя в очередях?
|
||||
2. Правда ли решаемая проблема существует? Действительно ли мы наблюдаем неравномерную загрузку кофейных автоматов по утрам? Правда ли люди страдают от того, что не могут найти поблизости нужный им латте с ореховым сиропом? Действительно ли людям важны те минуты, которые они теряют, стоя в очередях?
|
||||
|
||||
3. Действительно ли мы обладаем достаточным ресурсом, чтобы решить эту проблему? Есть ли у нас доступ к достаточному количеству кофемашин и клиентов, чтобы обеспечить работоспособность системы?
|
||||
3. Действительно ли мы обладаем достаточным ресурсом, чтобы решить эту проблему? Есть ли у нас доступ к достаточному количеству кофе-машин и клиентов, чтобы обеспечить работоспособность системы?
|
||||
|
||||
4. Наконец, правда ли мы решим проблему? Как мы поймём, что оптимизировали перечисленные факторы?
|
||||
|
||||
На все эти вопросы, в общем случае, простого ответа нет. В идеале ответы на эти вопросы должны даваться с цифрами в руках. Сколько конкретно времени тратится неоптимально, и какого значения мы рассчитываем добиться, располагая какой плотностью кофемашин? Заметим также, что в реальной жизни просчитать такого рода цифры можно в основном для проектов, которые пытаются влезть на уже устоявшийся рынок; если вы пытаетесь сделать что-то новое, то, вероятно, вам придётся ориентироваться в основном на свою интуицию.
|
||||
На все эти вопросы, в общем случае, простого ответа нет. В идеале ответы на эти вопросы должны даваться с цифрами в руках. Сколько конкретно времени тратится неоптимально, и какого значения мы рассчитываем добиться, располагая какой плотностью кофе-машин? Заметим также, что в реальной жизни просчитать такого рода цифры можно в основном для проектов, которые пытаются влезть на уже устоявшийся рынок; если вы пытаетесь сделать что-то новое, то, вероятно, вам придётся ориентироваться в основном на свою интуицию.
|
||||
|
||||
#### Почему API?
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
1. Что конкретно мы делаем
|
||||
2. Как мы это делаем
|
||||
|
||||
В случае нашего кофепримера мы:
|
||||
В случае нашего кофе-примера мы:
|
||||
|
||||
1. Предоставляем сервисам с большой пользовательской аудиторией API для того, чтобы их потребители могли максимально удобно для себя заказать кофе.
|
||||
2. Для этого мы абстрагируем за нашим HTTP API доступ к «железу» и предоставим методы для выбора вида напитка и места его приготовления и для непосредственно исполнения заказа.
|
||||
|
@ -1,13 +1,13 @@
|
||||
### Разделение уровней абстракции
|
||||
|
||||
«Разделите свой код на уровни абстракции» - пожалуй, самый общий совет для разработчиков программного обеспечения. Однако будет вовсе не преувеличением сказать, что изоляция уровней абстрации — самая сложная задача, стоящая перед разработчиком API.
|
||||
«Разделите свой код на уровни абстракции» - пожалуй, самый общий совет для разработчиков программного обеспечения. Однако будет вовсе не преувеличением сказать, что изоляция уровней абстракции — самая сложная задача, стоящая перед разработчиком API.
|
||||
|
||||
Прежде чем переходить к теории, следует чётко сформулировать, _зачем_ нужны уровни абстракции и каких целей мы хотим достичь их выделением.
|
||||
|
||||
Вспомним, что программный продукт - это средство связи контекстов, средство преобразования терминов и операций одной предметной области в другую. Чем дальше друг от друга эти области отстоят - тем большее число промежуточных передаточных звеньев нам придётся ввести. Вернёмся к нашему примеру с кофейнями. Какие уровни сущностей мы видим?
|
||||
|
||||
1. Непосредственно состояние кофе-машины и шаги приготовления кофе. Температура, давление, объём воды.
|
||||
2. У кофе есть мета-характерстики: сорт, вкус, вид напитка.
|
||||
2. У кофе есть мета-характеристики: сорт, вкус, вид напитка.
|
||||
3. Мы готовим с помощью нашего API *заказ* — один или несколько стаканов кофе с определенной стоимостью.
|
||||
4. Наши кофе-машины как-то распределены в пространстве (и времени).
|
||||
5. Кофе-машина принадлежит какой-то сети кофеен, каждая из которых обладает какой-то айдентикой и специальными возможностями.
|
||||
@ -22,12 +22,12 @@
|
||||
|
||||
Допустим, мы имеем следующий интерфейс:
|
||||
|
||||
* `GET /recipes/lungo`
|
||||
* `GET /v1/recipes/lungo`
|
||||
— возвращает рецепт лунго;
|
||||
* `POST /coffee-machines/orders?machine_id={id}`
|
||||
* `POST /v1/coffee-machines/orders?machine_id={id}`
|
||||
`{recipe:"lungo"}`
|
||||
— размещает на указанной кофе-машине заказ на приготовление лунго и возвращает идентификатор заказа;
|
||||
* `GET /orders?order_id={id}`
|
||||
* `GET /v1/orders?order_id={id}`
|
||||
— возвращает состояние заказа;
|
||||
|
||||
И зададимся вопросом, каким образом разработчик определит, что заказ клиента готов. Допустим, мы сделаем так: добавим в рецепт лунго эталонный объём, а в состояние заказа — количество уже налитого кофе. Тогда разработчику нужно будет проверить совпадение этих двух цифр, чтобы убедиться, что кофе готов.
|
||||
@ -39,11 +39,11 @@
|
||||
2. Мы автоматически получаем проблемы, если захотим варьировать размер кофе. Допустим, в какой-то момент мы захотим представить пользователю выбор, сколько конкретно миллилитров лунго он желает. Тогда нам придётся проделать один из следующих трюков:
|
||||
* или мы фиксируем список допустимых объёмов и заводим фиктивные рецепты типа `/recipes/small-lungo`, `recipes/large-lungo`. Почему фиктивные? Потому что рецепт один и тот же, меняется только объём. Нам придётся либо тиражировать одинаковые рецепты, отличающиеся только объёмом, либо вводить какое-то «наследование» рецептов, чтобы можно было указать базовый рецепт и только переопределить объём;
|
||||
* или мы модифицируем интерфейс, объявляя объём кофе, указанный в рецепте, значением по умолчанию; при размещении заказа мы разрешаем указать объём, отличный от эталонного:
|
||||
`POST /coffee-machines/orders?machine_id={id}`
|
||||
`POST /v1/coffee-machines/orders?machine_id={id}`
|
||||
`{recipe:"lungo","volume":"800ml"}`
|
||||
Для таких кофе произвольного объёма нужно будет получать требуемый объём не из `GET /recipes`, а из `GET /orders`. Сделав так, мы сразу получаем клубок из связанных проблем:
|
||||
* разработчик, которому придётся поддержать эту функциональность, имеет высокие шансы сделать ошибку: добавив поддержку произвольного объёма кофе в код, работающий с `POST /coffee-machines/orders` нужно не забыть переписать код проверки готовности заказа;
|
||||
* мы получим классическую ситуацию, когда одно и то же поле (объём кофе) значит разные вещи в разных интерфейсах. В `GET /recipes` поле «объём» теперь значит «объём, который будет запрошен, если не передать его явно в `POST /coffee-machines/orders`»; переименовать его в «объём по умолчанию» уже не получиться, с этой проблемой теперь придётся жить.
|
||||
Для таких кофе произвольного объёма нужно будет получать требуемый объём не из `GET /v1/recipes`, а из `GET /v1/orders`. Сделав так, мы сразу получаем клубок из связанных проблем:
|
||||
* разработчик, которому придётся поддержать эту функциональность, имеет высокие шансы сделать ошибку: добавив поддержку произвольного объёма кофе в код, работающий с `POST /v1/coffee-machines/orders` нужно не забыть переписать код проверки готовности заказа;
|
||||
* мы получим классическую ситуацию, когда одно и то же поле (объём кофе) значит разные вещи в разных интерфейсах. В `GET /v1/recipes` поле «объём» теперь значит «объём, который будет запрошен, если не передать его явно в `POST /v1/coffee-machines/orders`»; переименовать его в «объём по умолчанию» уже не получится, с этой проблемой теперь придётся жить.
|
||||
|
||||
3. Вся эта схема полностью неработоспособна, если разные модели кофе-машин производят лунго разного объёма. Для решения задачи «объём лунго зависит от вида машины» нам придётся сделать совсем неприятную вещь: сделать рецепт зависимым от id машины. Тем самым мы начнём активно смешивать уровни абстракции: одной частью нашего API (рецептов) станет невозможно пользоваться без другой части (информации о кофе-машинах). Что немаловажно, от разработчиков потребуется изменить логику своего приложения: если раньше они могли предлагать сначала выбрать объём, а потом кофе-машину, то теперь им придётся полностью изменить этот шаг.
|
||||
|
||||
@ -51,8 +51,6 @@
|
||||
|
||||
1. От сценариев использования к их внутренней реализации: высокоуровневые сущности и номенклатура их методов должны напрямую отражать сценарии использования API; низкоуровневый - отражать декомпозицию сценариев на составные части.
|
||||
|
||||
Здесь мы должны явно обратиться к выписанному нами ранее «что» и «как». В идеальном мире высший уровень абстракции вашего API должен быть просто переводом записанной человекочитаемой фразы на машинный язык. Если нужно узнать, готов ли заказ — значит, должен быть метод `is-order-ready` (если мы считаем эту операцию действительно важной и частотной) или хотя бы `GET /orders/{id}/status` для того, чтобы явно узнать статус заказа. Эту логику требуется прорастить вниз до самых мелких и частных сценариев типа определения температуры напитка или наличия у исполнителя картонного держателя нужного размера.
|
||||
|
||||
2. От терминов предметной области пользователя к терминам предметной области исходных данных — в нашем случае от высокоуровневых понятий «рецепт», «заказ», «бренд», «кофейня» к низкоуровневым «температура напитка» и «координаты кофе-машины»
|
||||
|
||||
3. Наконец, от структур данных, в которых удобно оперировать пользователю к структурам данных, максимально приближенных к «сырым» - в нашем случае от «лунго» и «сети кофеен "Ромашка"» - к сырым байтовый данным, описывающим состояние кофе-машины марки «Доброе утро» в процессе приготовления напитка.
|
||||
@ -64,36 +62,147 @@
|
||||
* с одной стороны, «заказ» не должен содержать информацию о датчиках и сенсорах кофе-машины;
|
||||
* с другой стороны, кофе-машина не должна хранить информацию о свойствах заказа (да и вероятно её API такой возможности и не предоставляет).
|
||||
|
||||
Введём промежуточный уровень: нам нужно звено, которое одновременно знает о заказе, рецепте и кофе-машине. Назовём его «уровнем исполнения»: его ответственностью является интерпретация заказа, превращение его в набор команд кофе-машине. Самый простой вариант — ввести абстрактную сущность «задание» `task`:
|
||||
Наивный подход в такой ситуации — искусственно ввести некий промежуточный уровень абстракции, «передаточное звено», который переформулирует задачи одного уровня абстракции в другой. Например, введём сущность `task` вида:
|
||||
```
|
||||
{
|
||||
…
|
||||
"volume_requested": "800ml",
|
||||
"volume_prepared": "200ml",
|
||||
"readiness_policy": "check_volume",
|
||||
"ready": false,
|
||||
"operation_state": {
|
||||
"status": "executing",
|
||||
"operations": [
|
||||
// описание операций, запущенных на
|
||||
// физической кофе-машине
|
||||
]
|
||||
}
|
||||
…
|
||||
}
|
||||
```
|
||||
|
||||
* заказ порождает одно или несколько заданий, указывая для задания конкретный рецепт и кофе-машину;
|
||||
* задание в свою очередь оперирует командами кофе-машины и отвечает за интерпретацию состояния датчиков.
|
||||
Я называю этот подход «наивным» не потому, что он неправильный; напротив, это вполне логичное решение «по умолчанию», если вы на данном этапе ещё не знаете или не понимаете, как будет выглядеть ваше API. Проблема его в том, что он умозрительный: он не добавляет понимания того, как устроена предметная область.
|
||||
|
||||
Таким образом, наше API будет выглядеть примерно так:
|
||||
* `POST /orders` — создаёт заказ;
|
||||
* `GET /tasks?order_id={order_id}` — позволяет получить список заданий по заказу.
|
||||
Хороший разработчик в нашем примере должен спросить: хорошо, а какие вообще говоря существуют варианты? Как можно определять готовность напитка? Если вдруг окажется, что сравнение объёмов — единственный способ определения готовности во всех без исключения кофе-машинах, то почти все рассуждения выше — неверны: можно совершенно спокойно включать в интерфейсы определение готовности кофе по объёму, т.к. никакого другого и не существует. Прежде, чем что-то абстрагировать — надо представлять, *что* мы, собственно, абстрагируем.
|
||||
|
||||
Внимательный читатель может здесь поинтересоваться, а в чём, собственно разница по сравнению с наивным подходом? Напомню, мы рассмотрели выше примерно такой вариант:
|
||||
Для нашего примера допустим, что мы сели изучать спецификации API кофе-машин и выяснили, что существует принципиально два класса устройств:
|
||||
* кофе-машины с предустановленными программами, которые умеют готовить заранее прошитые N видов напитков, и мы можем управлять только какими-то параметрами напитка (скажем, объёмом напитка, вкусом сиропа и видом молока); у таких машин отсутствует доступ к внутренним функциям и датчикам, но зато машина умеет через API сама отдавать статус приготовления напитка;
|
||||
* кофе-машины с предустановленными функциями типа «смолоть такой-то объём кофе», «пролить N миллилитров воды», «взбить молочную пену» и т.д.: у таких машин отсутствует понятие «программа приготовления», но есть доступ к микрокомандам и датчикам.
|
||||
|
||||
* `POST /coffee-machines/orders?machine_id={id}`
|
||||
`{recipe:"lungo","volume":"800ml"}`
|
||||
— создаёт заказ указанного объёма
|
||||
* `GET /orders/{id}`
|
||||
`{…"volume_requested":"800ml","volume_prepared":"120ml"…}`
|
||||
— состояние исполнения заказа (налито 120 мл из запрошенных 800).
|
||||
Предположим, для большей конкретности, что эти два класса устройств поставляются вот с таким физическим API:
|
||||
|
||||
По сути пара `volume_requested` / `volume_prepared` и является аналогом дополнительной сущности `task`, зачем мы тогда усложняли?
|
||||
* Машины с предустановленными программами:
|
||||
```
|
||||
GET /programs
|
||||
// Возвращает список предустановленных программ
|
||||
{
|
||||
// идентификатор программы
|
||||
"program": "01",
|
||||
// вид кофе
|
||||
"type": "lungo"
|
||||
}
|
||||
```
|
||||
```
|
||||
POST /execute
|
||||
{
|
||||
"program": 1,
|
||||
"volume": "200ml"
|
||||
}
|
||||
// Запускает указанную программу на исполнение
|
||||
// и возвращает статус исполнения
|
||||
{
|
||||
// Уникальный идентификатор задания
|
||||
"execution_id": "01-01",
|
||||
// Идентификатор исполняемой программы
|
||||
"program": 1,
|
||||
// Запрошенный объём напитка
|
||||
"volume": "200ml",
|
||||
// Ожидаемое время приготовления
|
||||
"preparation_time": "20s",
|
||||
// Готовность
|
||||
"ready": false
|
||||
}
|
||||
```
|
||||
```
|
||||
POST /cancel
|
||||
// Отменяет текущую программу
|
||||
```
|
||||
```
|
||||
GET /execution/{execution_id}/status
|
||||
// Возвращает статус исполнения
|
||||
// Формат аналогичен формату ответа `POST /execute`
|
||||
```
|
||||
|
||||
Во-первых, в схеме с дополнительным уровнем абстракции мы скрываем конструирование самого объекта `task`. Если от `GET /orders/{id}` ожидается, что он вернёт хотя бы логически те же параметры заказа, что были переданы в `POST /coffee-machines/orders`, то при конструировании `task` сформировать нужный набор параметров — уже наша ответственность, спрятанная внутри обработчика создания заказа. Мы можем переформулировать параметры заказа в более удобные для исполнения на кофе-машине термины — например, возвращаясь к вопросу проверки готовности, явно сформулировать политику определения готовности кофе:
|
||||
_NB_. На всякий случай отметим, что данное API нарушает множество описанных нами принципов проектирования, начиная с отсутствия версионирования; оно приведено в таком виде по двум причинам: (1) чтобы мы могли показать, как спроектировать API более удачно; (2) скорее всего, в реальной жизни вы получите именно такое API от производителей кофе-машин, и это ещё довольно вменяемый вариант.
|
||||
|
||||
* `POST /tasks/?order_id={order_id}`
|
||||
`{…"volume_requested":"800ml","readiness_policy":"check_volume"…}`
|
||||
— внутри обработчика создания заказа мы обратились к спецификации кофе-машины и поставили задачу в соответствии с ней. (Здесь мы предполагаем, что `POST /tasks` — внутренний метод создания задач; он может и не существовать в виде API.)
|
||||
* `GET /tasks/{id}/status`
|
||||
`{…"volume_prepared":"200ml","ready":false}`
|
||||
— в публичном интерфейсе
|
||||
* Машины с предустановленными функциями:
|
||||
```
|
||||
GET /functions
|
||||
// Возвращает список доступных функций
|
||||
{
|
||||
"functions": [
|
||||
{
|
||||
// Тип операции
|
||||
// * set_cup — поставить стакан
|
||||
// * grind_coffee — смолоть кофе
|
||||
// * shed_water — пролить воду
|
||||
// * discard_cup — утилизировать стакан
|
||||
"type": "set_cup",
|
||||
// Допустимые аргументы для каждой операции
|
||||
// Для простоты ограничимся одним аргументом:
|
||||
// * volume — объём стакана, кофе или воды
|
||||
"arguments": ["volume"]
|
||||
},
|
||||
…
|
||||
]
|
||||
}
|
||||
```
|
||||
```
|
||||
POST /functions
|
||||
{
|
||||
"type": "set_cup",
|
||||
"arguments": [{ "name": "volume", "value": "300ml" }]
|
||||
}
|
||||
// Запускает на исполнение функцию
|
||||
// с передачей указанных значений аргументов
|
||||
```
|
||||
```
|
||||
GET /sensors
|
||||
// Возвращает статусы датчиков
|
||||
{
|
||||
"sensors": [
|
||||
{
|
||||
// Допустимые значения
|
||||
// * cup_volume — объём установленного стакана
|
||||
// * ground_coffee_volume — объём смолотого кофе
|
||||
// * cup_filled_volume — объём напитка в стакане
|
||||
"type": "cup_volume",
|
||||
"value": "200ml"
|
||||
},
|
||||
…
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
_NB_. Пример нарочно сделан умозрительным для моделирования ситуации, описанной в начале главы: для определения готовности напитка нужно сличить объём налитого с эталоном.
|
||||
|
||||
На это (совершенно верное!) замечаниемы ответим, что выделение уровней абстракции — прежде всего _логическая_ процедура: как мы объясняем себе и разработчику, из чего состоит наш API. Мы могли бы просто ограничиться выделением секции `task` в ответе `GET /orders/{id}` — или вовсе сказать, что `task` — это просто четверка полей (`ready`, `volume_requested`, `volume_prepared`, `readiness_policy`) и есть. **Абстрагируемая дистанция между сущностями существует объективно**, каким бы образом мы ни написали конкретные интерфейсы. Наша задача состоит только лишь в том, чтобы эта дистанция была разделена на уровни _явно_. Чем неявнее разведены уровни абстракции (или хуже того, перемешаны) уровни абстракции, тем сложнее будет разобраться в вашем API и тем хуже будет написан использующий его код.
|
||||
Теперь картина становится более явной: нам нужно абстрагировать работу с кофе-машиной так, чтобы наш «уровень исполнения» в API предоставлял общие функции (такие, как определение готовности напитка) в унифицированном виде. Важно отметить, что с точки зрения разделения абстракций два этих вида кофе-машин сами находятся на разных уровнях: первые предоставляют API более высокого уровня, нежели вторые; следовательно, и «ветка» нашего API, работающая со вторым видом машин, будет более «развесистой».
|
||||
|
||||
Следующий шаг, необходимый для отделения уровней абстракции — необходимо понять, какую функциональность нам, собственно, необходимо абстрагировать. Для этого нам необходимо обратиться к задачам, которые решает разработчик на уровне работы с заказами, и понять, какие проблемы у него возникнут в случае отсутствия нашего слоя абстракции.
|
||||
1. Очевидно, что разработчику хочется создавать заказ унифицированным образом — перечислить высокоуровневые параметры заказа (вид напитка, объём и специальные требования, такие как вид сиропа или молока) — и не думать о том, как на конкретной машине исполнить этот заказ.
|
||||
2. Разработчику надо понимать состояние исполнения — готов ли заказ или нет; если не готов — когда ожидать готовность (и надо ли её ожидать вообще в случае ошибки исполнения).
|
||||
3. Разработчику нужно уметь соотносить заказ с его положением в пространстве и времени — чтобы показать потребителю, когда и как нужно заказ забрать.
|
||||
4. Наконец, разработчику нужно выполнять атомарные операции — прежде всего, отменять заказ.
|
||||
|
||||
Таким образом, наш промежуточный уровень абстракции должен:
|
||||
* скрывать параметры создания задания на приготовление напитка — их вычисление должно производиться в нашем коде;
|
||||
* обобщать состояние исполнения в единый набор статусов, не зависящий от физического уровня приготовления напитка;
|
||||
* связывать параметры заказа (его уникальный идентификатор) с конкретным процессом приготовления напитка;
|
||||
* предоставлять атомарный интерфейс логических операций, скрывая за фасадом различия в физической имплементации этих операций.
|
||||
|
||||
Первая из проблем, которую мы видим на данном этапе — это отсутствие на физическом уровне машин второго типа самого понятия «заказ» или «напиток»: машина выполняет какой-то набор операций
|
||||
|
||||
Выделение уровней абстракции — прежде всего _логическая_ процедура: как мы объясняем себе и разработчику, из чего состоит наш API. Мы могли бы просто ограничиться выделением секции `task` в ответе `GET /orders/{id}` — или вовсе сказать, что `task` — это просто четверка полей (`ready`, `volume_requested`, `volume_prepared`, `readiness_policy`) и есть. **Абстрагируемая дистанция между сущностями существует объективно**, каким бы образом мы ни написали конкретные интерфейсы. Наша задача состоит только лишь в том, чтобы эта дистанция была разделена на уровни _явно_. Чем неявнее разведены уровни абстракции (или хуже того, перемешаны) уровни абстракции, тем сложнее будет разобраться в вашем API и тем хуже будет написан использующий его код.
|
||||
|
||||
NB: важно заметить, что с дальнейшей проработкой уровень исполнения, скорее всего, сам должен будет разделиться на два и более уровня, т.к. «задача» по сути — просто сущность-зонтик, связывающая в рамках заказа несколько высокоуровневых сущностей. Идея определения параметров кофе-машины на этапе создания заказов не очень удобна, да и до манипуляции командами кофе-машины и состоянием сенсоров всё ещё далеко с точки зрения абстрагирования. Но мы пока оставим в таком виде, для удобства дальнейшего изложения.
|
||||
|
||||
@ -105,7 +214,7 @@ NB: важно заметить, что с дальнейшей проработ
|
||||
|
||||
Важным следствием этого принципа является то, что информацию о готовности заказа нам придётся «прорастить» через все уровни абстракции:
|
||||
|
||||
1. На физическом уровне мы будем оперировать состоянием кофе-машины, её сенсоров;
|
||||
1. На физическом уровне мы будем оперировать состоянием кофе-машины, её сенсоров; с точки зрения физических сенсоров нет никакой «готовности заказа», есть только состояние выполнения команд;
|
||||
2. На уровне исполнения статус готовности означает, что состояние сенсоров приведено к эталонному (в случае политики "check_volume" — что налит именно тот объём кофе, который был запрошен);
|
||||
3. На пользовательском уровне статус готовности заказа означает, что все ассоциированные задачи выполнены.
|
||||
|
||||
@ -115,8 +224,8 @@ NB: важно заметить, что с дальнейшей проработ
|
||||
|
||||
* `POST /orders/{id}/cancel` работает с высокоуровневыми данными о заказе:
|
||||
* проверяет авторизацию, т.е. имеет ли право этот пользователь отменять этот заказ;
|
||||
* решает денежные вопросы — нужно ли делать рефанд
|
||||
* находит все незавершённые задачи и отменяет их
|
||||
* решает денежные вопросы — нужно ли делать рефанд;
|
||||
* находит все незавершённые задачи и отменяет их;
|
||||
* `POST /tasks/{id}/cancel` работает с исполнением заказа:
|
||||
* определяет, возможно ли физически отменить исполнение, есть ли такая функция у кофе-машины;
|
||||
* генерирует последовательность действий отмены (возможно, не только непосредственно для самой машины — вполне вероятно, необходимо будет поставить задание сотруднику кофейни утилизировать невостребованный напиток);
|
||||
@ -128,10 +237,6 @@ NB: важно заметить, что с дальнейшей проработ
|
||||
`{"status":"canceled","operation_state":{"status":"canceling","operations":[…]}`
|
||||
— с т.з. высокоуровневого кода задача завершена (`canceled`), но с точки зрения низкоуровневого кода список исполняемых операций непуст, т.е. задача продолжает работать.
|
||||
|
||||
NB: так как `task` связывает два разных уровня абстракции, то и статусов у неё два: внешний `canceled` и внутренний `canceling`. Мы могли бы опустить второй статус и предложить ориентироваться на содержание `operations`, но это вновь (а) неявно, (б) предполагает необходимость разбираться в более низкоуровневом интерфейсе `operation_state`, что, быть может, разработчику вовсе и не нужно.
|
||||
**NB**: так как `task` связывает два разных уровня абстракции, то и статусов у неё два: внешний `canceled` и внутренний `canceling`. Мы могли бы опустить второй статус и предложить ориентироваться на содержание `operations`, но это (а) неявно, (б) предполагает необходимость разбираться в более низкоуровневом интерфейсе `operation_state`, что, быть может, разработчику вовсе и не нужно.
|
||||
|
||||
Может показаться, что соблюдение правила изоляции уровней абстракции является избыточным и заставляет усложнять интерфейс. И это в действительности так: важно понимать, что никакая гибкость, логичность, читабельность и расширяемость не бывает бесплатной. Можно построить API так, чтобы оно выполняло свою функцию с минимальными накладными расходами, по сути — дать интерфейс к микроконтроллерам кофе-машины. Однако пользоваться им будет крайне неудобно, и расширяемость такого API будет нулевой.
|
||||
|
||||
Дублирование функций на каждом уровне абстракций позволяет добиться важной вещи: возможности сменить нижележащие уровни без необходимости переписывать верхнеуровневый код. Мы можем добавить другие виды кофе-машин с принципиально другими физическими способами определения готовности напитка, и наш метод `GET /orders?order_id={id}` продолжит работать, как работал.
|
||||
|
||||
Да, код, который работал с физическим уровнем, придётся переписать. Но, во-первых, это неизбежно: изменение принципов работы физического уровня автоматически означает необходимость переписать код. Во-вторых, такое разделение ставит перед нами четкий вопрос: до какого момента API должно предоставлять публичный доступ? Стоило ли предоставлять пользователю методы физического уровня?
|
@ -26,13 +26,9 @@
|
||||
|
||||
#### Декомпозиция интерфейсов
|
||||
|
||||
// TODO
|
||||
На этапе разделения уровней абстракции мы упомянули, что одна из целей такого разделения — добиться возможности безболезненно сменить нижележащие уровни абстракции при добавлении новых технологий или изменении нижележащих протоколов. Декомпозиция интерфейсов — основной инструмент, позволяющий добиться такой гибкости.
|
||||
|
||||
NB. Во многих языках программирования нет поддержки интерфейсов, абстрактных классов и/или множественного наследования. Однако выделять интерфейсы нам это не мешает, поскольку мы всегда можем "договориться", что объект source имеет право пользоваться только вот этим набором свойств и методов. Конечно, контролировать соблюдение этой договоренности в достаточно развесистом API довольно сложно, но, поверьте автору, вполне возможно, тем более, что тестами и/или статическим анализом кода соблюдение договоренностей об интерфейсах можно проверить почти всегда.
|
||||
|
||||
Разделение контекстов - не единственная причина, по которой выделение интерфейсов критически важно при проектировании API. Предъявление к входящим параметрам требования только удовлетворять интерфейсу существенно упрощает создание альтернативных реализаций ваших объектов, в том числе в целях тестирования. Теперь чтобы протестировать объект source достаточно написать mock на IGeoContext, а не весь класс map целиком. Аналогично, если мы захотим использовать наши оверлеи для показа их, скажем, в качестве какой-то инфографики или на абстрактном плане местности, нам не придётся переделывать для этого класс Map - достаточно будет альтернативной реализации IGraphicalContext.
|
||||
|
||||
При выделении интерфейсов важно также понимать, что интерфейс, в отличие от его реализации, должен быть минимально достаточным и не должен включать в себя вспомогательные методы. Например, если класс map имеет как метод для получения всей области картографирования в виде четырехугольника getBBox, так и методы получения каждого из углов по отдельности - getLeftBottom, getRightTop, например, — то интерфейс IGeoContext должен содержать что-то одно. Нет никакого смысла загромождать интерфейс альтернативными реализациями одной и той же функциональности — это затрудняет чтение и усложняет написание собственных реализаций. Если только нет каких-то показаний с точки зрения производительности, следует отдать предпочтение максимально общему методу - в нашем случае getBBox.
|
||||
На этом этапе нам предстоит отделить частное от общего: понять, какие свойства сущностей фиксированы в нашей модели, а какие будут зависеть от реализации. В нашем кофе-примере таким сильно зависящим от имплементации объектом, очевидно, будут сами кофе-машины. Попробуем ещё немного развить наше API: каким образом нам нужно описать модели кофе-машин для того, чтобы предоставить интерфейсы для работы с задачами заказа?
|
||||
|
||||
#### Интерфейсы как универсальный паттерн
|
||||
|
||||
@ -67,4 +63,5 @@ NB. Во многих языках программирования нет по
|
||||
|
||||
Дерево информационных контекстов (какой объект обладает какой информацией, и кто является транслятором из одного контекста в другой), по сути, представляет собой «срез» нашего дерева иерархии интерфейсов; выделение такого среза позволяет проще и удобнее удерживать в голове всю архитектуру проекта.
|
||||
|
||||
// TODO: простые вещи делаются просто без бойлерплейта
|
||||
// TODO
|
||||
// Хелперы, бойлерплейт
|
@ -20,19 +20,19 @@
|
||||
* плохо:
|
||||
```
|
||||
GET /orders/cancellation
|
||||
отменяет заказ
|
||||
// отменяет заказ
|
||||
```
|
||||
Неочевидно, что достаточно просто обращения к сущности `cancellation` (что это?), тем более немодифицирующим методом `GET`, чтобы отменить заказ;
|
||||
|
||||
Хорошо:
|
||||
```
|
||||
POST /orders/cancel
|
||||
отменяет заказ
|
||||
POST /orders/cancel
|
||||
отменяет заказ
|
||||
```
|
||||
* плохо:
|
||||
* плохо:
|
||||
```
|
||||
GET /orders/statistics
|
||||
Возвращает агрегированную статистику заказов за всё время
|
||||
// Возвращает агрегированную статистику заказов за всё время
|
||||
```
|
||||
Даже если операция немодифицирующая, но вычислительно дорогая — следует об этом явно индицировать, особенно если вычислительные ресурсы тарифицируются для пользователя; тем более не стоит подбирать значения по умолчанию так, чтобы вызов операции без параметров максимально расходовал ресурсы.
|
||||
|
||||
@ -40,11 +40,17 @@
|
||||
```
|
||||
POST /orders/statistics/aggregate
|
||||
{ "start_date", "end_date" }
|
||||
Возвращает агрегированную статистику заказов за указанный период
|
||||
// Возвращает агрегированную статистику заказов за указанный период
|
||||
```
|
||||
|
||||
**Стремитесь к тому, чтобы из сигнатуры функции было абсолютно ясно, что она делает, что принимает на вход и что возвращает**. Вообще, при прочтении кода, работающего с вашим API, должно быть сразу понятно, что, собственно, он делает — без подглядывания в документацию.
|
||||
|
||||
Два важных следствия:
|
||||
|
||||
**1.1.** Если операция модифицирующая, это должно быть очевидно из сигнатуры. В частности, не может быть модифицирующих операций за `GET`.
|
||||
|
||||
**1.2.** Если операция асинхронная, это должно быть очевидно из сигнатуры, **либо** должна существовать конвенция именования, позволяющая отличаться синхронные операции от асинхронных.
|
||||
|
||||
#### 2. Сущности должны именоваться конкретно
|
||||
|
||||
Избегайте слов-«амёб» без определённой семантики, таких как get, apply, make. Сущности должны именоваться конкретно:
|
||||
@ -59,13 +65,11 @@
|
||||
* плохо:
|
||||
```
|
||||
strpbrk (str1, str2)
|
||||
возвращает положение первого вхождения в строку str2 любого символа из строки str2
|
||||
// возвращает положение первого вхождения в строку str2
|
||||
// любого символа из строки str2
|
||||
```
|
||||
Возможно, автору этого API казалось, что аббревиатура `pbrk` что-то значит для читателя, но он явно ошибся. К тому же, невозможно сходу понять, какая из строк `str1`, `str2` является набором символов для поиска.
|
||||
Хорошо:
|
||||
```
|
||||
str_search_for_characters(lookup_character_set, str)
|
||||
```
|
||||
Хорошо: `str_search_for_characters(lookup_character_set, str)`
|
||||
Однако необходимость существования такого метода вообще вызывает сомнения, достаточно было бы иметь удобную функцию поиска подстроки с нужными параметрами. Аналогично сокращение `string` до `str` выглядит совершенно бессмысленным, но, увы, является устоявшимся для большого количества предметных областей.
|
||||
|
||||
#### 4. Тип поля должен быть ясен из его названия
|
||||
@ -80,7 +84,16 @@
|
||||
* плохо: `"task.status": true` — неочевидно, что статус бинарен, плюс такое API будет нерасширяемым;
|
||||
хорошо: `"task.is_finished": true`
|
||||
|
||||
Отдельно следует оговорить, что на разных платформах эти правила следует дополнить по-своему с учетом специфики first-class citizen-типов. Например, объекты типа `Date`, если таковые имеются, разумно индицировать с помощью, например, постфикса `_at` (`created_at`, `occurred_at`, etc).
|
||||
Отдельно следует оговорить, что на разных платформах эти правила следует дополнить по-своему с учетом специфики first-class citizen-типов. Например, объекты типа `Date`, если таковые имеются, разумно индицировать с помощью, например, постфикса `_at` (`created_at`, `occurred_at`, etc) или `_date`.
|
||||
|
||||
Если наименование сущности само по себе является каким-либо термином, способным смутить разработчика, лучше добавить лишний префикс или постфикс, чтобы избежать непонимания.
|
||||
* Плохо:
|
||||
```
|
||||
GET /coffee-machines/functions
|
||||
// Возвращает список встроенных функций кофе-машины
|
||||
```
|
||||
Слово "functions" многозначное; может означать и встроенные функции, и написанный код, и состояние (функционирует-не функционирует).
|
||||
Хорошо: `GET /coffee-machines/builtin-functions-list`
|
||||
|
||||
#### 5. Подобные сущности должны называться подобно и вести себя подобным образом
|
||||
|
||||
@ -90,11 +103,13 @@
|
||||
* плохо:
|
||||
```
|
||||
strpos(haystack, needle)
|
||||
Находит первую позицию позицию строки `needle` внутри строки `haystack`
|
||||
// Находит первую позицию позицию строки `needle`
|
||||
// внутри строки `haystack`
|
||||
```
|
||||
```
|
||||
str_replace(needle, replace, haystack)
|
||||
Находит и заменяет все вхождения строки `needle` внутри строки `haystack` на строку `replace`
|
||||
// Находит и заменяет все вхождения строки `needle`
|
||||
// внутри строки `haystack` на строку `replace`
|
||||
```
|
||||
Здесь нарушены сразу несколько правил:
|
||||
* написание неконсистентно в части знака подчеркивания;
|
||||
@ -123,9 +138,10 @@
|
||||
|
||||
Также следует отметить, что в некоторых областях ситуация со стандартами настолько плоха, что как ни сделай — кто-то останется недовольным. Классический пример такого рода — порядок геокоординат ("широта-долгота" против "долгота-широта"). Здесь, увы, вам остаётся только смириться и проявлять выдержку при нападках на ваше API.
|
||||
|
||||
#### 7. Сохранение точности дробных чисел
|
||||
// TODO: блокнот душевного спокойствия
|
||||
|
||||
#### 7. Сохраняйте точность дробных чисел
|
||||
|
||||
Там, где это позволено протоколом, дробные числа с фиксированной запятой — такие, как денежные суммы, например — должны передаваться в виде специально предназначенных для этого объектов, например, Decimal или аналогичных.
|
||||
|
||||
Если в протоколе нет Decimal-типов (в частности, в JSON нет чисел с фиксированной запятой), следует либо привести к целому, либо использовать строковый тип.
|
||||
|
||||
|
@ -22,11 +22,21 @@ body {
|
||||
padding-left: 92px;
|
||||
}
|
||||
|
||||
code {
|
||||
code, pre {
|
||||
font-family: Inconsolata, sans-serif;
|
||||
font-size: 12pt;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 12pt 0;
|
||||
padding: 12pt;
|
||||
border-radius: .25em;
|
||||
border-top: 1px solid rgba(0,0,0,.45);
|
||||
border-left: 1px solid rgba(0,0,0,.45);
|
||||
box-shadow: .1em .1em .1em rgba(0,0,0,.45);
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
.page-break {
|
||||
page-break-after: always;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user