You've already forked The-API-Book
							
							
				mirror of
				https://github.com/twirl/The-API-Book.git
				synced 2025-10-30 23:37:47 +02:00 
			
		
		
		
	Идемпотетнтость, кэширование, пагинация
This commit is contained in:
		
							
								
								
									
										246
									
								
								docs/API.ru.html
									
									
									
									
									
								
							
							
						
						
									
										246
									
								
								docs/API.ru.html
									
									
									
									
									
								
							| @@ -531,50 +531,63 @@ GET /sensors | |||||||
| <li><p>С точки зрения верхнеуровневого API отмена заказа является терминальным действием, т.е. никаких последующих операций уже быть не может; а с точки зрения низкоуровневого API обработка заказа продолжается, т.к. нужно дождаться, когда стакан будет утилизирован, и после этого освободить кофе-машину (т.е. разрешить создание новых рантаймов на ней). Это вторая задача для уровня исполнения: связывать оба статуса, внешний (заказ отменён) и внутренний (исполнение продолжается).</p></li> | <li><p>С точки зрения верхнеуровневого API отмена заказа является терминальным действием, т.е. никаких последующих операций уже быть не может; а с точки зрения низкоуровневого API обработка заказа продолжается, т.к. нужно дождаться, когда стакан будет утилизирован, и после этого освободить кофе-машину (т.е. разрешить создание новых рантаймов на ней). Это вторая задача для уровня исполнения: связывать оба статуса, внешний (заказ отменён) и внутренний (исполнение продолжается).</p></li> | ||||||
| </ol> | </ol> | ||||||
| <p>Может показаться, что соблюдение правила изоляции уровней абстракции является избыточным и заставляет усложнять интерфейс. И это в действительности так: важно понимать, что никакая гибкость, логичность, читабельность и расширяемость не бывает бесплатной. Можно построить API так, чтобы оно выполняло свою функцию с минимальными накладными расходами, по сути — дать интерфейс к микроконтроллерам кофе-машины. Однако пользоваться им будет крайне неудобно, и расширяемость такого API будет нулевой.</p> | <p>Может показаться, что соблюдение правила изоляции уровней абстракции является избыточным и заставляет усложнять интерфейс. И это в действительности так: важно понимать, что никакая гибкость, логичность, читабельность и расширяемость не бывает бесплатной. Можно построить API так, чтобы оно выполняло свою функцию с минимальными накладными расходами, по сути — дать интерфейс к микроконтроллерам кофе-машины. Однако пользоваться им будет крайне неудобно, и расширяемость такого API будет нулевой.</p> | ||||||
| <p>Выделение уровней абстракции — прежде всего <em>логическая</em> процедура: как мы объясняем себе и разработчику, из чего состоит наш API. <strong>Абстрагируемая дистанция между сущностями существует объективно</strong>, каким бы образом мы ни написали конкретные интерфейсы. Наша задача состоит только лишь в том, чтобы эта дистанция была разделена на уровни <em>явно</em>. Чем неявнее разведены уровни абстракции (или хуже того, перемешаны) уровни абстракции, тем сложнее будет разобраться в вашем API и тем хуже будет написан использующий его код.</p><div class="page-break"></div><h3 id="10">Глава 10. Разграничение областей ответственности</h3> | <p>Выделение уровней абстракции — прежде всего <em>логическая</em> процедура: как мы объясняем себе и разработчику, из чего состоит наш API. <strong>Абстрагируемая дистанция между сущностями существует объективно</strong>, каким бы образом мы ни написали конкретные интерфейсы. Наша задача состоит только лишь в том, чтобы эта дистанция была разделена на уровни <em>явно</em>. Чем более неявно разведены (или, хуже того, перемешаны) уровни абстракции, тем сложнее будет разобраться в вашем API, и тем хуже будет написан использующий его код.</p> | ||||||
|  | <h4 id="-1">Потоки данных</h4> | ||||||
|  | <p>Полезное упражнение, позволяющее рассмотреть иерархию уровней абстракции API — исключить из рассмотрения все частности и построить — в голове или на бумаге — дерево потоков данных: какие данные протекают через объекты вашего API и как видоизменяются на каждом этапе.</p> | ||||||
|  | <p>Это упражнение не только полезно для проектирования, но и, помимо прочего, является единственным способом развивать большие (в смысле номенклатуры объектов) API. Человеческая память не безгранична, и любой активно развивающийся проект достаточно быстро станет настолько большим, что удержать в голове всю иерархию сущностей со всеми полями и методами станет невозможно. Но вот держать в уме схему потоков данных обычно вполне возможно — или, во всяком случае, получается держать в уме на порядок больший фрагмент дерева сущностей API.</p> | ||||||
|  | <p>Какие потоки данных мы имеем в нашем кофейном API?</p> | ||||||
|  | <ol> | ||||||
|  | <li><p>Данные с сенсоров — объёмы кофе / воды / стакана. Это низший из доступных нам уровней данных, здесь мы не можем ничего изменить или переформулировать.</p></li> | ||||||
|  | <li><p>Непрерывный поток данных сенсоров мы преобразуем в дискретные статусы исполнения команд, вводя в него понятия, не существующие в предметной области. API кофе-машины не предоставляет нам понятий «кофе наливается» или «стакан ставится» — это наше программное обеспечение трактует поступающие потоки данных от сенсоров, вводя новые понятия: если наблюдаемый объём (кофе или воды) меньше целевого — значит, процесс не закончен; если объём достигнут — значит, необходимо сменить статус исполнения и выполнить следующее действие.<br /> | ||||||
|  | Важно отметить, что мы не просто вычисляем какие-то новые параметры из имеющихся данных сенсоров: мы сначала создаём новый кортеж данных более высокого уровня — «программа исполнения» как последовательность шагов и условий — и инициализируем его начальные значения. Без этого контекста определить, что собственно происходит с кофе-машиной невозможно.</p></li> | ||||||
|  | <li><p>Обладая логическими данными о состоянии исполнения программы, мы можем (вновь через создание нового, более высокоуровневого контекста данных!) свести данные от двух типов API к единому формату: исполнение операции создания напитка и её логические параметры: целевой рецепт, объём, готов ли заказ.</p></li> | ||||||
|  | </ol> | ||||||
|  | <p>Таким образом, каждый уровень абстракции нашего API соответствует какому-то обобщению и обогащению потока данных, преобразованию его из терминов нижележащего (и вообще говоря бесполезного для потребителя) контекста в термины вышестоящего контекста.</p> | ||||||
|  | <p>Дерево можно развернуть и в обратную сторону:</p> | ||||||
|  | <ol> | ||||||
|  | <li><p>На уровне заказа мы задаём его логические параметры: рецепт, объём, место исполнения и набор допустимых статусов заказа.</p></li> | ||||||
|  | <li><p>На уровне исполнения мы обращаемся к данным уровня заказа и создаём более низкоуровневый контекст: программа исполнения в виде последовательности шагов, их параметров и условий перехода от одного шага к другому и начальное состояние (с какого шага начать исполнение и с какими параметрами).</p></li> | ||||||
|  | <li><p>На уровне рантайма мы обращаемся к целевым значениям (какую операцию выполнить и какой целевой объём) и преобразуем её в набор микрокоманд API кофе-машины и набор статусов исполнения каждой команды.</p></li> | ||||||
|  | </ol> | ||||||
|  | <p>Если обратиться к описанному в начале главы «плохому» решению (предполагающему самостоятельное определение факта готовности заказа разработчиком), то мы увидим, что и с точки зрения потоков данных происходит смешение понятий: </p> | ||||||
|  | <ul> | ||||||
|  | <li><p>с одной стороны, в контексте заказа оказываются данные (объём кофе), «просочившиеся» откуда-то с физического уровня; тем самым, уровни абстракции непоправимо смешиваются без возможности их разделить;</p></li> | ||||||
|  | <li><p>с другой стороны, сам контекст заказа неполноценный: он не задаёт новых мета-переменных, которые отсутствуют на более низких уровнях абстракции (статус заказа), не инициализирует их и не предоставляет правил работы.</p> | ||||||
|  | <p>Более подробно о контекстах данных мы поговорим в разделе II. Здесь же ограничимся следующим выводом: потоки данных и их преобразования можно и нужно рассматривать как некоторый срез, который, с одной стороны, помогает нам правильно разделить уровни абстракции, а с другой — проверить, что наши теоретические построения действительно работают так, как нужно.</p></li> | ||||||
|  | </ul><div class="page-break"></div><h3 id="10">Глава 10. Разграничение областей ответственности</h3> | ||||||
| <p>Исходя из описанного в предыдущей главе, мы понимаем, что иерархия абстракций в нашем гипотетическом проекте должна выглядеть примерно так:</p> | <p>Исходя из описанного в предыдущей главе, мы понимаем, что иерархия абстракций в нашем гипотетическом проекте должна выглядеть примерно так:</p> | ||||||
| <ul> | <ul> | ||||||
| <li>пользовательский уровень (те сущности, с которыми непосредственно взаимодействует пользователь и сформулированы в понятных для него терминах; например, заказы и виды кофе);</li> | <li>пользовательский уровень (те сущности, с которыми непосредственно взаимодействует пользователь и сформулированы в понятных для него терминах; например, заказы и виды кофе);</li> | ||||||
| <li>исполнительный уровень (те сущности, которые отвечают за переформулирование заказа в машинные термины);</li> | <li>уровень исполнения программ (те сущности, которые отвечают за преобразование заказа в машинные термины);</li> | ||||||
| <li>физический уровень (непосредственно сами датчики машины).</li> | <li>уровень рантайма для API второго типа (сущности, отвечающие за state-машину выполнения заказа).</li> | ||||||
| </ul> | </ul> | ||||||
| <p>Теперь нам необходимо определить ответственность каждой сущности: в чём смысл её существования в рамках нашего API, какие действия можно выполнять с самой сущностью, а какие — делегировать другим объектам. Фактически, нам нужно применить «зачем-принцип» к каждой отдельной сущности нашего API.</p> | <p>Теперь нам необходимо определить ответственность каждой сущности: в чём смысл её существования в рамках нашего API, какие действия можно выполнять с самой сущностью, а какие — делегировать другим объектам. Фактически, нам нужно применить «зачем-принцип» к каждой отдельной сущности нашего API.</p> | ||||||
| <p>Для этого нам нужно пройти по нашему API и сформулировать в терминах предметной области, что представляет из себя каждый объект. Напомню, что из концепции уровней абстракции следует, что каждый уровень иерархии — это некоторая собственная промежуточная предметная область, ступенька, по которой мы переходим от описания задачи в терминах одного связываемого контекста («заказанный пользователем лунго») к описанию в терминах второго («задание кофе-машине на выполнение указанной программы»).</p> | <p>Для этого нам нужно пройти по нашему API и сформулировать в терминах предметной области, что представляет из себя каждый объект. Напомню, что из концепции уровней абстракции следует, что каждый уровень иерархии — это некоторая собственная промежуточная предметная область, ступенька, по которой мы переходим от описания задачи в терминах одного связываемого контекста («заказанный пользователем лунго») к описанию в терминах второго («задание кофе-машине на выполнение указанной программы»).</p> | ||||||
| <p>В нашем умозрительном примере получится примерно так:</p> | <p>В нашем умозрительном примере получится примерно так:</p> | ||||||
| <ol> | <ol> | ||||||
|  | <li>Сущности уровня пользователя (те, работая с которыми, разработчик непосредственно решает задачи пользователя).<ul> | ||||||
| <li>Заказ <code>order</code> — описывает некоторую логическую единицу взаимодействия с пользователем. Заказ можно:<ul> | <li>Заказ <code>order</code> — описывает некоторую логическую единицу взаимодействия с пользователем. Заказ можно:<ul> | ||||||
| <li>создавать</li> | <li>создавать;</li> | ||||||
| <li>проверять статус</li> | <li>проверять статус;</li> | ||||||
| <li>получать или отменять</li></ul></li> | <li>получать или отменять.</li></ul></li> | ||||||
| <li>Рецепт <code>recipe</code> — описывает «идеальную модель» вида кофе, его потребительские свойства. Рецепт в данном контексте для нас неизменяемая сущность, которую можно только просмотреть и выбрать;</li> | <li>Рецепт <code>recipe</code> — описывает «идеальную модель» вида кофе, его потребительские свойства. Рецепт в данном контексте для нас неизменяемая сущность, которую можно только просмотреть и выбрать.</li> | ||||||
| <li>Задание <code>task</code> — описывает некоторые задачи, на которые декомпозируются заказ;</li> | <li>Кофе-машина <code>coffee-machine</code> — модель объекта реального мира. Из описания кофе-машины мы, в частности, должны извлечь её положение в пространстве и предоставляемые опции (о чём подробнее поговорим ниже).</li></ul></li> | ||||||
| <li>Кофе-машина <code>cofee-machine</code> — модель объекта реального мира. Мы можем:<ul> | <li>Сущности уровня управления исполнением (те, работая с которыми, можно непосредственно исполнить заказ):<ul> | ||||||
| <li>получать статусы датчиков</li> | <li>Программа <code>program</code> — описывает доступные возможности конкретной кофе-машины. Программы можно только просмотреть.</li> | ||||||
| <li>отправлять команды и проверять их исполнение</li></ul></li> | <li>Селектор программ <code>programs/matcher</code> — позволяет связать рецепт и программу исполнения, т.е. фактически выяснить набор данных, необходимых для приготовления конкретного рецепта на конкретной кофе-машине. Селектор работает только на выбор нужной программы.</li> | ||||||
|  | <li>Запуск программы <code>programs/run</code> — конкретный факт исполнения программы на конкретной кофе-машине. Запуски можно:<ul> | ||||||
|  | <li>инициировать (создавать);</li> | ||||||
|  | <li>отменять;</li> | ||||||
|  | <li>проверять состояние запуска.</li></ul></li></ul></li> | ||||||
|  | <li>Сущности уровня программ исполнения (те, работая с которыми, можно непосредственно управлять состоянием кофе-машины через API второго типа).<ul> | ||||||
|  | <li>Рантайм <code>runtime</code> — контекст исполнения программы, т.е. состояние всех переменных. Рантаймы можно:<ul> | ||||||
|  | <li>создавать;</li> | ||||||
|  | <li>проверять статус;</li> | ||||||
|  | <li>терминировать.</li></ul></li></ul></li> | ||||||
| </ol> | </ol> | ||||||
| <p>Если внимательно посмотреть на каждый объект, то мы увидим, что, в итоге, каждый объект оказался в смысле своей ответственности составным. Например, <code>coffee-machine</code> будет частично оперировать реальными командами кофе-машины <em>и</em> представлять их состояние в каком-то машиночитаемом виде.</p> | <p>Если внимательно посмотреть на каждый объект, то мы увидим, что, в итоге, каждый объект оказался в смысле своей ответственности составным. Например, <code>program</code> будет оперировать данными высшего уровня (рецепт и кофе-машина), дополняя их терминами своего уровня (идентификатор запуска). Это совершенно нормально: API должно связывать контексты.</p> | ||||||
| <h4 id="">Декомпозиция интерфейсов</h4> | <h4 id="">Сценарии использования</h4> | ||||||
| <p>На этапе разделения уровней абстракции мы упомянули, что одна из целей такого разделения — добиться возможности безболезненно сменить нижележащие уровни абстракции при добавлении новых технологий или изменении нижележащих протоколов. Декомпозиция интерфейсов — основной инструмент, позволяющий добиться такой гибкости.</p> | <p>На этом уровне, когда наше API уже в целом понятно устроено и спроектированы, мы должны поставить себя на место разработчика и попробовать написать код.</p> | ||||||
| <p>На этом этапе нам предстоит отделить частное от общего: понять, какие свойства сущностей фиксированы в нашей модели, а какие будут зависеть от реализации. В нашем кофе-примере таким сильно зависящим от имплементации объектом, очевидно, будут сами кофе-машины. Попробуем ещё немного развить наше API: каким образом нам нужно описать модели кофе-машин для того, чтобы предоставить интерфейсы для работы с задачами заказа?</p> |  | ||||||
| <h4 id="-1">Интерфейсы как универсальный паттерн</h4> |  | ||||||
| <p>Как мы убедились в предыдущей главе, выделение интерфейсов крайне важно с точки зрения удобства написания кода. Однако, интерфейсы играют и другую важную роль в проектировании: они позволяют уложить в голове архитектуру API целиком.</p> |  | ||||||
| <p>Любой сколько-нибудь крупный API рано или поздно обрастает разнообразной номенклатурой сущностей, их свойст и методов, как в силу того, что в одном объекте «сходятся» несколько предметных областей, так и в силу появления со временем разнообразной вспомогательной и дополнительной функциональности. Особенно сложной номенклатура объектов и их методов становится в случае появления альтернативных реализаций одного и того же интерфейса.</p> |  | ||||||
| <p>Человеческие возможности небезграничны: невозможно держать в голове всю номенклатуру объектов. Это осложняет и проектирование API, и рефакторинг, и просто решение возникающих задач по реализации той или иной бизнес-логики.</p> |  | ||||||
| <p>Держать же в голове схему взаимодействия интерфейсов гораздо проще - как в силу исключения из рассмотрения разнообразных вспомогательных и специфических методов, так и в силу того, что интерфейсы позволяют отделить существенное (в чем смысл конкретной сущности) от несущественного (деталей реализации).</p> |  | ||||||
| <p>Поскольку задача выделения интерфейсов есть задача удобного манипулирования сущностями в голове разработчика, мы рекомендуем при проектировании интерфейсов руководствоваться, прежде всего, здравым смыслом: интерфейсы должны быть ровно настолько сложны, насколько это удобно для человеческого восприятия (а лучше даже чуть проще). В простейших случаях это просто означает, что интерфейс должен содержать семь плюс-минуса два свойства/метода. Более сложные интерфейсы должны декомпозироваться в несколько простых.</p> |  | ||||||
| <p>Это правило существенно важно не только при проектировании api - не забывайте, что ваши пользователи неизбежно столкнутся с той же проблемой - понять примерную архитектуру вашего api, запомнить, что с чем связано в вашей системе. Правильно выделенные интерфейсы помогут и здесь, причём сразу в двух смыслах - как непосредственно работающему с вашим кодом программисту, так и документатору, которому будет гораздо проще описать структуру вашего api, опираясь на дерево интерфейсов.</p> |  | ||||||
| <p>С другой стороны надо вновь напомнить, что бесплатно ничего не бывает, и выделение интерфейсов - самая «небесплатная» часть процесса разработки API, поскольку в чистом виде приносится в жертву удобство разработки ради построения «правильной» архитектуры: разумеется, код писать куда проще, когда имеешь доступ ко всем объектам API со всей их богатой номенклатурой методов, нежели когда из каждого объекта доступны только пара непосредственно примыкающих интерфейсов, притом с максимально общими методами.</p> |  | ||||||
| <p>Помимо прочего, это означает, что интерфейсы необходимо выделять там, где это актуально решаемой задаче - прежде всего, в точках будущего роста и там, где возможны альтернативные реализации. Чем проще API, тем меньше нужда в интерфейсах, и наоборот: сложное API требует интерфейсов практически всюду просто для того, чтобы ограничить разрастание излишне сильной связанности и при этом не сойти с ума.</p> |  | ||||||
| <p>В пределе в сложном api должна сложиться ситуация, при которой все объекты взаимодействуют друг с другом только как интерфейсы — нет ни одной публичной сигнатуры, принимающей конкретный объект, а не его интерфейс. Разумеется, достичь такого уровня абстракции практически невозможно - почти в любой системе есть глобальные объекты, разнообразные технические сущности (имплементации стандартных структур данных, например); наконец, невозможно «спрятать» за интерфейсы системные объекты или сущности физического уровня.</p> |  | ||||||
| <h4 id="-2">Информационные контексты</h4> |  | ||||||
| <p>При выделении интерфейсов и вообще при проектировании api бывает полезно взглянуть на иерархию абстракций с другой точки зрения, а именно: каким образом информация «протекает» через нашу иерархию.</p> |  | ||||||
| <p>Вспомним, что одним из критериев отделения уровней абстракции является переход от структур данных одной предметной области к структурам данных другой. В рамках нашего примера через иерархию наших объектов происходит трансляция данных реального мира — «железные» кофе-машины, которые готовят реально существующие напитки — в виртуальные интерфейсы «заказов».</p> |  | ||||||
| <p>Если обратиться к правилу «неперепрыгивания» через уровни абстракции, то с точки зрения потоков данных оно формулируется так:</p> |  | ||||||
| <ul> |  | ||||||
| <li>каждый объект в иерархии абстракций должен оперировать данными согласно своему уровню иерархии;</li> |  | ||||||
| <li>преобразованием данных имеют право заниматься только те объекты, в чьи непосредственные обязанности это входит.</li> |  | ||||||
| </ul> |  | ||||||
| <p>Дерево информационных контекстов (какой объект обладает какой информацией, и кто является транслятором из одного контекста в другой), по сути, представляет собой «срез» нашего дерева иерархии интерфейсов; выделение такого среза позволяет проще и удобнее удерживать в голове всю архитектуру проекта.</p> |  | ||||||
| <p>// TODO | <p>// TODO | ||||||
| // Хелперы, бойлерплейт</p><div class="page-break"></div><h3 id="11">Глава 11. Описание конечных интерфейсов</h3> | // Хелперы, бойлерплейт</p><div class="page-break"></div><h3 id="11">Глава 11. Описание конечных интерфейсов</h3> | ||||||
| <p>Определив все сущности, их ответственность и отношения друг с другом, мы переходим непосредственно к разработке API: нам осталось прописать номенклатуру всех объектов, полей, методов и функций в деталях. В этой главе мы дадим сугубо практические советы, как сделать API удобным и понятным.</p> | <p>Определив все сущности, их ответственность и отношения друг с другом, мы переходим непосредственно к разработке API: нам осталось прописать номенклатуру всех объектов, полей, методов и функций в деталях. В этой главе мы дадим сугубо практические советы, как сделать API удобным и понятным.</p> | ||||||
| @@ -603,16 +616,16 @@ GET /orders/statistics | |||||||
| <p>Даже если операция немодифицирующая, но вычислительно дорогая — следует об этом явно индицировать, особенно если вычислительные ресурсы тарифицируются для пользователя; тем более не стоит подбирать значения по умолчанию так, чтобы вызов операции без параметров максимально расходовал ресурсы.</p> | <p>Даже если операция немодифицирующая, но вычислительно дорогая — следует об этом явно индицировать, особенно если вычислительные ресурсы тарифицируются для пользователя; тем более не стоит подбирать значения по умолчанию так, чтобы вызов операции без параметров максимально расходовал ресурсы.</p> | ||||||
| <p><strong>Хорошо</strong>:</p> | <p><strong>Хорошо</strong>:</p> | ||||||
| <pre><code>// Возвращает агрегированную статистику заказов за указанный период | <pre><code>// Возвращает агрегированную статистику заказов за указанный период | ||||||
| POST /orders/statistics/aggregate | POST /v1/orders/statistics/aggregate | ||||||
| { "start_date", "end_date" } | { "start_date", "end_date" } | ||||||
| </code></pre></li> | </code></pre></li> | ||||||
| </ul> | </ul> | ||||||
| <p><strong>Стремитесь к тому, чтобы из сигнатуры функции было абсолютно ясно, что она делает, что принимает на вход и что возвращает</strong>. Вообще, при прочтении кода, работающего с вашим API, должно быть сразу понятно, что, собственно, он делает — без подглядывания в документацию.</p> | <p><strong>Стремитесь к тому, чтобы из сигнатуры функции было абсолютно ясно, что она делает, что принимает на вход и что возвращает</strong>. Вообще, при прочтении кода, работающего с вашим API, должно быть сразу понятно, что, собственно, он делает — без подглядывания в документацию.</p> | ||||||
| <p>Два важных следствия:</p> | <p>Два важных следствия:</p> | ||||||
| <p><strong>1.1.</strong> Если операция модифицирующая, это должно быть очевидно из сигнатуры. В частности, не может быть модифицирующих операций за <code>GET</code>.</p> | <p><strong>1.1.</strong> Если операция модифицирующая, это должно быть очевидно из сигнатуры. В частности, не может быть модифицирующих операций за <code>GET</code>.</p> | ||||||
| <p><strong>1.2.</strong> Если операция асинхронная, это должно быть очевидно из сигнатуры, <strong>либо</strong> должна существовать конвенция именования, позволяющая отличаться синхронные операции от асинхронных.</p> | <p><strong>1.2.</strong> Если в номенклатуре вашего API есть как синхронные операции, так и асинхронные, то (а)синхронность должна быть очевидна из сигнатур, <strong>либо</strong> должна существовать конвенция именования, позволяющая отличать синхронные операции от асинхронных.</p> | ||||||
| <h4 id="2">2. Использованные стандарты указывайте явно</h4> | <h4 id="2">2. Использованные стандарты указывайте явно</h4> | ||||||
| <p>К сожалению, человечество не в состоянии договориться о таких простейших вещах, как «начинать ли неделю с понедельника или с воскресенья», что уж говорить о каких-то более сложных стандартах.</p> | <p>К сожалению, человечество не в состоянии договориться о таких простейших вещах, как «с какого дня начинается неделя», что уж говорить о каких-то более сложных стандартах.</p> | ||||||
| <p>Поэтому <em>всегда</em> указывайте, по какому конкретно стандарту вы отдаёте те или иные величины. Исключения возможны только там, где вы на 100% уверены, что в мире существует только один стандарт для этой сущности, и всё население земного шара о нём в курсе:</p> | <p>Поэтому <em>всегда</em> указывайте, по какому конкретно стандарту вы отдаёте те или иные величины. Исключения возможны только там, где вы на 100% уверены, что в мире существует только один стандарт для этой сущности, и всё население земного шара о нём в курсе:</p> | ||||||
| <ul> | <ul> | ||||||
| <li><strong>плохо</strong>: <code>"date":"11/12/2020"</code> — стандартов записи дат существует огромное количество, плюс из этой записи невозможно даже понять, что здесь число, а что месяц;<br /> | <li><strong>плохо</strong>: <code>"date":"11/12/2020"</code> — стандартов записи дат существует огромное количество, плюс из этой записи невозможно даже понять, что здесь число, а что месяц;<br /> | ||||||
| @@ -626,7 +639,7 @@ POST /orders/statistics/aggregate | |||||||
| <code>"duration":{"unit":"ms","value":5000}</code>.</li> | <code>"duration":{"unit":"ms","value":5000}</code>.</li> | ||||||
| </ul> | </ul> | ||||||
| <p>Отдельное следствие из этого правила — денежные величины <em>всегда</em> должны сопровождаться указанием кода валюты.</p> | <p>Отдельное следствие из этого правила — денежные величины <em>всегда</em> должны сопровождаться указанием кода валюты.</p> | ||||||
| <p>Также следует отметить, что в некоторых областях ситуация со стандартами настолько плоха, что как ни сделай — кто-то останется недовольным. Классический пример такого рода — порядок геокоординат ("широта-долгота" против "долгота-широта"). Здесь, увы, есть только один работающий метод борьбы с фрустрацией — «блокнот душевного спокойствия», который будет описан в разделе II.</p> | <p>Также следует отметить, что в некоторых областях ситуация со стандартами настолько плоха, что как ни сделай — кто-то останется недовольным. Классический пример такого рода — порядок географических координат ("широта-долгота" против "долгота-широта"). Здесь, увы, есть только один работающий метод борьбы с фрустрацией — «блокнот душевного спокойствия», который будет описан в разделе II.</p> | ||||||
| <h4 id="3">3. Сохраняйте точность дробных чисел</h4> | <h4 id="3">3. Сохраняйте точность дробных чисел</h4> | ||||||
| <p>Там, где это позволено протоколом, дробные числа с фиксированной запятой — такие, как денежные суммы, например — должны передаваться в виде специально предназначенных для этого объектов, например, Decimal или аналогичных.</p> | <p>Там, где это позволено протоколом, дробные числа с фиксированной запятой — такие, как денежные суммы, например — должны передаваться в виде специально предназначенных для этого объектов, например, Decimal или аналогичных.</p> | ||||||
| <p>Если в протоколе нет Decimal-типов (в частности, в JSON нет чисел с фиксированной запятой), следует либо привести к целому (путём домножения на указанный множитель), либо использовать строковый тип.</p> | <p>Если в протоколе нет Decimal-типов (в частности, в JSON нет чисел с фиксированной запятой), следует либо привести к целому (путём домножения на указанный множитель), либо использовать строковый тип.</p> | ||||||
| @@ -648,7 +661,7 @@ POST /orders/statistics/aggregate | |||||||
| strpbrk (str1, str2) | strpbrk (str1, str2) | ||||||
| </code> | </code> | ||||||
| Возможно, автору этого API казалось, что аббревиатура <code>pbrk</code> что-то значит для читателя, но он явно ошибся. К тому же, невозможно сходу понять, какая из строк <code>str1</code>, <code>str2</code> является набором символов для поиска.<br /> | Возможно, автору этого API казалось, что аббревиатура <code>pbrk</code> что-то значит для читателя, но он явно ошибся. К тому же, невозможно сходу понять, какая из строк <code>str1</code>, <code>str2</code> является набором символов для поиска.<br /> | ||||||
| <strong>Хорошо</strong>: <code>str_search_for_characters(lookup_character_set, str)</code><br /> | <strong>Хорошо</strong>: <code>str_search_for_characters (lookup_character_set, str)</code><br /> | ||||||
| Однако необходимость существования такого метода вообще вызывает сомнения, достаточно было бы иметь удобную функцию поиска подстроки с нужными параметрами. Аналогично сокращение <code>string</code> до <code>str</code> выглядит совершенно бессмысленным, но, увы, является устоявшимся для большого количества предметных областей.</li> | Однако необходимость существования такого метода вообще вызывает сомнения, достаточно было бы иметь удобную функцию поиска подстроки с нужными параметрами. Аналогично сокращение <code>string</code> до <code>str</code> выглядит совершенно бессмысленным, но, увы, является устоявшимся для большого количества предметных областей.</li> | ||||||
| </ul> | </ul> | ||||||
| <h4 id="6">6. Тип поля должен быть ясен из его названия</h4> | <h4 id="6">6. Тип поля должен быть ясен из его названия</h4> | ||||||
| @@ -672,7 +685,7 @@ strpbrk (str1, str2) | |||||||
| GET /coffee-machines/functions | GET /coffee-machines/functions | ||||||
| </code> | </code> | ||||||
| Слово "functions" многозначное; может означать и встроенные функции, и написанный код, и состояние (функционирует-не функционирует).<br /> | Слово "functions" многозначное; может означать и встроенные функции, и написанный код, и состояние (функционирует-не функционирует).<br /> | ||||||
| <strong>Хорошо</strong>: <code>GET /coffee-machines/builtin-functions-list</code></li> | <strong>Хорошо</strong>: <code>GET /v1/coffee-machines/builtin-functions-list</code></li> | ||||||
| </ul> | </ul> | ||||||
| <h4 id="7">7. Подобные сущности должны называться подобно и вести себя подобным образом</h4> | <h4 id="7">7. Подобные сущности должны называться подобно и вести себя подобным образом</h4> | ||||||
| <ul> | <ul> | ||||||
| @@ -722,14 +735,14 @@ GET /comments/{id} | |||||||
| <strong>Хорошо</strong>: | <strong>Хорошо</strong>: | ||||||
| <code> | <code> | ||||||
| // Создаёт комментарий и возвращает его | // Создаёт комментарий и возвращает его | ||||||
| POST /comments | POST /v1/comments | ||||||
| { "content" } | { "content" } | ||||||
| → | → | ||||||
| { "comment_id", "published", "action_required", "content" } | { "comment_id", "published", "action_required", "content" } | ||||||
| </code> | </code> | ||||||
| <code> | <code> | ||||||
| // Возвращает комментарий по его id | // Возвращает комментарий по его id | ||||||
| GET /comments/{id} | GET /v1/comments/{id} | ||||||
| → | → | ||||||
| { /* в точности тот же формат,  | { /* в точности тот же формат,  | ||||||
|    что и в ответе POST /comments */ |    что и в ответе POST /comments */ | ||||||
| @@ -737,5 +750,154 @@ GET /comments/{id} | |||||||
| } | } | ||||||
| </code> | </code> | ||||||
| Вообще в 9 случаях из 10 (а фактически — всегда, когда размер ответа невелик) во всех отношениях лучше из любой модифицирующей операции возвращать полное состояние сущности в том же формате, что и из операции доступа на чтение.</li> | Вообще в 9 случаях из 10 (а фактически — всегда, когда размер ответа невелик) во всех отношениях лучше из любой модифицирующей операции возвращать полное состояние сущности в том же формате, что и из операции доступа на чтение.</li> | ||||||
|  | </ul> | ||||||
|  | <h4 id="9">9. Идемпотентность</h4> | ||||||
|  | <p>Все эндпойнты должны быть идемпотентны. Напомним, идемпотентность — это следующее свойство: повторный вызов той же операции с теми же параметрами не изменяет результат. Поскольку мы обсуждаем в первую очередь клиент-серверное взаимодействие, узким местом в котором является ненадежность сетевой составляющей, повтор запроса при обрыве соединения — не исключительная ситуация, а норма жизни.</p> | ||||||
|  | <p>Там, где идемпотентность не может быть обеспечена естественным образом, необходимо добавить явный параметр — ключ идемпотентности или ревизию.</p> | ||||||
|  | <ul> | ||||||
|  | <li><strong>Плохо</strong> | ||||||
|  | <code> | ||||||
|  | // Создаёт заказ | ||||||
|  | POST /orders | ||||||
|  | </code> | ||||||
|  | Повтор запроса создаст два заказа!</li> | ||||||
|  | <li><strong>Хорошо</strong> | ||||||
|  | <code> | ||||||
|  | // Создаёт заказ | ||||||
|  | POST /v1/orders | ||||||
|  | X-Idempotency-Token: <случайная строка> | ||||||
|  | </code> | ||||||
|  | Клиент на своей стороне запоминает X-Idempotency-Token, и, в случае автоматического повторного перезапроса, обязан его сохранить. Сервер на своей стороне проверяет токен и, если заказ с таким токеном уже существует для этого клиента, не даёт создать заказ повторно.<br /> | ||||||
|  | Альтернатива: | ||||||
|  | <code> | ||||||
|  | // Создаёт черновик заказа | ||||||
|  | POST /v1/orders | ||||||
|  | → | ||||||
|  | { "draft_id" } | ||||||
|  | </code> | ||||||
|  | <code> | ||||||
|  | // Подтверждает черновик заказа | ||||||
|  | PUT /v1/orders/drafts/{draft_id} | ||||||
|  | { "confirmed": true } | ||||||
|  | </code> | ||||||
|  | Создание черновика заказа — необязывающая операция, которая не приводит ни к каким последствиям, поэтому допустимо создавать черновики без токена идемпотентности. | ||||||
|  | Операция подтверждения заказа — уже естественным образом идемпотентна, для неё <code>draft_id</code> играет роль ключа идемпотентности.</li> | ||||||
|  | </ul> | ||||||
|  | <h4 id="10">10. Кэширование</h4> | ||||||
|  | <p>В клиент-серверном API, как правило, сеть и ресурс сервера не бесконечны, поэтому кэширование на клиенте результатов операции является стандартным действием.</p> | ||||||
|  | <p>Желательно в такой ситуации внести ясность; если не из сигнатур операций, то хотя бы из документации должно быть понятно, каким образом можно кэшировать результат.</p> | ||||||
|  | <ul> | ||||||
|  | <li><p><strong>Плохо</strong></p> | ||||||
|  | <pre><code>// Возвращает цену лунго в кафе, | ||||||
|  | // ближайшем к указанной точке | ||||||
|  | GET /price?recipe=lungo&longitude={longitude}&latitude={latitude} | ||||||
|  | → | ||||||
|  | { "currency_code", "price" } | ||||||
|  | </code></pre> | ||||||
|  | <p>Возникает два вопроса:</p> | ||||||
|  | <ul> | ||||||
|  | <li>в течение какого времени эта цена действительна?</li> | ||||||
|  | <li>на каком расстоянии от указанной точки цена всё ещё действительна?  </li></ul> | ||||||
|  | <p>Если на первый вопрос легко ответить введением стандартных заголовков Cache-Control, то для второго вопроса готовых решений нет. В ситуации, когда кэш нужен и по временной, и по пространственной координате следует поступить примерно так:</p> | ||||||
|  | <pre><code>// Возвращает предложение: за какую сумму | ||||||
|  | // наш сервис готов приготовить лунго | ||||||
|  | GET /price?recipe=lungo&longitude={longitude}&latitude={latitude} | ||||||
|  | → | ||||||
|  | { | ||||||
|  |   "offer": { | ||||||
|  |     "id", | ||||||
|  |     "currency_code", | ||||||
|  |     "price", | ||||||
|  |     "terms": { | ||||||
|  |       // До какого времени валидно предложение | ||||||
|  |       "valid_until", | ||||||
|  |       // Где валидно предложение: | ||||||
|  |       // * город | ||||||
|  |       // * географический объект | ||||||
|  |       // * … | ||||||
|  |       "valid_within" | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </code></pre></li> | ||||||
|  | </ul> | ||||||
|  | <h4 id="11-1">11. Пагинация, фильтрация и курсоры</h4> | ||||||
|  | <p>Любой эндпойнт, возвращающий массивы данных, должен содержать пагинацию. Никаких исключений в этом правиле быть не может.</p> | ||||||
|  | <p>Любой эндпойнт, возвращающий изменяемые данные постранично, должен обеспечивать возможность эти данные перебрать.</p> | ||||||
|  | <ul> | ||||||
|  | <li><p><strong>Плохо</strong>:</p> | ||||||
|  | <pre><code>// Возвращает указанный limit записей, | ||||||
|  | // отсортированных по дате создания | ||||||
|  | // начиная с записи с номером offset | ||||||
|  | GET /records?limit=10&offset=100 | ||||||
|  | </code></pre> | ||||||
|  | <p>На первый взгляд это самый что ни на есть стандартный способ организации пагинации в API. Однако зададим себе три вопроса:</p> | ||||||
|  | <ol> | ||||||
|  | <li>Каким образом клиент узнает о появлении новых записей в начале списка?<br /> | ||||||
|  | Легко заметить, что клиент может только попытаться повторить первый запрос и сличить идентификаторы с запомненным началом списка. Но что делать, если добавленное количество записей превышает limit? Представим себе ситуацию:<ul> | ||||||
|  | <li>клиент обрабатывает записи в порядке поступления;</li> | ||||||
|  | <li>произошла какая-то проблема, и накопилось большое количество необработанных записей;</li> | ||||||
|  | <li>клиент запрашивает новые записи (offset=0), однако не находит на первой странице известных идентификаторов — новых записей накопилось больше, чем limit;</li> | ||||||
|  | <li>клиент вынужден продолжить перебирать записи (увеличивая offset), пока не доберётся до последней известной ему; всё это время клиент простаивает;</li> | ||||||
|  | <li>таким образом может сложиться ситуация, когда клиент вообще никогда не обработает всю очередь, т.к. будет занят беспорядочным линейным перебором.</li></ul></li> | ||||||
|  | <li>Что произойдёт, если при переборе списка одна из записей в уже перебранной части будет удалена?<br /> | ||||||
|  | Произойдёт следующее: клиент пропустит одну запись и никогда не сможет об этом узнать.</li> | ||||||
|  | <li>Какие параметры кэширования мы можем выставить на этот эндпойнт?<br /> | ||||||
|  | Никакие: повторяя запрос с теми же limit-offset мы каждый раз получаем новый набор записей.</li></ol> | ||||||
|  | <p><strong>Хорошо</strong>: в таких однонаправленных списках пагинация должна быть организована по тому ключу, порядок которых фиксирован. Например, вот так:</p> | ||||||
|  | <pre><code>// Возвращает указанный limit записей, | ||||||
|  | // отсортированных по дате создания, | ||||||
|  | // начиная с первой записи, созданной позднее, | ||||||
|  | // чем запись с указанным id | ||||||
|  | GET /records?older_than={record_id}&limit=10 | ||||||
|  | // Возвращает указанный limit записей, | ||||||
|  | // отсортированных по дате создания, | ||||||
|  | // начиная с первой записи, созданной раньше, | ||||||
|  | // чем запись с указанным id | ||||||
|  | GET /records?newer_than={record_id}&limit=10 | ||||||
|  | </code></pre> | ||||||
|  | <p>При такой организации клиенту не надо заботиться об удалении или добавлении записей в уже перебранной части списка: он продолжает перебор по идентификатору известной записи — первой известной, если надо получить новые записи; последней известной, если надо продолжить перебор. | ||||||
|  | Если операции удаления записей нет, то такие запросы можно свободно кэшировать — по одному и тому же URL будет всегда возвращаться один и тот же набор записей.<br /> | ||||||
|  | Другой вариант организации таких списков — возврат курсора <code>cursor</code>, который используется вместо <code>record_id</code>, что делает интерфейсы универсальнее.</p></li> | ||||||
|  | <li><p><strong>Плохо</strong>:</p> | ||||||
|  | <pre><code>// Возвращает указанный limit записей, | ||||||
|  | // отсортированных по полю sort_by | ||||||
|  | // в порядке sort_order, | ||||||
|  | // начиная с записи с номером offset | ||||||
|  | GET /records?sort_by=date_modified&sort_order=desc&limit=10&offset=100 | ||||||
|  | </code></pre> | ||||||
|  | <p>Сортировка по дате модификации обычно означает, что данные могут меняться. Иными словами, между запросом первой порции данных и запросом второй порции данных какая-то запись может измениться; она просто пропадёт из перечисления, т.к. автоматически попадает на первую страницу. Клиент никогда не получит те записи, которые менялись во время перебора, и у него даже нет способа узнать о самом факте такого пропуска. Помимо этого отметим, что такое API нерасширяемо — невозможно добавить сортировку по двум или более полям.</p> | ||||||
|  | <p><strong>Хорошо</strong>: в представленной постановке задача, вообще говоря, не решается. Список записей по дате изменения всегда будет непредсказуемо изменяться, поэтому необходимо изменить сам подход к формированию данных, одним из двух способов.</p> | ||||||
|  | <ul> | ||||||
|  | <li><p>Фиксировать порядок в момент обработки запроса; т.е. сервер формирует полный список и сохраняет его в неизменяемом виде:</p> | ||||||
|  | <pre><code>// Создаёт представление по указанным параметрам | ||||||
|  | POST /v1/record-views | ||||||
|  | { | ||||||
|  |   sort_by: [ | ||||||
|  |     { "field": "date_modified", "order": "desc" } | ||||||
|  |   ] | ||||||
|  | } | ||||||
|  | → | ||||||
|  | { "id", "cursor" } | ||||||
|  | </code></pre> | ||||||
|  | <pre><code>// Позволяет получить часть представления | ||||||
|  | GET /v1/record-views/{id}?cursor={cursor} | ||||||
|  | </code></pre> | ||||||
|  | <p>Т.к. созданное представление уже неизменяемо, доступ к нему можно организовать как угодно: через курсор, limit/offest, заголовок Range и т.д. Однако надо иметь в виду, что при переборе таких списков может получиться так, что порядок будет нарушен: записи, изменённые уже после генерации представления, будут находиться не на своих местах (либо быть неактуальны, если запись копируется целиком).</p></li> | ||||||
|  | <li><p>Гарантировать строгий неизменяемый порядок записей, например, путём введения понятия события изменения записи:</p> | ||||||
|  | <pre><code>POST /v1/records/modified/list | ||||||
|  | { | ||||||
|  |   // Опционально | ||||||
|  |   "cursor" | ||||||
|  | } | ||||||
|  | → | ||||||
|  | { | ||||||
|  |   "modified": [ | ||||||
|  |     { "date", "record_id" } | ||||||
|  |   ], | ||||||
|  |   "cursor" | ||||||
|  | } | ||||||
|  | </code></pre> | ||||||
|  | <p>Недостатком этой схемы является необходимость заводить отдельные списки под каждый вид сортировки.</p></li></ul></li> | ||||||
| </ul><div class="page-break"></div></article> | </ul><div class="page-break"></div></article> | ||||||
| </body></html> | </body></html> | ||||||
							
								
								
									
										
											BIN
										
									
								
								docs/API.ru.pdf
									
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docs/API.ru.pdf
									
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							| @@ -361,5 +361,35 @@ POST /v1/runtimes | |||||||
|  |  | ||||||
| Может показаться, что соблюдение правила изоляции уровней абстракции является избыточным и заставляет усложнять интерфейс. И это в действительности так: важно понимать, что никакая гибкость, логичность, читабельность и расширяемость не бывает бесплатной. Можно построить API так, чтобы оно выполняло свою функцию с минимальными накладными расходами, по сути — дать интерфейс к микроконтроллерам кофе-машины. Однако пользоваться им будет крайне неудобно, и расширяемость такого API будет нулевой. | Может показаться, что соблюдение правила изоляции уровней абстракции является избыточным и заставляет усложнять интерфейс. И это в действительности так: важно понимать, что никакая гибкость, логичность, читабельность и расширяемость не бывает бесплатной. Можно построить API так, чтобы оно выполняло свою функцию с минимальными накладными расходами, по сути — дать интерфейс к микроконтроллерам кофе-машины. Однако пользоваться им будет крайне неудобно, и расширяемость такого API будет нулевой. | ||||||
|  |  | ||||||
| Выделение уровней абстракции — прежде всего _логическая_ процедура: как мы объясняем себе и разработчику, из чего состоит наш API. **Абстрагируемая дистанция между сущностями существует объективно**, каким бы образом мы ни написали конкретные интерфейсы. Наша задача состоит только лишь в том, чтобы эта дистанция была разделена на уровни _явно_. Чем неявнее разведены уровни абстракции (или хуже того, перемешаны) уровни абстракции, тем сложнее будет разобраться в вашем API и тем хуже будет написан использующий его код. | Выделение уровней абстракции — прежде всего _логическая_ процедура: как мы объясняем себе и разработчику, из чего состоит наш API. **Абстрагируемая дистанция между сущностями существует объективно**, каким бы образом мы ни написали конкретные интерфейсы. Наша задача состоит только лишь в том, чтобы эта дистанция была разделена на уровни _явно_. Чем более неявно разведены (или, хуже того, перемешаны) уровни абстракции, тем сложнее будет разобраться в вашем API, и тем хуже будет написан использующий его код. | ||||||
|  |  | ||||||
|  | #### Потоки данных | ||||||
|  |  | ||||||
|  | Полезное упражнение, позволяющее рассмотреть иерархию уровней абстракции API — исключить из рассмотрения все частности и построить — в голове или на бумаге — дерево потоков данных: какие данные протекают через объекты вашего API и как видоизменяются на каждом этапе. | ||||||
|  |  | ||||||
|  | Это упражнение не только полезно для проектирования, но и, помимо прочего, является единственным способом развивать большие (в смысле номенклатуры объектов) API. Человеческая память не безгранична, и любой активно развивающийся проект достаточно быстро станет настолько большим, что удержать в голове всю иерархию сущностей со всеми полями и методами станет невозможно. Но вот держать в уме схему потоков данных обычно вполне возможно — или, во всяком случае, получается держать в уме на порядок больший фрагмент дерева сущностей API. | ||||||
|  |  | ||||||
|  | Какие потоки данных мы имеем в нашем кофейном API? | ||||||
|  |  | ||||||
|  |   1. Данные с сенсоров — объёмы кофе / воды / стакана. Это низший из доступных нам уровней данных, здесь мы не можем ничего изменить или переформулировать. | ||||||
|  |  | ||||||
|  |   2. Непрерывный поток данных сенсоров мы преобразуем в дискретные статусы исполнения команд, вводя в него понятия, не существующие в предметной области. API кофе-машины не предоставляет нам понятий «кофе наливается» или «стакан ставится» — это наше программное обеспечение трактует поступающие потоки данных от сенсоров, вводя новые понятия: если наблюдаемый объём (кофе или воды) меньше целевого — значит, процесс не закончен; если объём достигнут — значит, необходимо сменить статус исполнения и выполнить следующее действие.   | ||||||
|  |   Важно отметить, что мы не просто вычисляем какие-то новые параметры из имеющихся данных сенсоров: мы сначала создаём новый кортеж данных более высокого уровня — «программа исполнения» как последовательность шагов и условий — и инициализируем его начальные значения. Без этого контекста определить, что собственно происходит с кофе-машиной невозможно. | ||||||
|  |  | ||||||
|  |   3. Обладая логическими данными о состоянии исполнения программы, мы можем (вновь через создание нового, более высокоуровневого контекста данных!) свести данные от двух типов API к единому формату: исполнение операции создания напитка и её логические параметры: целевой рецепт, объём, готов ли заказ. | ||||||
|  |  | ||||||
|  | Таким образом, каждый уровень абстракции нашего API соответствует какому-то обобщению и обогащению потока данных, преобразованию его из терминов нижележащего (и вообще говоря бесполезного для потребителя) контекста в термины вышестоящего контекста. | ||||||
|  |  | ||||||
|  | Дерево можно развернуть и в обратную сторону: | ||||||
|  |  | ||||||
|  |   1. На уровне заказа мы задаём его логические параметры: рецепт, объём, место исполнения и набор допустимых статусов заказа. | ||||||
|  |  | ||||||
|  |   2. На уровне исполнения мы обращаемся к данным уровня заказа и создаём более низкоуровневый контекст: программа исполнения в виде последовательности шагов, их параметров и условий перехода от одного шага к другому и начальное состояние (с какого шага начать исполнение и с какими параметрами). | ||||||
|  |  | ||||||
|  |   3. На уровне рантайма мы обращаемся к целевым значениям (какую операцию выполнить и какой целевой объём) и преобразуем её в набор микрокоманд API кофе-машины и набор статусов исполнения каждой команды. | ||||||
|  |  | ||||||
|  | Если обратиться к описанному в начале главы «плохому» решению (предполагающему самостоятельное определение факта готовности заказа разработчиком), то мы увидим, что и с точки зрения потоков данных происходит смешение понятий:  | ||||||
|  |   * с одной стороны, в контексте заказа оказываются данные (объём кофе), «просочившиеся» откуда-то с физического уровня; тем самым, уровни абстракции непоправимо смешиваются без возможности их разделить; | ||||||
|  |   * с другой стороны, сам контекст заказа неполноценный: он не задаёт новых мета-переменных, которые отсутствуют на более низких уровнях абстракции (статус заказа), не инициализирует их и не предоставляет правил работы. | ||||||
|  |  | ||||||
|  |   Более подробно о контекстах данных мы поговорим в разделе II. Здесь же ограничимся следующим выводом: потоки данных и их преобразования можно и нужно рассматривать как некоторый срез, который, с одной стороны, помогает нам правильно разделить уровни абстракции, а с другой — проверить, что наши теоретические построения действительно работают так, как нужно. | ||||||
| @@ -3,8 +3,8 @@ | |||||||
| Исходя из описанного в предыдущей главе, мы понимаем, что иерархия абстракций в нашем гипотетическом проекте должна выглядеть примерно так: | Исходя из описанного в предыдущей главе, мы понимаем, что иерархия абстракций в нашем гипотетическом проекте должна выглядеть примерно так: | ||||||
|  |  | ||||||
|   * пользовательский уровень (те сущности, с которыми непосредственно взаимодействует пользователь и сформулированы в понятных для него терминах; например, заказы и виды кофе); |   * пользовательский уровень (те сущности, с которыми непосредственно взаимодействует пользователь и сформулированы в понятных для него терминах; например, заказы и виды кофе); | ||||||
|   * исполнительный уровень (те сущности, которые отвечают за переформулирование заказа в машинные термины); |   * уровень исполнения программ (те сущности, которые отвечают за преобразование заказа в машинные термины); | ||||||
|   * физический уровень (непосредственно сами датчики машины). |   * уровень рантайма для API второго типа (сущности, отвечающие за state-машину выполнения заказа). | ||||||
|  |  | ||||||
| Теперь нам необходимо определить ответственность каждой сущности: в чём смысл её существования в рамках нашего API, какие действия можно выполнять с самой сущностью, а какие — делегировать другим объектам. Фактически, нам нужно применить «зачем-принцип» к каждой отдельной сущности нашего API. | Теперь нам необходимо определить ответственность каждой сущности: в чём смысл её существования в рамках нашего API, какие действия можно выполнять с самой сущностью, а какие — делегировать другим объектам. Фактически, нам нужно применить «зачем-принцип» к каждой отдельной сущности нашего API. | ||||||
|  |  | ||||||
| @@ -12,56 +12,31 @@ | |||||||
|  |  | ||||||
| В нашем умозрительном примере получится примерно так: | В нашем умозрительном примере получится примерно так: | ||||||
|  |  | ||||||
|   1. Заказ `order` — описывает некоторую логическую единицу взаимодействия с пользователем. Заказ можно: |   1. Сущности уровня пользователя (те, работая с которыми, разработчик непосредственно решает задачи пользователя). | ||||||
|     * создавать |       * Заказ `order` — описывает некоторую логическую единицу взаимодействия с пользователем. Заказ можно: | ||||||
|     * проверять статус |         * создавать; | ||||||
|     * получать или отменять |         * проверять статус; | ||||||
|   2. Рецепт `recipe` — описывает «идеальную модель» вида кофе, его потребительские свойства. Рецепт в данном контексте для нас неизменяемая сущность, которую можно только просмотреть и выбрать; |         * получать или отменять. | ||||||
|   3. Задание `task` — описывает некоторые задачи, на которые декомпозируются заказ; |       * Рецепт `recipe` — описывает «идеальную модель» вида кофе, его потребительские свойства. Рецепт в данном контексте для нас неизменяемая сущность, которую можно только просмотреть и выбрать. | ||||||
|   4. Кофе-машина `cofee-machine` — модель объекта реального мира. Мы можем: |       * Кофе-машина `coffee-machine` — модель объекта реального мира. Из описания кофе-машины мы, в частности, должны извлечь её положение в пространстве и предоставляемые опции (о чём подробнее поговорим ниже). | ||||||
|     * получать статусы датчиков |   2. Сущности уровня управления исполнением (те, работая с которыми, можно непосредственно исполнить заказ): | ||||||
|     * отправлять команды и проверять их исполнение |       * Программа `program` — описывает доступные возможности конкретной кофе-машины. Программы можно только просмотреть. | ||||||
|  |       * Селектор программ `programs/matcher` — позволяет связать рецепт и программу исполнения, т.е. фактически выяснить набор данных, необходимых для приготовления конкретного рецепта на конкретной кофе-машине. Селектор работает только на выбор нужной программы. | ||||||
|  |       * Запуск программы `programs/run` — конкретный факт исполнения программы на конкретной кофе-машине. Запуски можно: | ||||||
|  |         * инициировать (создавать); | ||||||
|  |         * отменять; | ||||||
|  |         * проверять состояние запуска. | ||||||
|  |   3. Сущности уровня программ исполнения (те, работая с которыми, можно непосредственно управлять состоянием кофе-машины через API второго типа). | ||||||
|  |       * Рантайм `runtime` — контекст исполнения программы, т.е. состояние всех переменных. Рантаймы можно: | ||||||
|  |         * создавать; | ||||||
|  |         * проверять статус; | ||||||
|  |         * терминировать. | ||||||
|  |  | ||||||
| Если внимательно посмотреть на каждый объект, то мы увидим, что, в итоге, каждый объект оказался в смысле своей ответственности составным. Например, `coffee-machine` будет частично оперировать реальными командами кофе-машины *и* представлять их состояние в каком-то машиночитаемом виде. | Если внимательно посмотреть на каждый объект, то мы увидим, что, в итоге, каждый объект оказался в смысле своей ответственности составным. Например, `program` будет оперировать данными высшего уровня (рецепт и кофе-машина), дополняя их терминами своего уровня (идентификатор запуска). Это совершенно нормально: API должно связывать контексты. | ||||||
|  |  | ||||||
| #### Декомпозиция интерфейсов | #### Сценарии использования | ||||||
|  |  | ||||||
| На этапе разделения уровней абстракции мы упомянули, что одна из целей такого разделения — добиться возможности безболезненно сменить нижележащие уровни абстракции при добавлении новых технологий или изменении нижележащих протоколов. Декомпозиция интерфейсов — основной инструмент, позволяющий добиться такой гибкости. | На этом уровне, когда наше API уже в целом понятно устроено и спроектированы, мы должны поставить себя на место разработчика и попробовать написать код. | ||||||
|  |  | ||||||
| На этом этапе нам предстоит отделить частное от общего: понять, какие свойства сущностей фиксированы в нашей модели, а какие будут зависеть от реализации. В нашем кофе-примере таким сильно зависящим от имплементации объектом, очевидно, будут сами кофе-машины. Попробуем ещё немного развить наше API: каким образом нам нужно описать модели кофе-машин для того, чтобы предоставить интерфейсы для работы с задачами заказа? |  | ||||||
|  |  | ||||||
| #### Интерфейсы как универсальный паттерн |  | ||||||
|  |  | ||||||
| Как мы убедились в предыдущей главе, выделение интерфейсов крайне важно с точки зрения удобства написания кода. Однако, интерфейсы играют и другую важную роль в проектировании: они позволяют уложить в голове архитектуру API целиком. |  | ||||||
|  |  | ||||||
| Любой сколько-нибудь крупный API рано или поздно обрастает разнообразной номенклатурой сущностей, их свойст и методов, как в силу того, что в одном объекте «сходятся» несколько предметных областей, так и в силу появления со временем разнообразной вспомогательной и дополнительной функциональности. Особенно сложной номенклатура объектов и их методов становится в случае появления альтернативных реализаций одного и того же интерфейса. |  | ||||||
|  |  | ||||||
| Человеческие возможности небезграничны: невозможно держать в голове всю номенклатуру объектов. Это осложняет и проектирование API, и рефакторинг, и просто решение возникающих задач по реализации той или иной бизнес-логики. |  | ||||||
|  |  | ||||||
| Держать же в голове схему взаимодействия интерфейсов гораздо проще - как в силу исключения из рассмотрения разнообразных вспомогательных и специфических методов, так и в силу того, что интерфейсы позволяют отделить существенное (в чем смысл конкретной сущности) от несущественного (деталей реализации). |  | ||||||
|  |  | ||||||
| Поскольку задача выделения интерфейсов есть задача удобного манипулирования сущностями в голове разработчика, мы рекомендуем при проектировании интерфейсов руководствоваться, прежде всего, здравым смыслом: интерфейсы должны быть ровно настолько сложны, насколько это удобно для человеческого восприятия (а лучше даже чуть проще). В простейших случаях это просто означает, что интерфейс должен содержать семь плюс-минуса два свойства/метода. Более сложные интерфейсы должны декомпозироваться в несколько простых. |  | ||||||
|  |  | ||||||
| Это правило существенно важно не только при проектировании api - не забывайте, что ваши пользователи неизбежно столкнутся с той же проблемой - понять примерную архитектуру вашего api, запомнить, что с чем связано в вашей системе. Правильно выделенные интерфейсы помогут и здесь, причём сразу в двух смыслах - как непосредственно работающему с вашим кодом программисту, так и документатору, которому будет гораздо проще описать структуру вашего api, опираясь на дерево интерфейсов. |  | ||||||
|  |  | ||||||
| С другой стороны надо вновь напомнить, что бесплатно ничего не бывает, и выделение интерфейсов - самая «небесплатная» часть процесса разработки API, поскольку в чистом виде приносится в жертву удобство разработки ради построения «правильной» архитектуры: разумеется, код писать куда проще, когда имеешь доступ ко всем объектам API со всей их богатой номенклатурой методов, нежели когда из каждого объекта доступны только пара непосредственно примыкающих интерфейсов, притом с максимально общими методами. |  | ||||||
|  |  | ||||||
| Помимо прочего, это означает, что интерфейсы необходимо выделять там, где это актуально решаемой задаче - прежде всего, в точках будущего роста и там, где возможны альтернативные реализации. Чем проще API, тем меньше нужда в интерфейсах, и наоборот: сложное API требует интерфейсов практически всюду просто для того, чтобы ограничить разрастание излишне сильной связанности и при этом не сойти с ума. |  | ||||||
|  |  | ||||||
| В пределе в сложном api должна сложиться ситуация, при которой все объекты взаимодействуют друг с другом только как интерфейсы — нет ни одной публичной сигнатуры, принимающей конкретный объект, а не его интерфейс. Разумеется, достичь такого уровня абстракции практически невозможно - почти в любой системе есть глобальные объекты, разнообразные технические сущности (имплементации стандартных структур данных, например); наконец, невозможно «спрятать» за интерфейсы системные объекты или сущности физического уровня. |  | ||||||
|  |  | ||||||
| #### Информационные контексты |  | ||||||
|  |  | ||||||
| При выделении интерфейсов и вообще при проектировании api бывает полезно взглянуть на иерархию абстракций с другой точки зрения, а именно: каким образом информация «протекает» через нашу иерархию. |  | ||||||
|  |  | ||||||
| Вспомним, что одним из критериев отделения уровней абстракции является переход от структур данных одной предметной области к структурам данных другой. В рамках нашего примера через иерархию наших объектов происходит трансляция данных реального мира — «железные» кофе-машины, которые готовят реально существующие напитки — в виртуальные интерфейсы «заказов». |  | ||||||
|  |  | ||||||
| Если обратиться к правилу «неперепрыгивания» через уровни абстракции, то с точки зрения потоков данных оно формулируется так: |  | ||||||
|  |  | ||||||
|   * каждый объект в иерархии абстракций должен оперировать данными согласно своему уровню иерархии; |  | ||||||
|   * преобразованием данных имеют право заниматься только те объекты, в чьи непосредственные обязанности это входит. |  | ||||||
|  |  | ||||||
| Дерево информационных контекстов (какой объект обладает какой информацией, и кто является транслятором из одного контекста в другой), по сути, представляет собой «срез» нашего дерева иерархии интерфейсов; выделение такого среза позволяет проще и удобнее удерживать в голове всю архитектуру проекта. |  | ||||||
|  |  | ||||||
| // TODO | // TODO | ||||||
| // Хелперы, бойлерплейт | // Хелперы, бойлерплейт | ||||||
| @@ -40,7 +40,7 @@ | |||||||
|     **Хорошо**: |     **Хорошо**: | ||||||
|     ``` |     ``` | ||||||
|     // Возвращает агрегированную статистику заказов за указанный период |     // Возвращает агрегированную статистику заказов за указанный период | ||||||
|     POST /orders/statistics/aggregate |     POST /v1/orders/statistics/aggregate | ||||||
|     { "start_date", "end_date" } |     { "start_date", "end_date" } | ||||||
|     ``` |     ``` | ||||||
|  |  | ||||||
| @@ -50,11 +50,11 @@ | |||||||
|  |  | ||||||
| **1.1.** Если операция модифицирующая, это должно быть очевидно из сигнатуры. В частности, не может быть модифицирующих операций за `GET`. | **1.1.** Если операция модифицирующая, это должно быть очевидно из сигнатуры. В частности, не может быть модифицирующих операций за `GET`. | ||||||
|  |  | ||||||
| **1.2.** Если операция асинхронная, это должно быть очевидно из сигнатуры, **либо** должна существовать конвенция именования, позволяющая отличаться синхронные операции от асинхронных. | **1.2.** Если в номенклатуре вашего API есть как синхронные операции, так и асинхронные, то (а)синхронность должна быть очевидна из сигнатур, **либо** должна существовать конвенция именования, позволяющая отличать синхронные операции от асинхронных. | ||||||
|  |  | ||||||
| #### 2. Использованные стандарты указывайте явно | #### 2. Использованные стандарты указывайте явно | ||||||
|  |  | ||||||
| К сожалению, человечество не в состоянии договориться о таких простейших вещах, как «начинать ли неделю с понедельника или с воскресенья», что уж говорить о каких-то более сложных стандартах. | К сожалению, человечество не в состоянии договориться о таких простейших вещах, как «с какого дня начинается неделя», что уж говорить о каких-то более сложных стандартах. | ||||||
|  |  | ||||||
| Поэтому _всегда_ указывайте, по какому конкретно стандарту вы отдаёте те или иные величины. Исключения возможны только там, где вы на 100% уверены, что в мире существует только один стандарт для этой сущности, и всё население земного шара о нём в курсе: | Поэтому _всегда_ указывайте, по какому конкретно стандарту вы отдаёте те или иные величины. Исключения возможны только там, где вы на 100% уверены, что в мире существует только один стандарт для этой сущности, и всё население земного шара о нём в курсе: | ||||||
|  |  | ||||||
| @@ -70,7 +70,7 @@ | |||||||
|  |  | ||||||
| Отдельное следствие из этого правила — денежные величины *всегда* должны сопровождаться указанием кода валюты. | Отдельное следствие из этого правила — денежные величины *всегда* должны сопровождаться указанием кода валюты. | ||||||
|  |  | ||||||
| Также следует отметить, что в некоторых областях ситуация со стандартами настолько плоха, что как ни сделай — кто-то останется недовольным. Классический пример такого рода — порядок геокоординат ("широта-долгота" против "долгота-широта"). Здесь, увы, есть только один работающий метод борьбы с фрустрацией — «блокнот душевного спокойствия», который будет описан в разделе II. | Также следует отметить, что в некоторых областях ситуация со стандартами настолько плоха, что как ни сделай — кто-то останется недовольным. Классический пример такого рода — порядок географических координат ("широта-долгота" против "долгота-широта"). Здесь, увы, есть только один работающий метод борьбы с фрустрацией — «блокнот душевного спокойствия», который будет описан в разделе II. | ||||||
|  |  | ||||||
| #### 3. Сохраняйте точность дробных чисел | #### 3. Сохраняйте точность дробных чисел | ||||||
|  |  | ||||||
| @@ -97,7 +97,7 @@ | |||||||
|     strpbrk (str1, str2) |     strpbrk (str1, str2) | ||||||
|     ``` |     ``` | ||||||
|     Возможно, автору этого API казалось, что аббревиатура `pbrk` что-то значит для читателя, но он явно ошибся. К тому же, невозможно сходу понять, какая из строк `str1`, `str2` является набором символов для поиска.   |     Возможно, автору этого API казалось, что аббревиатура `pbrk` что-то значит для читателя, но он явно ошибся. К тому же, невозможно сходу понять, какая из строк `str1`, `str2` является набором символов для поиска.   | ||||||
|     **Хорошо**: `str_search_for_characters(lookup_character_set, str)`   |     **Хорошо**: `str_search_for_characters (lookup_character_set, str)`   | ||||||
|     Однако необходимость существования такого метода вообще вызывает сомнения, достаточно было бы иметь удобную функцию поиска подстроки с нужными параметрами. Аналогично сокращение `string` до `str` выглядит совершенно бессмысленным, но, увы, является устоявшимся для большого количества предметных областей. |     Однако необходимость существования такого метода вообще вызывает сомнения, достаточно было бы иметь удобную функцию поиска подстроки с нужными параметрами. Аналогично сокращение `string` до `str` выглядит совершенно бессмысленным, но, увы, является устоявшимся для большого количества предметных областей. | ||||||
|  |  | ||||||
| #### 6. Тип поля должен быть ясен из его названия | #### 6. Тип поля должен быть ясен из его названия | ||||||
| @@ -122,7 +122,7 @@ | |||||||
|     GET /coffee-machines/functions |     GET /coffee-machines/functions | ||||||
|     ``` |     ``` | ||||||
|     Слово "functions" многозначное; может означать и встроенные функции, и написанный код, и состояние (функционирует-не функционирует).   |     Слово "functions" многозначное; может означать и встроенные функции, и написанный код, и состояние (функционирует-не функционирует).   | ||||||
|     **Хорошо**: `GET /coffee-machines/builtin-functions-list` |     **Хорошо**: `GET /v1/coffee-machines/builtin-functions-list` | ||||||
|  |  | ||||||
| #### 7. Подобные сущности должны называться подобно и вести себя подобным образом | #### 7. Подобные сущности должны называться подобно и вести себя подобным образом | ||||||
|  |  | ||||||
| @@ -175,14 +175,14 @@ | |||||||
|     **Хорошо**: |     **Хорошо**: | ||||||
|     ``` |     ``` | ||||||
|     // Создаёт комментарий и возвращает его |     // Создаёт комментарий и возвращает его | ||||||
|     POST /comments |     POST /v1/comments | ||||||
|     { "content" } |     { "content" } | ||||||
|     → |     → | ||||||
|     { "comment_id", "published", "action_required", "content" } |     { "comment_id", "published", "action_required", "content" } | ||||||
|     ``` |     ``` | ||||||
|     ``` |     ``` | ||||||
|     // Возвращает комментарий по его id |     // Возвращает комментарий по его id | ||||||
|     GET /comments/{id} |     GET /v1/comments/{id} | ||||||
|     → |     → | ||||||
|     { /* в точности тот же формат,  |     { /* в точности тот же формат,  | ||||||
|        что и в ответе POST /comments */ |        что и в ответе POST /comments */ | ||||||
| @@ -191,3 +191,171 @@ | |||||||
|     ``` |     ``` | ||||||
|     Вообще в 9 случаях из 10 (а фактически — всегда, когда размер ответа невелик) во всех отношениях лучше из любой модифицирующей операции возвращать полное состояние сущности в том же формате, что и из операции доступа на чтение. |     Вообще в 9 случаях из 10 (а фактически — всегда, когда размер ответа невелик) во всех отношениях лучше из любой модифицирующей операции возвращать полное состояние сущности в том же формате, что и из операции доступа на чтение. | ||||||
|  |  | ||||||
|  | #### 9. Идемпотентность | ||||||
|  |  | ||||||
|  | Все эндпойнты должны быть идемпотентны. Напомним, идемпотентность — это следующее свойство: повторный вызов той же операции с теми же параметрами не изменяет результат. Поскольку мы обсуждаем в первую очередь клиент-серверное взаимодействие, узким местом в котором является ненадежность сетевой составляющей, повтор запроса при обрыве соединения — не исключительная ситуация, а норма жизни. | ||||||
|  |  | ||||||
|  | Там, где идемпотентность не может быть обеспечена естественным образом, необходимо добавить явный параметр — ключ идемпотентности или ревизию. | ||||||
|  |  | ||||||
|  |   * **Плохо** | ||||||
|  |     ``` | ||||||
|  |     // Создаёт заказ | ||||||
|  |     POST /orders | ||||||
|  |     ``` | ||||||
|  |     Повтор запроса создаст два заказа! | ||||||
|  |   * **Хорошо** | ||||||
|  |     ``` | ||||||
|  |     // Создаёт заказ | ||||||
|  |     POST /v1/orders | ||||||
|  |     X-Idempotency-Token: <случайная строка> | ||||||
|  |     ``` | ||||||
|  |     Клиент на своей стороне запоминает X-Idempotency-Token, и, в случае автоматического повторного перезапроса, обязан его сохранить. Сервер на своей стороне проверяет токен и, если заказ с таким токеном уже существует для этого клиента, не даёт создать заказ повторно.   | ||||||
|  |     Альтернатива: | ||||||
|  |     ``` | ||||||
|  |     // Создаёт черновик заказа | ||||||
|  |     POST /v1/orders | ||||||
|  |     → | ||||||
|  |     { "draft_id" } | ||||||
|  |     ``` | ||||||
|  |     ``` | ||||||
|  |     // Подтверждает черновик заказа | ||||||
|  |     PUT /v1/orders/drafts/{draft_id} | ||||||
|  |     { "confirmed": true } | ||||||
|  |     ``` | ||||||
|  |     Создание черновика заказа — необязывающая операция, которая не приводит ни к каким последствиям, поэтому допустимо создавать черновики без токена идемпотентности. | ||||||
|  |     Операция подтверждения заказа — уже естественным образом идемпотентна, для неё `draft_id` играет роль ключа идемпотентности. | ||||||
|  |  | ||||||
|  | #### 10. Кэширование | ||||||
|  |  | ||||||
|  | В клиент-серверном API, как правило, сеть и ресурс сервера не бесконечны, поэтому кэширование на клиенте результатов операции является стандартным действием. | ||||||
|  |  | ||||||
|  | Желательно в такой ситуации внести ясность; если не из сигнатур операций, то хотя бы из документации должно быть понятно, каким образом можно кэшировать результат. | ||||||
|  |  | ||||||
|  |   * **Плохо** | ||||||
|  |     ``` | ||||||
|  |     // Возвращает цену лунго в кафе, | ||||||
|  |     // ближайшем к указанной точке | ||||||
|  |     GET /price?recipe=lungo&longitude={longitude}&latitude={latitude} | ||||||
|  |     → | ||||||
|  |     { "currency_code", "price" } | ||||||
|  |     ``` | ||||||
|  |     Возникает два вопроса: | ||||||
|  |       * в течение какого времени эта цена действительна? | ||||||
|  |       * на каком расстоянии от указанной точки цена всё ещё действительна?   | ||||||
|  |  | ||||||
|  |     Если на первый вопрос легко ответить введением стандартных заголовков Cache-Control, то для второго вопроса готовых решений нет. В ситуации, когда кэш нужен и по временной, и по пространственной координате следует поступить примерно так: | ||||||
|  |     ``` | ||||||
|  |     // Возвращает предложение: за какую сумму | ||||||
|  |     // наш сервис готов приготовить лунго | ||||||
|  |     GET /price?recipe=lungo&longitude={longitude}&latitude={latitude} | ||||||
|  |     → | ||||||
|  |     { | ||||||
|  |       "offer": { | ||||||
|  |         "id", | ||||||
|  |         "currency_code", | ||||||
|  |         "price", | ||||||
|  |         "terms": { | ||||||
|  |           // До какого времени валидно предложение | ||||||
|  |           "valid_until", | ||||||
|  |           // Где валидно предложение: | ||||||
|  |           // * город | ||||||
|  |           // * географический объект | ||||||
|  |           // * … | ||||||
|  |           "valid_within" | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     ``` | ||||||
|  |  | ||||||
|  | #### 11. Пагинация, фильтрация и курсоры | ||||||
|  |  | ||||||
|  | Любой эндпойнт, возвращающий массивы данных, должен содержать пагинацию. Никаких исключений в этом правиле быть не может. | ||||||
|  |  | ||||||
|  | Любой эндпойнт, возвращающий изменяемые данные постранично, должен обеспечивать возможность эти данные перебрать. | ||||||
|  |   * **Плохо**: | ||||||
|  |     ``` | ||||||
|  |     // Возвращает указанный limit записей, | ||||||
|  |     // отсортированных по дате создания | ||||||
|  |     // начиная с записи с номером offset | ||||||
|  |     GET /records?limit=10&offset=100 | ||||||
|  |     ``` | ||||||
|  |     На первый взгляд это самый что ни на есть стандартный способ организации пагинации в API. Однако зададим себе три вопроса: | ||||||
|  |       1. Каким образом клиент узнает о появлении новых записей в начале списка?   | ||||||
|  |         Легко заметить, что клиент может только попытаться повторить первый запрос и сличить идентификаторы с запомненным началом списка. Но что делать, если добавленное количество записей превышает limit? Представим себе ситуацию: | ||||||
|  |           * клиент обрабатывает записи в порядке поступления; | ||||||
|  |           * произошла какая-то проблема, и накопилось большое количество необработанных записей; | ||||||
|  |           * клиент запрашивает новые записи (offset=0), однако не находит на первой странице известных идентификаторов — новых записей накопилось больше, чем limit; | ||||||
|  |           * клиент вынужден продолжить перебирать записи (увеличивая offset), пока не доберётся до последней известной ему; всё это время клиент простаивает; | ||||||
|  |           * таким образом может сложиться ситуация, когда клиент вообще никогда не обработает всю очередь, т.к. будет занят беспорядочным линейным перебором. | ||||||
|  |       2. Что произойдёт, если при переборе списка одна из записей в уже перебранной части будет удалена?   | ||||||
|  |         Произойдёт следующее: клиент пропустит одну запись и никогда не сможет об этом узнать. | ||||||
|  |       3. Какие параметры кэширования мы можем выставить на этот эндпойнт?   | ||||||
|  |         Никакие: повторяя запрос с теми же limit-offset мы каждый раз получаем новый набор записей. | ||||||
|  |  | ||||||
|  |     **Хорошо**: в таких однонаправленных списках пагинация должна быть организована по тому ключу, порядок которых фиксирован. Например, вот так: | ||||||
|  |     ``` | ||||||
|  |     // Возвращает указанный limit записей, | ||||||
|  |     // отсортированных по дате создания, | ||||||
|  |     // начиная с первой записи, созданной позднее, | ||||||
|  |     // чем запись с указанным id | ||||||
|  |     GET /records?older_than={record_id}&limit=10 | ||||||
|  |     // Возвращает указанный limit записей, | ||||||
|  |     // отсортированных по дате создания, | ||||||
|  |     // начиная с первой записи, созданной раньше, | ||||||
|  |     // чем запись с указанным id | ||||||
|  |     GET /records?newer_than={record_id}&limit=10 | ||||||
|  |     ``` | ||||||
|  |     При такой организации клиенту не надо заботиться об удалении или добавлении записей в уже перебранной части списка: он продолжает перебор по идентификатору известной записи — первой известной, если надо получить новые записи; последней известной, если надо продолжить перебор. | ||||||
|  |     Если операции удаления записей нет, то такие запросы можно свободно кэшировать — по одному и тому же URL будет всегда возвращаться один и тот же набор записей.   | ||||||
|  |     Другой вариант организации таких списков — возврат курсора `cursor`, который используется вместо `record_id`, что делает интерфейсы универсальнее. | ||||||
|  |  | ||||||
|  |   * **Плохо**: | ||||||
|  |     ``` | ||||||
|  |     // Возвращает указанный limit записей, | ||||||
|  |     // отсортированных по полю sort_by | ||||||
|  |     // в порядке sort_order, | ||||||
|  |     // начиная с записи с номером offset | ||||||
|  |     GET /records?sort_by=date_modified&sort_order=desc&limit=10&offset=100 | ||||||
|  |     ``` | ||||||
|  |     Сортировка по дате модификации обычно означает, что данные могут меняться. Иными словами, между запросом первой порции данных и запросом второй порции данных какая-то запись может измениться; она просто пропадёт из перечисления, т.к. автоматически попадает на первую страницу. Клиент никогда не получит те записи, которые менялись во время перебора, и у него даже нет способа узнать о самом факте такого пропуска. Помимо этого отметим, что такое API нерасширяемо — невозможно добавить сортировку по двум или более полям. | ||||||
|  |  | ||||||
|  |     **Хорошо**: в представленной постановке задача, вообще говоря, не решается. Список записей по дате изменения всегда будет непредсказуемо изменяться, поэтому необходимо изменить сам подход к формированию данных, одним из двух способов. | ||||||
|  |       * Фиксировать порядок в момент обработки запроса; т.е. сервер формирует полный список и сохраняет его в неизменяемом виде: | ||||||
|  |  | ||||||
|  |         ``` | ||||||
|  |         // Создаёт представление по указанным параметрам | ||||||
|  |         POST /v1/record-views | ||||||
|  |         { | ||||||
|  |           sort_by: [ | ||||||
|  |             { "field": "date_modified", "order": "desc" } | ||||||
|  |           ] | ||||||
|  |         } | ||||||
|  |         → | ||||||
|  |         { "id", "cursor" } | ||||||
|  |         ``` | ||||||
|  |         ``` | ||||||
|  |         // Позволяет получить часть представления | ||||||
|  |         GET /v1/record-views/{id}?cursor={cursor} | ||||||
|  |         ``` | ||||||
|  |  | ||||||
|  |         Т.к. созданное представление уже неизменяемо, доступ к нему можно организовать как угодно: через курсор, limit/offest, заголовок Range и т.д. Однако надо иметь в виду, что при переборе таких списков может получиться так, что порядок будет нарушен: записи, изменённые уже после генерации представления, будут находиться не на своих местах (либо быть неактуальны, если запись копируется целиком). | ||||||
|  |  | ||||||
|  |       * Гарантировать строгий неизменяемый порядок записей, например, путём введения понятия события изменения записи: | ||||||
|  |  | ||||||
|  |         ``` | ||||||
|  |         POST /v1/records/modified/list | ||||||
|  |         { | ||||||
|  |           // Опционально | ||||||
|  |           "cursor" | ||||||
|  |         } | ||||||
|  |         → | ||||||
|  |         { | ||||||
|  |           "modified": [ | ||||||
|  |             { "date", "record_id" } | ||||||
|  |           ], | ||||||
|  |           "cursor" | ||||||
|  |         } | ||||||
|  |         ``` | ||||||
|  |  | ||||||
|  |         Недостатком этой схемы является необходимость заводить отдельные списки под каждый вид сортировки. | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user