1
0
mirror of https://github.com/twirl/The-API-Book.git synced 2025-01-05 10:20:22 +02:00

epub version added

This commit is contained in:
Sergey Konstantinov 2020-12-08 15:43:01 +03:00
parent ffc186c2f4
commit 0e3fc37211
7 changed files with 287 additions and 129 deletions

118
build.js
View File

@ -1,23 +1,38 @@
const puppeteer = require('puppeteer');
const fs = require('fs');
const mdHtml = new (require('showdown').Converter)();
const path = require('path');
const langsToBuild = process.argv[2] &&
process.argv[2].split(',').map((s) => s.trim()) ||
['ru', 'en'];
const targets = (process.argv[3] &&
process.argv[3].split(',') ||
['html', 'pdf', 'epub']
).reduce((targets, arg) => {
targets[arg.trim()] = true;
return targets;
}, {});
const l10n = {
en: {
title: 'Sergey Konstantinov. The API',
title: 'The API',
author: 'Sergey Konstantinov',
chapter: 'Chapter'
chapter: 'Chapter',
toc: 'Table of Contents',
frontPage: 'Front Page'
},
ru: {
title: 'Сергей Константинов. API',
title: 'API',
author: 'Сергей Константинов',
chapter: 'Глава'
chapter: 'Глава',
toc: 'Содержание',
frontPage: 'Титульный лист'
}
};
const builders = require('./builders');
buildDocs(langsToBuild, l10n).then(() => {
buildDocs(langsToBuild, targets, l10n).then(() => {
console.log('Done!');
process.exit(0);
}, (e) => {
@ -25,77 +40,88 @@ buildDocs(langsToBuild, l10n).then(() => {
process.exit(255);
});
function buildDocs (langsToBuild, l10n) {
console.log(`Building in following languages: ${langsToBuild.join(', ')}`);
function buildDocs (langsToBuild, targets, l10n) {
console.log(`Building in following languages: ${
langsToBuild.join(', ')
}, targets: ${
Object.keys(targets).join(', ')
}`);
return Promise.all(
langsToBuild.map((lang) => buildDoc(lang, l10n[lang]))
langsToBuild.map((lang) => buildDoc(lang, targets, l10n[lang]))
);
}
function buildDoc (lang, l10n) {
const content = getParts({
function buildDoc (lang, targets, l10n) {
const structure = getStructure({
path: `./src/${lang}/clean-copy/`,
l10n,
pageBreak:'<div class="page-break"></div>'
}).join('');
});
const htmlContent = [
structure.frontPage,
...structure.sections
.map((section) => section.chapters.reduce((content, chapter) => {
if (chapter.title) {
content.push(`<h3>${chapter.title}</h3>`);
}
content.push(chapter.content);
return content;
}, [section.title ? `<h2>${section.title}</h2>` : '']).join(''))
].join('\n');
const html = `<html><head>
<meta charset="utf-8"/>
<title>${l10n.title}</title>
<title>${l10n.author}. ${l10n.title}</title>
<meta name="author" content="${l10n.author}"/>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=PT+Serif&amp;family=PT+Sans&amp;family=Inconsolata"/>
<style>${fs.readFileSync('src/style.css', 'utf-8')}</style>
</head><body>
<article>${content}</article>
<article>${htmlContent}</article>
</body></html>`;
fs.writeFileSync(`./docs/API.${lang}.html`, html);
return buildPdf(`./docs/API.${lang}.pdf`, html);
return Promise.all(['html', 'pdf', 'epub'].map((target) => {
return targets[target] ? builders[target]({
lang,
structure,
html,
l10n,
path: path.join(__dirname, 'docs', `API.${lang}.${target}`)
}) : Promise.resolve();
}));
}
function getParts ({ path, l10n: { chapter }, pageBreak}) {
const parts = [
fs.readFileSync(`${path}intro.html`, 'utf-8') + pageBreak
];
function getStructure ({ path, l10n: { chapter }, pageBreak}) {
const structure = {
frontPage: fs.readFileSync(`${path}intro.html`, 'utf-8') + pageBreak,
sections: []
};
let counter = 1;
fs.readdirSync(path)
.filter((p) => fs.statSync(`${path}${p}`).isDirectory())
.sort()
.forEach((dir) => {
const name = dir.split('-')[1];
parts.push(`<h2>${name}</h2>`);
const section = {
title: name,
chapters: []
}
const subdir = `${path}${dir}/`;
fs.readdirSync(subdir)
.filter((p) => fs.statSync(`${subdir}${p}`).isFile() && p.indexOf('.md') == p.length - 3)
.sort()
.forEach((file) => {
const md = fs.readFileSync(`${subdir}${file}`, 'utf-8');
parts.push(
mdHtml.makeHtml(
md.trim()
.replace(/^### /, `### ${chapter} ${counter++}. `)
) + pageBreak
);
const md = fs.readFileSync(`${subdir}${file}`, 'utf-8').trim();
const [ title, ...paragraphs ] = md.split(/\r?\n/);
section.chapters.push({
title: title.replace(/^### /, `${chapter} ${counter++}. `),
content: mdHtml.makeHtml(paragraphs.join('\n')) + pageBreak
});
});
structure.sections.push(section);
});
return parts;
}
async function buildPdf (path, html) {
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.setContent(html, {
waitUntil: 'load'
});
const pdf = await page.pdf({
path,
preferCSSPageSize: true,
printBackground: true
});
await browser.close();
return structure;
}

60
builders.js Normal file
View File

@ -0,0 +1,60 @@
const puppeteer = require('puppeteer');
const fs = require('fs');
const Epub = require('epub-gen');
module.exports = {
html: function ({ html, path }) {
return new Promise((resolve, reject) => {
fs.writeFile(path, html, (e) => {
if (e) {
reject(e);
} else {
resolve();
}
});
});
},
pdf: async function ({ path, html }) {
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.setContent(html, {
waitUntil: 'load'
});
const pdf = await page.pdf({
path,
preferCSSPageSize: true,
printBackground: true
});
await browser.close();
},
epub: function ({ lang, l10n, structure, path}) {
const epubData = {
title: l10n.title,
author: l10n.author,
tocTitle: l10n.toc,
appendChapterTitles: false,
content: structure.sections.reduce((content, section) => {
content.push({
title: section.title.toUpperCase(),
data: `<h2>${section.title}</h2>`
});
section.chapters.forEach((chapter) => {
content.push({
title: chapter.title,
data: `<h3>${chapter.title}</h3>\n${chapter.content}`
});
});
return content;
}, [{
title: l10n.frontPage,
data: structure.frontPage,
beforeToc: true
}]),
lang
};
const epub = new Epub(epubData, path);
return epub.promise;
}
};

BIN
docs/API.ru.epub Normal file

Binary file not shown.

View File

@ -3,28 +3,18 @@
<title>Сергей Константинов. API</title>
<meta name="author" content="Сергей Константинов"/>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=PT+Serif&amp;family=PT+Sans&amp;family=Inconsolata"/>
<style>body {
<style>html {
width: 100%;
margin: 0;
padding: 0;
}
body {
font-family: 'PT Serif';
font-size: 14pt;
text-align: justify;
}
@media screen {
body {
margin: 20px auto;
max-width: 60%;
}
}
@media print {
h1 {
margin: 4.5in 0 5.2in 0;
}
body {
font-size: 20pt;
}
}
.cc-by-nc {
background: transparent url(https://i.creativecommons.org/l/by-nc/4.0/88x31.png) 0 5px no-repeat;
padding-left: 92px;
@ -46,6 +36,7 @@ pre {
border-left: 1px solid rgba(0,0,0,.45);
box-shadow: .1em .1em .1em rgba(0,0,0,.45);
page-break-inside: avoid;
overflow-x: auto;
}
pre code {
@ -73,10 +64,12 @@ h1 {
h2 {
font-size: 160%;
text-transform: uppercase;
}
h3 {
font-size: 140%;
font-variant: small-caps;
}
h4, h5 {
@ -92,19 +85,53 @@ h4, h5 {
--main-font: 'PT Serif';
--alt-font: 'PT Serif';
--code-font: Inconsolata;
}</style>
}
@media screen {
body {
margin: 2em auto;
max-width: 60%;
}
}
@media print {
h1 {
margin: 4in 0 4in 0;
}
body {
font-size: 14pt;
}
}
@media screen and (max-width: 1000px) {
body {
padding: 2em;
margin: 0;
max-width: none;
text-align: left;
}
code {
font-size: 11pt;
}
pre {
margin: 0;
padding: 0.2em;
}
}
</style>
</head><body>
<article><h1>Сергей Константинов<br/>API</h1>
<p class="cc-by-nc">Это произведение доступно по <a href="http://creativecommons.org/licenses/by-nc/4.0/">лицензии Creative Commons «Attribution-NonCommercial» («Атрибуция — Некоммерческое использование») 4.0 Всемирная</a>.</p>
<div class="page-break"></div><h2>Введение</h2><h3 id="1">Глава 1. О структуре этой книги</h3>
<p>Книга, которую вы держите в руках, состоит из введения и трех больших разделов.</p>
<div class="page-break"></div>
<h2>Введение</h2><h3>Глава 1. О структуре этой книги</h3><p>Книга, которую вы держите в руках, состоит из введения и трех больших разделов.</p>
<p>В первом разделе мы поговорим о проектировании API на стадии разработки концепции — как грамотно выстроить архитектуру, от крупноблочного планирования до конечных интерфейсов.</p>
<p>Второй раздел будет посвящён жизненному циклу API — как интерфейсы эволюционируют со временем и как развивать продукт так, чтобы отвечать потребностям пользователей.</p>
<p>Наконец, третий раздел будет касаться больше не-разработческих сторон жизни API — поддержки, маркетинга, работы с комьюнити.</p>
<p>Первые два будут интересны скорее разработчикам, третий — и разработчикам, и менеджерам. При этом мы настаиваем, что как раз третий раздел — самый важный для разработчика API. Ввиду того, что API — продукт для разработчиков, перекладывать ответственность за его развитие и поддержку на не-разработчиков неправильно: никто кроме вас самих не понимает так хорошо продуктовые свойства вашего API.</p>
<p>На этом переходим к делу.</p><div class="page-break"></div><h3 id="2api">Глава 2. Определение API</h3>
<p>Прежде чем говорить о разработке API, необходимо для начала договориться о том, что же такое API. Энциклопедия скажет нам, что API — это программный интерфейс приложений. Это точное определение, но бессмысленное. Примерно как определение человека по Платону: «двуногое без перьев» — определение точное, но никоим образом не дающее нам представление о том, чем на самом деле человек примечателен. (Да и не очень-то и точное: Диоген Синопский как-то ощипал петуха и заявил, что это человек Платона; пришлось дополнить определение уточнением «с плоскими ногтями».)</p>
<p>На этом переходим к делу.</p><div class="page-break"></div><h3>Глава 2. Определение API</h3><p>Прежде чем говорить о разработке API, необходимо для начала договориться о том, что же такое API. Энциклопедия скажет нам, что API — это программный интерфейс приложений. Это точное определение, но бессмысленное. Примерно как определение человека по Платону: «двуногое без перьев» — определение точное, но никоим образом не дающее нам представление о том, чем на самом деле человек примечателен. (Да и не очень-то и точное: Диоген Синопский как-то ощипал петуха и заявил, что это человек Платона; пришлось дополнить определение уточнением «с плоскими ногтями».)</p>
<p>Что же такое API по смыслу, а не по формальному определению?</p>
<p>Вероятно, вы сейчас читаете эту книгу посредством браузера. Чтобы браузер смог отобразить эту страничку, должны корректно отработать: разбор URL согласно спецификации; служба DNS; соединение по протоколу TLS; передача данных по протоколу HTTP; разбор HTML-документа; разбор CSS-документа; корректный рендеринг HTML+CSS.</p>
<p>Но это только верхушка айсберга. Для работы HTTP необходима корректная работа всего сетевого стека, который состоит из 4-5, а то и больше, протоколов разных уровней. Разбор HTML-документа производится согласно сотням различных спецификаций. Рендеринг документа обращается к нижележащему API операционной системы, а также напрямую к API видеокарты. И так далее, и тому подобное — вплоть до того, что наборы команд современных CISC-процессоров имплементируются поверх API микрокоманд.</p>
@ -117,8 +144,7 @@ h4, h5 {
</ul>
<p>Отличие римского виадука от хорошего API состоит лишь в том, что API предлагает <em>программный</em> контракт. Для связывания двух областей необходимо написать некоторый <em>код</em>. Цель этой книги — помочь вам разработать API, так же хорошо выполняющий свою задачу, как и римский виадук.</p>
<p>Виадук также хорошо иллюстрирует другую проблему разработки API: вашими пользователями являются инженеры. Вы не поставляете воду напрямую потребителю: к вашей инженерной мысли подключаются заказчики путём пристройки к ней каких-то своих инженерных конструкций. С одной стороны, вы можете обеспечить водой гораздо больше людей, нежели если бы вы сами подводили трубы к каждому крану. С другой — качество инженерных решений заказчика вы не может контролировать, и проблемы с водой, вызванные некомпетентностью подрядчика, неизбежно будут валить на вас.</p>
<p>Поэтому проектирование API налагает на вас несколько большую ответственность. <strong>API является как мультипликатором ваших возможностей, так и мультипликатором ваших ошибок</strong>.</p><div class="page-break"></div><h3 id="3api">Глава 3. Критерии качества API</h3>
<p>Прежде чем излагать рекомендации, нам следует определиться с тем, что мы считаем «хорошим» API, и какую пользу мы получаем от того, что наше API «хорошее».</p>
<p>Поэтому проектирование API налагает на вас несколько большую ответственность. <strong>API является как мультипликатором ваших возможностей, так и мультипликатором ваших ошибок</strong>.</p><div class="page-break"></div><h3>Глава 3. Критерии качества API</h3><p>Прежде чем излагать рекомендации, нам следует определиться с тем, что мы считаем «хорошим» API, и какую пользу мы получаем от того, что наше API «хорошее».</p>
<p>Начнём со второго вопроса. Очевидно, «хорошесть» API определяется в первую очередь тем, насколько он помогает разработчикам решать стоящие перед ними задачи. (Можно резонно возразить, что решение задач, стоящих перед разработчиками, не обязательно влечёт за собой выполнение целей, которые мы ставим перед собой, предлагая разработчикам API. Однако манипуляция общественным мнением не входит в область интересов автора этой книги: здесь и далее предполагается, что API существует в первую очередь для того, чтобы разработчики решали с его помощью свои задачи, а не ради каких-то не декларируемых явно целей.)</p>
<p>Как же дизайн API может помочь разработчику? Очень просто: API должно решать задачи <em>максимально удобно и понятно</em>. Путь разработчика от формулирования своей задачи до написания работающего кода должен быть максимально коротким. Это, в том числе, означает, что:</p>
<ul>
@ -128,14 +154,12 @@ h4, h5 {
</ul>
<p>Однако статическое удобство и понятность API — это простая часть. В конце концов, никто не стремится специально сделать API нелогичным и нечитаемым — всегда при разработке мы начинаем с каких-то понятных базовых концепций. При минимальном опыте проектирования сложно сделать ядро API, не удовлетворяющее критериям очевидности, читаемости и консистентности.</p>
<p>Проблемы начинаются, когда мы начинаем API развивать. Добавление новой функциональности рано или поздно приводит к тому, что некогда простое и понятное API становится наслоением разных концепций, а попытки сохранить обратную совместимость приводят к нелогичным, неочевидным и попросту плохим решениям. Отчасти это связано так же и с тем, что невозможно обладать полным знанием о будущем: ваше понимание о «правильном» API тоже будет меняться со временем, как в объективной части (какие задачи решает API и как лучше это сделать), так и в субъективной — что такое очевидность, читабельность и консистентность для вашего API.</p>
<p>Принципы, которые мы будем излагать ниже, во многом ориентированы именно на то, чтобы API правильно развивалось во времени и не превращалось в нагромождение разнородных неконсистентных интерфейсов. Важно понимать, что такой подход тоже не бесплатен: необходимость держать в голове варианты развития событий и закладывать возможность изменений в API означает избыточность интерфейсов и возможно излишнее абстрагирование. И то, и другое, помимо прочего, усложняет и работу программиста, пользующегося вашим API. <strong>Закладывание перспектив «на будущее» имеет смысл, только если это будущее у API есть, иначе это попросту оверинжиниринг</strong>.</p><div class="page-break"></div><h3 id="4">Глава 4. Обратная совместимость</h3>
<p>Обратная совместимость — это некоторая <em>временна́я</em> характеристика качества вашего API. Именно необходимость поддержания обратной совместимости отличает разработку API от разработки программного обеспечения вообще.</p>
<p>Принципы, которые мы будем излагать ниже, во многом ориентированы именно на то, чтобы API правильно развивалось во времени и не превращалось в нагромождение разнородных неконсистентных интерфейсов. Важно понимать, что такой подход тоже не бесплатен: необходимость держать в голове варианты развития событий и закладывать возможность изменений в API означает избыточность интерфейсов и возможно излишнее абстрагирование. И то, и другое, помимо прочего, усложняет и работу программиста, пользующегося вашим API. <strong>Закладывание перспектив «на будущее» имеет смысл, только если это будущее у API есть, иначе это попросту оверинжиниринг</strong>.</p><div class="page-break"></div><h3>Глава 4. Обратная совместимость</h3><p>Обратная совместимость — это некоторая <em>временна́я</em> характеристика качества вашего API. Именно необходимость поддержания обратной совместимости отличает разработку API от разработки программного обеспечения вообще.</p>
<p>Разумеется, обратная совместимость не абсолютна. В некоторых предметных областях выпуск новых обратно несовместимых версий API является вполне рутинной процедурой. Тем не менее, каждый раз, когда выпускается новая обратно несовместимая версия API, всем разработчикам приходится инвестировать какое-то ненулевое количество усилий, чтобы адаптировать свой код к новой версии. В этом плане выпуск новых версий API является некоторого рода «налогом» на потребителей — им нужно тратить вполне осязаемые деньги только для того, чтобы их продукт продолжал работать.</p>
<p>Конечно, крупные компании с прочным положением на рынке могут позволить себе такой налог взымать. Более того, они могут вводить какие-то санкции за отказ от перехода на новые версии API, вплоть до отключения приложений.</p>
<p>С нашей точки зрения, подобное поведение ничем не может быть оправдано. Избегайте скрытых налогов на своих пользователей. Если вы можете не ломать обратную совместимость — не ломайте её.</p>
<p>Да, безусловно, поддержка старых версий API — это тоже своего рода налог. Технологии меняются, и, как бы хорошо ни было спроектировано ваше API, всего предусмотреть невозможно. В какой-то момент ценой поддержки старых версий становится невозможность предоставлять новую функциональность и поддерживать новые платформы, и выпустить новую версию всё равно придётся. Однако вы по крайней мере сможете убедить своих потребителей в необходимости перехода.</p>
<p>Более подробно о жизненном цикле API и политиках выпуска новых версий будет рассказано в разделе II.</p><div class="page-break"></div><h3 id="5">Глава 5. О версионировании</h3>
<p>Здесь и далее мы будем придерживаться принципов версионирования <a href="https://semver.org/">semver</a>:</p>
<p>Более подробно о жизненном цикле API и политиках выпуска новых версий будет рассказано в разделе II.</p><div class="page-break"></div><h3>Глава 5. О версионировании</h3><p>Здесь и далее мы будем придерживаться принципов версионирования <a href="https://semver.org/">semver</a>:</p>
<ol>
<li>Версия API задаётся тремя цифрами, вида <code>1.2.3</code>.</li>
<li>Первая цифра (мажорная версия) увеличивается при обратно несовместимых изменениях в API.</li>
@ -143,8 +167,7 @@ h4, h5 {
<li>Третья цифра (патч) увеличивается при выпуске новых версий, содержащих только исправление ошибок.</li>
</ol>
<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>Более подробно о политиках версионирования будет рассказано в разделе II. В разделе I мы ограничимся лишь указанием версии API в формате <code>v1</code>, <code>v2</code>, etc.</p><div class="page-break"></div><h3>Глава 6. Условные обозначения и терминология</h3><p>Разработка программного обеспечения характеризуется, помимо прочего, существованием множества различных парадигм разработки, адепты которых зачастую настроены весьма воинственно по отношению к адептам других парадигм. Поэтому при написании этой книги мы намеренно избегаем слов «метод», «объект», «функция» и так далее, используя нейтральный термин «сущность». Под «сущностью» понимается некоторая атомарная единица функциональности — класс, метод, объект, монада, прототип (нужное подчеркнуть).</p>
<p>Для составных частей сущности, к сожалению, достаточно нейтрального термина нам придумать не удалось, поэтому мы используем слова «поля» и «методы».</p>
<p>Большинство примеров API в общих разделах будут даны в виде JSON-over-HTTP-эндпойтов. Это некоторая условность, которая помогает описать концепции, как нам кажется, максимально понятно. Вместо <code>GET /v1/orders</code> вполне может быть вызов метода <code>orders.get()</code>, локальный или удалённый; вместо JSON может быть любой другой формат данных. Смысл утверждений от этого не меняется.</p>
<p>Рассмотрим следующую запись:</p>
@ -167,7 +190,7 @@ Cache-Control: no-cache
</code></pre>
<p>Её следует читать так:</p>
<ul>
<li>выполняется POST-запрос к ресурсу <code>/v1/bucket/{id}/some-resource</code>, где <code>{id}</code> заменяется на некоторый идентификатор <code>bucket</code>-а (при отсутствии уточнений подстановки вида <code>{something}</code> следует относить к ближайшему термину слева);</li>
<li>клиент выполняет POST-запрос к ресурсу <code>/v1/bucket/{id}/some-resource</code>, где <code>{id}</code> заменяется на некоторый идентификатор <code>bucket</code>-а (при отсутствии уточнений подстановки вида <code>{something}</code> следует относить к ближайшему термину слева);</li>
<li>запрос сопровождается (помимо стандартных заголовков, которые мы опускаем) дополнительным заголовком <code>X-Idempotency-Token</code>;</li>
<li>фразы в угловых скобках (<code>&lt;токен идемпотентности&gt;</code>) описывают семантику значения сущности (поля, заголовка, параметра);</li>
<li>в качестве тела запроса передаётся JSON, содержащий поле <code>some_parameter</code> со значением <code>value</code> и ещё какие-то поля, которые для краткости опущены (что показано многоточием);</li>
@ -175,11 +198,12 @@ Cache-Control: no-cache
<li>в ответе также могут находиться дополнительные заголовки, на которые мы обращаем внимание;</li>
<li>телом ответа является JSON, состоящий из единственного поля <code>error_message</code>; отсутствие значения поля означает, что его значением является именно то, что в этом поле и ожидается — в данном случае какое-то сообщение об ошибке.</li>
</ul>
<p>Здесь термин «клиент» означает «приложение, установленное на устройстве пользователя, использующее рассматриваемое API». Приложение может быть как нативным, так и веб-приложением. Термины «агент» и «юзер-агент» являются синонимами термина «клиент».</p>
<p>Ответ (частично или целиком) и тело запроса могут быть опущены, если в контексте обсуждаемого вопроса их содержание не имеют значения.</p>
<p>Для упрощения возможна сокращенная запись вида: <code>POST /v1/bucket/{id}/some-resource</code> <code>{…,"some_parameter",…}</code><code>{ "operation_id" }</code>; тело запроса и/или ответа может опускаться аналогично полной записи.</p>
<p>Возможна сокращённая запись вида: <code>POST /v1/bucket/{id}/some-resource</code> <code>{…,"some_parameter",…}</code><code>{ "operation_id" }</code>; тело запроса и/или ответа может опускаться аналогично полной записи.</p>
<p>Чтобы сослаться на это описание будут использоваться выражения типа «метод <code>POST /v1/bucket/{id}/some-resource</code>» или, для простоты, «метод <code>some-resource</code>» или «метод <code>bucket/some-resource</code>» (если никаких других <code>some-resource</code> в контексте главы не упоминается и перепутать не с чем).</p>
<p>Помимо HTTP API-нотации мы будем активно использовать C-подобный псевдокод — точнее будет сказать, JavaScript или Python-подобный, поскольку нотации типов мы будем опускать. Мы предполагаем, что подобного рода императивные конструкции достаточно читабельны, и не будем здесь описывать грамматику подробно.</p><div class="page-break"></div><h2>I. Проектирование API</h2><h3 id="7api">Глава 7. Пирамида контекстов API</h3>
<p>Подход, который мы используем для проектирования, состоит из четырёх шагов:</p>
<p>Помимо HTTP API-нотации мы будем активно использовать C-подобный псевдокод — точнее будет сказать, JavaScript или Python-подобный, поскольку нотации типов мы будем опускать. Мы предполагаем, что подобного рода императивные конструкции достаточно читабельны, и не будем здесь описывать грамматику подробно.</p><div class="page-break"></div>
<h2>I. Проектирование API</h2><h3>Глава 7. Пирамида контекстов API</h3><p>Подход, который мы используем для проектирования, состоит из четырёх шагов:</p>
<ul>
<li>определение области применения;</li>
<li>разделение уровней абстракции;</li>
@ -188,8 +212,7 @@ Cache-Control: no-cache
</ul>
<p>Этот алгоритм строит API сверху вниз, от общих требований и сценариев использования до конкретной номенклатуры сущностей; фактически, двигаясь этим путем, вы получите на выходе готовое API — чем этот подход и ценен.</p>
<p>Может показаться, что наиболее полезные советы приведены в последнем разделе, однако это не так; цена ошибки, допущенной на разных уровнях весьма различна. Если исправить плохое именование довольно просто, то исправить неверное понимание того, зачем вообще нужно API, практически невозможно.</p>
<p><strong>NB</strong>. Здесь и далее мы будем рассматривать концепции разработки API на примере некоторого гипотетического API заказа кофе в городских кофейнях. На всякий случай сразу уточним, что пример является синтетическим; в реальной ситуации, если бы такой API пришлось проектировать, он, вероятно, был бы совсем не похож на наш выдуманный пример.</p><div class="page-break"></div><h3 id="8">Глава 8. Определение области применения</h3>
<p>Ключевой вопрос, который вы должны задать себе четыре раза, выглядит так: какую проблему мы решаем? Задать его следует четыре раза с ударением на каждом из четырёх слов.</p>
<p><strong>NB</strong>. Здесь и далее мы будем рассматривать концепции разработки API на примере некоторого гипотетического API заказа кофе в городских кофейнях. На всякий случай сразу уточним, что пример является синтетическим; в реальной ситуации, если бы такой API пришлось проектировать, он, вероятно, был бы совсем не похож на наш выдуманный пример.</p><div class="page-break"></div><h3>Глава 8. Определение области применения</h3><p>Ключевой вопрос, который вы должны задать себе четыре раза, выглядит так: какую проблему мы решаем? Задать его следует четыре раза с ударением на каждом из четырёх слов.</p>
<ol>
<li><p><em>Какую</em> проблему мы решаем? Можем ли мы чётко описать, в какой ситуации гипотетическим потребителям-разработчикам нужно наше API?</p></li>
<li><p>Какую <em>проблему</em> мы решаем? А мы правда уверены, что описанная выше ситуация — проблема? Действительно ли кто-то готов платить (в прямом и переносном смысле) за то, что ситуация будет как-то автоматизирована?</p></li>
@ -225,16 +248,15 @@ Cache-Control: no-cache
<ol>
<li>Предоставляем сервисам с большой пользовательской аудиторией API для того, чтобы их потребители могли максимально удобно для себя заказать кофе.</li>
<li>Для этого мы абстрагируем за нашим HTTP API доступ к «железу» и предоставим методы для выбора вида напитка и места его приготовления и для непосредственно исполнения заказа.</li>
</ol><div class="page-break"></div><h3 id="9">Глава 9. Разделение уровней абстракции</h3>
<p>«Разделите свой код на уровни абстракции» - пожалуй, самый общий совет для разработчиков программного обеспечения. Однако будет вовсе не преувеличением сказать, что изоляция уровней абстракции — самая сложная задача, стоящая перед разработчиком API.</p>
</ol><div class="page-break"></div><h3>Глава 9. Разделение уровней абстракции</h3><p>«Разделите свой код на уровни абстракции» - пожалуй, самый общий совет для разработчиков программного обеспечения. Однако будет вовсе не преувеличением сказать, что изоляция уровней абстракции — самая сложная задача, стоящая перед разработчиком API.</p>
<p>Прежде чем переходить к теории, следует чётко сформулировать, <em>зачем</em> нужны уровни абстракции и каких целей мы хотим достичь их выделением.</p>
<p>Вспомним, что программный продукт - это средство связи контекстов, средство преобразования терминов и операций одной предметной области в другую. Чем дальше друг от друга эти области отстоят - тем большее число промежуточных передаточных звеньев нам придётся ввести. Вернёмся к нашему примеру с кофейнями. Какие уровни сущностей мы видим?</p>
<ol>
<li>Мы готовим с помощью нашего API <em>заказ</em> — один или несколько стаканов кофе — и взымаем за это плату.</li>
<li>Каждый стакан кофе приготовлен по определённому рецепту, что подразумевает наличие разных ингредиентов и последовательности выполнения шагов приготовления.</li>
<li>Напиток готовится на конкретной физической кофе-машине, располагающейся в какой-то точке пространства.</li>
<li>Каждый стакан кофе приготовлен по определённому <em>рецепту</em>, что подразумевает наличие разных ингредиентов и последовательности выполнения шагов приготовления.</li>
<li>Напиток готовится на конкретной физической <em>кофе-машине</em>, располагающейся в какой-то точке пространства.</li>
</ol>
<p>Каждый из этих уровней задаёт некоторый срез нашего API, с которым будет работать потребитель. Выделяя иерархию абстракций мы прежде всего стремимся снизить связность различных сущностей нашего API. Это позволит нам добиться нескольких целей:</p>
<p>Каждый из этих уровней задаёт некоторый срез нашего API, с которым будет работать потребитель. Выделяя иерархию абстракций мы прежде всего стремимся снизить связность различных сущностей нашего API. Это позволит нам добиться нескольких целей.</p>
<ol>
<li><p>Упрощение работы разработчика и легкость обучения: в каждый момент времени разработчику достаточно будет оперировать только теми сущностями, которые нужны для решения его задачи; и наоборот, плохо выстроенная изоляция приводит к тому, что разработчику нужно держать в голове множество концепций, не имеющих прямого отношения к решаемой задаче.</p></li>
<li><p>Возможность поддерживать обратную совместимость; правильно подобранные уровни абстракции позволят нам в дальнейшем добавлять новую функциональность, не меняя интерфейс.</p></li>
@ -247,13 +269,14 @@ Cache-Control: no-cache
<pre><code> // размещает на указанной кофе-машине
// заказ на приготовление лунго
// и возвращает идентификатор заказа
POST /v1/coffee-machines/orders?machine_id={id}
POST /v1/coffee-machines/orders
{
"coffee_machine_id",
"recipe": "lungo"
}
</code></pre>
<pre><code> // возвращает состояние заказа
GET /v1/orders?order_id={id}
GET /v1/orders/{id}
</code></pre>
<p>И зададимся вопросом, каким образом разработчик определит, что заказ клиента готов. Допустим, мы сделаем так: добавим в рецепт лунго эталонный объём, а в состояние заказа — количество уже налитого кофе. Тогда разработчику нужно будет проверить совпадение этих двух цифр, чтобы убедиться, что кофе готов.</p>
<p>Такое решение выглядит интуитивно плохим, и это действительно так: оно нарушает все вышеперечисленные принципы.</p>
@ -261,8 +284,9 @@ Cache-Control: no-cache
<p><strong>Во-вторых</strong>, мы автоматически получаем проблемы, если захотим варьировать размер кофе. Допустим, в какой-то момент мы захотим представить пользователю выбор, сколько конкретно миллилитров лунго он желает. Тогда нам придётся проделать один из следующих трюков.</p>
<p>Вариант 1: мы фиксируем список допустимых объёмов и заводим фиктивные рецепты типа <code>/recipes/small-lungo</code>, <code>recipes/large-lungo</code>. Почему фиктивные? Потому что рецепт один и тот же, меняется только объём. Нам придётся либо тиражировать одинаковые рецепты, отличающиеся только объёмом, либо вводить какое-то «наследование» рецептов, чтобы можно было указать базовый рецепт и только переопределить объём.</p>
<p>Вариант 2: мы модифицируем интерфейс, объявляя объём кофе, указанный в рецепте, значением по умолчанию; при размещении заказа мы разрешаем указать объём, отличный от эталонного:</p>
<pre><code>POST /v1/coffee-machines/orders?machine_id={id}
<pre><code>POST /v1/coffee-machines/orders
{
"coffee_machine_id",
"recipe":"lungo",
"volume":"800ml"
}
@ -512,7 +536,7 @@ GET /sensors
<p>Вернёмся к нашему примеру. Каким образом будет работать операция получения статуса заказа? Для получения статуса будет выполнена следующая цепочка вызовов:</p>
<ul>
<li>пользователь вызовет метод <code>GET /v1/orders</code>;</li>
<li>обработчик <code>orders</code> выполнит операции своего уровня ответственности (проверку авторизации, в частности), найдёт идентификатор <code>program_run_id</code> и обратится к API программ <code>GET /v1/programs/{id}/runs/{program_run_id}</code>;</li>
<li>обработчик <code>orders</code> выполнит операции своего уровня ответственности (проверку авторизации, в частности), найдёт идентификатор <code>program_run_id</code> и обратится к API программ <code>runs/{program_run_id}</code>;</li>
<li>обработчик <code>runs</code> в свою очередь выполнит операции своего уровня (в частности, проверит тип API кофе-машины) и в зависимости от типа API пойдёт по одной из двух веток исполнения:<ul>
<li>либо вызовет <code>GET /execution/status</code> физического API кофе-машины, получит объём кофе и сличит с эталонным;</li>
<li>либо обратится к <code>GET /v1/runtimes/{runtime_id}</code>, получит <code>state.status</code> и преобразует его к статусу заказа;</li></ul></li>
@ -531,7 +555,7 @@ GET /sensors
<li>обработчик метода произведёт операции в своей зоне ответственности:<ul>
<li>проверит авторизацию;</li>
<li>решит денежные вопросы — нужно ли делать рефанд;</li>
<li>найдёт идентификатор <code>program_run_id</code> и обратится к <code>POST /v1/programs/{id}/runs/{program_run_id}/cancel</code>;</li></ul></li>
<li>найдёт идентификатор <code>program_run_id</code> и обратится к обработчику <code>runs/{program_run_id}/cancel</code>;</li></ul></li>
<li>обработчик <code>runs/cancel</code> произведёт операции своего уровня (в частности, установит тип API кофе-машины) и в зависимости от типа API пойдёт по одной из двух веток исполнения:<ul>
<li>либо вызовет <code>POST /execution/cancel</code> физического API кофе-машины;</li>
<li>либо вызовет <code>POST /v1/runtimes/{id}/terminate</code>;</li></ul></li>
@ -572,8 +596,7 @@ GET /sensors
<li><p>с одной стороны, в контексте заказа оказываются данные (объём кофе), «просочившиеся» откуда-то с физического уровня; тем самым, уровни абстракции непоправимо смешиваются без возможности их разделить;</p></li>
<li><p>с другой стороны, сам контекст заказа неполноценный: он не задаёт новых мета-переменных, которые отсутствуют на более низких уровнях абстракции (статус заказа), не инициализирует их и не предоставляет правил работы.</p>
<p>Более подробно о контекстах данных мы поговорим в разделе II. Здесь же ограничимся следующим выводом: потоки данных и их преобразования можно и нужно рассматривать как некоторый срез, который, с одной стороны, помогает нам правильно разделить уровни абстракции, а с другой — проверить, что наши теоретические построения действительно работают так, как нужно.</p></li>
</ul><div class="page-break"></div><h3 id="10">Глава 10. Разграничение областей ответственности</h3>
<p>Исходя из описанного в предыдущей главе, мы понимаем, что иерархия абстракций в нашем гипотетическом проекте должна выглядеть примерно так:</p>
</ul><div class="page-break"></div><h3>Глава 10. Разграничение областей ответственности</h3><p>Исходя из описанного в предыдущей главе, мы понимаем, что иерархия абстракций в нашем гипотетическом проекте должна выглядеть примерно так:</p>
<ul>
<li>пользовательский уровень (те сущности, с которыми непосредственно взаимодействует пользователь и сформулированы в понятных для него терминах; например, заказы и виды кофе);</li>
<li>уровень исполнения программ (те сущности, которые отвечают за преобразование заказа в машинные термины);</li>
@ -822,8 +845,7 @@ app.display(coffeeMachines);
</code></pre>
<p>Такое API читать и воспринимать гораздо удобнее, нежели сплошную простыню различных атрибутов. Более того, возможно, стоит на будущее сразу дополнительно сгруппировать, например, <code>place</code> и <code>route</code> в одну структуру <code>location</code>, или <code>offer</code> и <code>pricing</code> в одну более общую структуру.</p>
<p>Важно, что читабельность достигается не просто снижением количества сущностей на одном уровне. Декомпозиция должна производиться таким образом, чтобы разработчик при чтении интерфейса сразу понимал: так, вот здесь находится описание заведения, оно мне пока неинтересно и углубляться в эту ветку я пока не буду. Если перемешать данные, которые одновременно в моменте нужны для выполнения действия, по разным композитам — это только ухудшит читабельность, а не улучшит.</p>
<p>Дополнительно правильная декомпозиция поможет нам в решении задачи расширения и развития API, о чем мы поговорим в разделе II.</p><div class="page-break"></div><h3 id="11">Глава 11. Описание конечных интерфейсов</h3>
<p>Определив все сущности, их ответственность и отношения друг с другом, мы переходим непосредственно к разработке API: нам осталось прописать номенклатуру всех объектов, полей, методов и функций в деталях. В этой главе мы дадим сугубо практические советы, как сделать API удобным и понятным.</p>
<p>Дополнительно правильная декомпозиция поможет нам в решении задачи расширения и развития API, о чем мы поговорим в разделе II.</p><div class="page-break"></div><h3>Глава 11. Описание конечных интерфейсов</h3><p>Определив все сущности, их ответственность и отношения друг с другом, мы переходим непосредственно к разработке API: нам осталось прописать номенклатуру всех объектов, полей, методов и функций в деталях. В этой главе мы дадим сугубо практические советы, как сделать API удобным и понятным.</p>
<p>Важное уточнение под номером ноль:</p>
<h4 id="0">0. Правила — это всего лишь обобщения</h4>
<p>Правила не действуют безусловно и не означают, что можно не думать головой. У каждого правила есть какая-то рациональная причина его существования. Если в вашей ситуации нет причин следовать правилу — значит, следовать ему не надо.</p>
@ -868,7 +890,7 @@ POST /v1/orders/statistics/aggregate
либо<br />
<code>"duration": {"unit": "ms", "value": 5000}</code>.</p>
<p>Отдельное следствие из этого правила — денежные величины <em>всегда</em> должны сопровождаться указанием кода валюты.</p>
<p>Также следует отметить, что в некоторых областях ситуация со стандартами настолько плоха, что как ни сделай — кто-то останется недовольным. Классический пример такого рода — порядок географических координат ("широта-долгота" против "долгота-широта"). Здесь, увы, есть только один работающий метод борьбы с фрустрацией — «блокнот душевного спокойствия», который будет описан в разделе II.</p>
<p>Также следует отметить, что в некоторых областях ситуация со стандартами настолько плоха, что, как ни сделай, — кто-то останется недовольным. Классический пример такого рода — порядок географических координат ("широта-долгота" против "долгота-широта"). Здесь, увы, есть только один работающий метод борьбы с фрустрацией — «блокнот душевного спокойствия», который будет описан в разделе II.</p>
<h4 id="3">3. Сохраняйте точность дробных чисел</h4>
<p>Там, где это позволено протоколом, дробные числа с фиксированной запятой — такие, как денежные суммы, например — должны передаваться в виде специально предназначенных для этого объектов, например, Decimal или аналогичных.</p>
<p>Если в протоколе нет Decimal-типов (в частности, в JSON нет чисел с фиксированной запятой), следует либо привести к целому (путём домножения на указанный множитель), либо использовать строковый тип.</p>
@ -944,7 +966,7 @@ GET /comments/{id}
"content"
}
</code></pre>
<p>— хотя операция будто бы выполнена успешна, клиенту необходимо сделать дополнительный запрос, чтобы понять необходимость решения капчи. Между вызовами <code>POST /comments</code> и <code>GET /comments/{id}</code> клиент находится в состоянии кота Шрёдингера: непонятно, опубликован комментарий или нет, и как отразить это пользователю.</p>
<p>— хотя операция будто бы выполнена успешно, клиенту необходимо сделать дополнительный запрос, чтобы понять необходимость решения капчи. Между вызовами <code>POST /comments</code> и <code>GET /comments/{id}</code> клиент находится в состоянии кота Шрёдингера: непонятно, опубликован комментарий или нет, и как отразить это пользователю.</p>
<p><strong>Хорошо</strong>:</p>
<pre><code>// Создаёт комментарий и возвращает его
POST /v1/comments
@ -960,7 +982,7 @@ GET /v1/comments/{id}
}
</code></pre>
<p>Вообще в 9 случаях из 10 (а фактически — всегда, когда размер ответа невелик) во всех отношениях лучше из любой модифицирующей операции возвращать полное состояние сущности в том же формате, что и из операции доступа на чтение.</p>
<p>Вообще в 9 случаях из 10 (а фактически — всегда, когда размер ответа не оказывает значительного влияния на производительность) во всех отношениях лучше из любой модифицирующей операции возвращать полное состояние сущности в том же формате, что и из операции доступа на чтение.</p>
<h4 id="9">9. Идемпотентность</h4>
<p>Все операции должны быть идемпотентны. Напомним, идемпотентность — это следующее свойство: повторный вызов той же операции с теми же параметрами не изменяет результат. Поскольку мы обсуждаем в первую очередь клиент-серверное взаимодействие, узким местом в котором является ненадежность сетевой составляющей, повтор запроса при обрыве соединения — не исключительная ситуация, а норма жизни.</p>
<p>Там, где идемпотентность не может быть обеспечена естественным образом, необходимо добавить явный параметр — ключ идемпотентности или ревизию.</p>
@ -974,7 +996,7 @@ POST /orders
POST /v1/orders
X-Idempotency-Token: &lt;случайная строка&gt;
</code></pre>
<p>Клиент на своей стороне запоминает X-Idempotency-Token, и, в случае автоматического повторного перезапроса, обязан его сохранить. Сервер на своей стороне проверяет токен и, если заказ с таким токеном уже существует для этого клиента, не даёт создать заказ повторно.</p>
<p>Клиент на своей стороне запоминает <code>X-Idempotency-Token</code>, и, в случае автоматического повторного перезапроса, обязан его сохранить. Сервер на своей стороне проверяет токен и, если заказ с таким токеном уже существует для этого клиента, не даёт создать заказ повторно.</p>
<p><strong>Альтернатива</strong>:</p>
<pre><code>// Создаёт черновик заказа
POST /v1/orders
@ -1003,7 +1025,7 @@ GET /price?recipe=lungo&amp;longitude={longitude}&amp;latitude={latitude}
<li>на каком расстоянии от указанной точки цена всё ещё действительна? </li>
</ul>
<p><strong>Хорошо</strong>:
Для указания времени жизни кэша можно пользоваться стандатрными средствами протокола, например, заголовком Cache-Control. В ситуации, когда кэш нужен и по временной, и по пространственной координате следует поступить примерно так:</p>
Для указания времени жизни кэша можно пользоваться стандартными средствами протокола, например, заголовком <code>Cache-Control</code>. В ситуации, когда кэш нужен и по временной, и по пространственной координате следует поступить примерно так:</p>
<pre><code>// Возвращает предложение: за какую сумму
// наш сервис готов приготовить лунго
GET /price?recipe=lungo&amp;longitude={longitude}&amp;latitude={latitude}
@ -1025,44 +1047,66 @@ GET /price?recipe=lungo&amp;longitude={longitude}&amp;latitude={latitude}
}
}
</code></pre>
<h4 id="11-1">11. Пагинация, фильтрация и курсоры</h4>
<h4 id="11">11. Пагинация, фильтрация и курсоры</h4>
<p>Любой эндпойнт, возвращающий массивы данных, должен содержать пагинацию. Никаких исключений в этом правиле быть не может.</p>
<p>Любой эндпойнт, возвращающий изменяемые данные постранично, должен обеспечивать возможность эти данные перебрать.</p>
<p><strong>Плохо</strong>:</p>
<pre><code>// Возвращает указанный limit записей,
// отсортированных по дате создания
// начиная с записи с номером offset
GET /records?limit=10&amp;offset=100
GET /v1/records?limit=10&amp;offset=100
</code></pre>
<p>На первый взгляд это самый что ни на есть стандартный способ организации пагинации в API. Однако зададим себе три вопроса:</p>
<ol>
<li>Каким образом клиент узнает о появлении новых записей в начале списка?<br />
Легко заметить, что клиент может только попытаться повторить первый запрос и сличить идентификаторы с запомненным началом списка. Но что делать, если добавленное количество записей превышает limit? Представим себе ситуацию:<ul>
Легко заметить, что клиент может только попытаться повторить первый запрос и сличить идентификаторы с запомненным началом списка. Но что делать, если добавленное количество записей превышает <code>limit</code>? Представим себе ситуацию:<ul>
<li>клиент обрабатывает записи в порядке поступления;</li>
<li>произошла какая-то проблема, и накопилось большое количество необработанных записей;</li>
<li>клиент запрашивает новые записи (offset=0), однако не находит на первой странице известных идентификаторов — новых записей накопилось больше, чем limit;</li>
<li>клиент вынужден продолжить перебирать записи (увеличивая offset), пока не доберётся до последней известной ему; всё это время клиент простаивает;</li>
<li>клиент запрашивает новые записи (<code>offset=0</code>), однако не находит на первой странице известных идентификаторов — новых записей накопилось больше, чем <code>limit</code>;</li>
<li>клиент вынужден продолжить перебирать записи (увеличивая <code>offset</code>), пока не доберётся до последней известной ему; всё это время клиент простаивает;</li>
<li>таким образом может сложиться ситуация, когда клиент вообще никогда не обработает всю очередь, т.к. будет занят беспорядочным линейным перебором.</li></ul></li>
<li>Что произойдёт, если при переборе списка одна из записей в уже перебранной части будет удалена?<br />
Произойдёт следующее: клиент пропустит одну запись и никогда не сможет об этом узнать.</li>
<li>Какие параметры кэширования мы можем выставить на этот эндпойнт?<br />
Никакие: повторяя запрос с теми же limit-offset, мы каждый раз получаем новый набор записей.</li>
Никакие: повторяя запрос с теми же <code>limit</code>-<code>offset</code>, мы каждый раз получаем новый набор записей.</li>
</ol>
<p><strong>Хорошо</strong>: в таких однонаправленных списках пагинация должна быть организована по тому ключу, порядок которых фиксирован. Например, вот так:</p>
<pre><code>// Возвращает указанный limit записей,
// отсортированных по дате создания,
// начиная с первой записи, созданной позднее,
// чем запись с указанным id
GET /records?older_than={record_id}&amp;limit=10
GET /v1/records?older_than={record_id}&amp;limit=10
// Возвращает указанный limit записей,
// отсортированных по дате создания,
// начиная с первой записи, созданной раньше,
// чем запись с указанным id
GET /records?newer_than={record_id}&amp;limit=10
GET /v1/records?newer_than={record_id}&amp;limit=10
</code></pre>
<p>При такой организации клиенту не надо заботиться об удалении или добавлении записей в уже перебранной части списка: он продолжает перебор по идентификатору известной записи — первой известной, если надо получить новые записи; последней известной, если надо продолжить перебор.
Если операции удаления записей нет, то такие запросы можно свободно кэшировать — по одному и тому же URL будет всегда возвращаться один и тот же набор записей.<br />
Другой вариант организации таких списков — возврат курсора <code>cursor</code>, который используется вместо <code>record_id</code>, что делает интерфейсы универсальнее.</p>
Другой вариант организации таких списков — возврат курсора <code>cursor</code>, который используется вместо <code>record_id</code>, что делает интерфейсы более универсальными.</p>
<pre><code>// Первый запрос данных
POST /v1/records/list
{
// Какие-то дополнительные параметры фильтрации
"filter": {
"category": "some_category",
"created_date": {
"older_than": "2020-12-07"
}
}
}
{
"cursor"
}
</code></pre>
<pre><code>// Последующие запросы
GET /v1/records?cursor=&lt;значение курсора&gt;
{ "records", "cursor" }
</code></pre>
<p>Достоинством схемы с курсором является возможно зашифровать в самом курсоре данные исходного запроса (т.е. <code>filter</code> в нашем примере), и таким образом не дублировать его в последующих запросах. Это может быть особенно актуально, если инициализирующий запрос готовит полный массив данных, например, перенося его из «холодного» хранилища в горячее.</p>
<p>Вообще схему с курсором можно реализовать множеством способов (например, не разделять первый и последующие запросы данных), главное — выбрать какой-то один.</p>
<p><strong>Плохо</strong>:</p>
<pre><code>// Возвращает указанный limit записей,
// отсортированных по полю sort_by
@ -1151,7 +1195,7 @@ GET /v1/record-views/{id}?cursor={cursor}
<p>Важно понимать, что язык пользователя и юрисдикция, в которой пользователь находится — разные вещи. Цикл работы вашего API всегда должен хранить локацию пользователя. Либо она задаётся явно (в запросе указываются географические координаты), либо неявно (первый запрос с географическими координатами инициировал создание сессии, в которой сохранена локация) — но без локации корректная локализация невозможна. В большинстве случаев локацию допустимо редуцировать до кода страны.</p>
<p>Дело в том, что множество параметров, потенциально влияющих на работу API, зависят не от языка, а именно от расположения пользователя. В частности, правила форматирования чисел (разделители целой и дробной частей, разделители разрядов) и дат, первый день недели, раскладка клавиатуры, система единиц измерения (которая к тому же может оказаться не десятичной!) и так далее. В некоторых ситуациях необходимо хранить две локации: та, в которой пользователь находится, и та, которую пользователь сейчас просматривает. Например, если пользователь из США планирует туристическую поездку в Европу, то цены ему желательно показывать в местной валюте, но отформатированными согласно правилам американского письма.</p>
<p>Следует иметь в виду, что явной передачи локации может оказаться недостаточно, поскольку в мире существуют территориальные конфликты и спорные территории. Каким образом API должно себя вести при попадании координат пользователя на такие территории — вопрос, к сожалению, в первую очередь юридический. Автору этой книги приходилось как-то разрабатывать API, в котором пришлось вводить концепцию «территория государства A по мнению официальных органов государства Б».</p>
<p><strong>Важно</strong>: различайте локализацию для конечного пользователя и локализацию для разработчика. В примере из п.&nbsp;12 сообщение <code>localized_message</code> адресовано пользователю — его должно показать приложение, если в коде обработка такой ошибки невозможно. Это сообщение должно быть написано на указанном в запросе языке и отформатировано согласно правилам локации пользователя. А вот сообщение <code>details.checks_failed[].message</code> написано не для пользователя, а для разработчика, который будет разбираться с проблемой. Соответственно, написано и отформатировано оно должно быть понятно для разработчика — что, скорее всего, означает «на английском языке», т.к. английский де факто является стандартом в мире разработки программного обеспечения.</p>
<p><strong>Важно</strong>: различайте локализацию для конечного пользователя и локализацию для разработчика. В примере из п.&nbsp;12 сообщение <code>localized_message</code> адресовано пользователю — его должно показать приложение, если в коде обработка такой ошибки не предусмотрена. Это сообщение должно быть написано на указанном в запросе языке и отформатировано согласно правилам локации пользователя. А вот сообщение <code>details.checks_failed[].message</code> написано не для пользователя, а для разработчика, который будет разбираться с проблемой. Соответственно, написано и отформатировано оно должно быть понятным для разработчика образом — что, скорее всего, означает «на английском языке», т.к. английский де факто является стандартом в мире разработки программного обеспечения.</p>
<p>Следует отметить, что индикация, какие сообщения следует показать пользователю, а какие написаны для разработчика, должна, разумеется, быть явной конвенцией вашего API. В примере для этого используется префикс <code>localized_</code>.</p>
<p>И ещё одна вещь: все строки должны быть в кодировке UTF-8 и никакой другой.</p><div class="page-break"></div></article>
</body></html>

Binary file not shown.

View File

@ -6,7 +6,8 @@
"repository": "github.com:twirl/The-API-Book",
"devDependencies": {
"puppeteer": "^5.5.0",
"showdown": "^1.9.1"
"showdown": "^1.9.1",
"epub-gen": "^0.1.0"
},
"scripts": {
"build": "node build.js"

View File

@ -1,25 +1,15 @@
html {
width: 100%;
margin: 0;
padding: 0;
}
body {
font-family: 'PT Serif';
font-size: 14pt;
text-align: justify;
}
@media screen {
body {
margin: 20px auto;
max-width: 60%;
}
}
@media print {
h1 {
margin: 4.5in 0 5.2in 0;
}
body {
font-size: 20pt;
}
}
.cc-by-nc {
background: transparent url(https://i.creativecommons.org/l/by-nc/4.0/88x31.png) 0 5px no-repeat;
padding-left: 92px;
@ -41,6 +31,7 @@ pre {
border-left: 1px solid rgba(0,0,0,.45);
box-shadow: .1em .1em .1em rgba(0,0,0,.45);
page-break-inside: avoid;
overflow-x: auto;
}
pre code {
@ -68,10 +59,12 @@ h1 {
h2 {
font-size: 160%;
text-transform: uppercase;
}
h3 {
font-size: 140%;
font-variant: small-caps;
}
h4, h5 {
@ -87,4 +80,38 @@ h4, h5 {
--main-font: 'PT Serif';
--alt-font: 'PT Serif';
--code-font: Inconsolata;
}
}
@media screen {
body {
margin: 2em auto;
max-width: 60%;
}
}
@media print {
h1 {
margin: 4in 0 4in 0;
}
body {
font-size: 14pt;
}
}
@media screen and (max-width: 1000px) {
body {
padding: 2em;
margin: 0;
max-width: none;
text-align: left;
}
code {
font-size: 11pt;
}
pre {
margin: 0;
padding: 0.2em;
}
}