From af53dab172acd45d716d4be2b1f884e3be229b7a Mon Sep 17 00:00:00 2001
From: Sergey Konstantinov Выражения «мажорная версия API» и «версия API, содержащая обратно несовместимые изменения функциональности» тем самым следует считать эквивалентными. Разработка программного обеспечения характеризуется, помимо прочего, существованием множества различных парадигм разработки, адепты которых зачастую настроены весьма воинственно по отношению к адептам других парадигм. Поэтому при написании этой книги мы намеренно избегаем слов «метод», «объект», «функция» и так далее, используя нейтральный термин «сущность». Под «сущностью» понимается некоторая атомарная единица функциональности — класс, метод, объект, монада, прототип (нужное подчеркнуть). Для составных частей сущности, к сожалению, достаточно нейтрального термина нам придумать не удалось, поэтому мы используем слова «поля» и «методы». Большинство примеров API в общих разделах будут даны в виде JSON-over-HTTP-эндпойтов. Это некоторая условность, которая помогает описать концепции, как нам кажется, максимально понятно. Вместо Большинство примеров API в общих разделах будут даны в виде JSON-over-HTTP-эндпойтов. Это некоторая условность, которая помогает описать концепции, как нам кажется, максимально понятно. Вместо Также в примерах часто применяется следующая конвенция. Запись Подход, который мы используем для проектирования, состоит из четырёх шагов: // TODO: простые вещи делаются просто без бойлерплейта Определив все сущности, их ответственность и отношения друг с другом, мы переходим непосредственно к разработке API: нам осталось прописать номенклатуру всех объектов, полей, методов и функций в деталях. В этой главе мы дадим сугубо практические советы, как сделать API удобным и понятным. Важное уточнение под номером ноль: Правила не действуют безусловно и не означают, что можно не думать головой. У каждого правила есть какая-то рациональная причина его существования. Если в вашей ситуации нет причин следовать правилу — значит, следовать ему не надо. Например, требование консистентности номенклатуры существует затем, чтобы разработчик тратил меньше времени на чтение документации; если вам необходимо, чтобы разработчик обязательно прочитал документацию по какому-то методу, вполне разумно сделать его сигнатуру нарочито неконсистентно. Это соображение применимо ко всем принципам ниже. Если из-за следования правилам у вас получается неудобное, громоздкое, неочевидное API — это повод пересмотреть правила (или API). Явное лучше неявного. Из названия любой сущности должно быть очевидно, что она делает и к каким сайд-эффектам может привести её использование. Важно понимать, что вы вольны вводить свои собственные конвенции. Например, в некоторых фреймворках сознательно отказываются от парных методов Из названия любой сущности должно быть очевидно, что она делает и к каким сайд-эффектам может привести её использование. Избегайте слов-«амёб» без определённой семантики, таких как get, apply, make. Сущности должны именоваться конкретно: плохо: Неочевидно, что достаточно просто обращения к сущности Хорошо: плохо: Даже если операция немодифицирующая, но вычислительно дорогая — следует об этом явно индицировать, особенно если вычислительные ресурсы тарифицируются для пользователя; тем более не стоит подбирать значения по умолчанию так, чтобы вызов операции без параметров максимально расходовал ресурсы. Хорошо: Стремитесь к тому, чтобы из сигнатуры функции было абсолютно ясно, что она делает, что принимает на вход и что возвращает. Вообще, при прочтении кода, работающего с вашим API, должно быть сразу понятно, что, собственно, он делает — без подглядывания в документацию. Избегайте слов-«амёб» без определённой семантики, таких как get, apply, make. Сущности должны именоваться конкретно: Не экономьте буквы. В XXI веке давно уже нет нужды называть переменные покороче: В XXI веке давно уже нет нужды называть переменные покороче. Сущности-массивы должны именоваться во множественном числе или собирательными выражениями — Если поле называется Сущности-массивы должны именоваться во множественном числе или собирательными выражениями — Аналогично, если ожидается булево значение, то из названия это должно быть очевидно, т.е. именование должно описывать некоторое качественное состояние, например, Аналогично, если ожидается булево значение, то из названия это должно быть очевидно, т.е. именование должно описывать некоторое качественное состояние, например, Сущности, выполняющие подобные функции, должны называться подобно и вести себя подобным образом. Отдельно следует оговорить, что на разных платформах эти правила следует дополнить по-своему с учетом специфики first-class citizen-типов. Например, объекты типа плохо: // TODO: единицы измерения, различные стандарты, консистентность
-// TODO: простые вещи делаются просто без бойлерплейтаГлава 6. Условные обозначения и терминология
GET /orders
вполне может быть вызов метода orders.get()
, локальный или удалённый; вместо JSON может быть любой другой формат данных. Смысл утверждений от этого не меняется.I. Проектирование API
Глава 7. Пирамида контекстов API
+GET /orders
вполне может быть вызов метода orders.get()
, локальный или удалённый; вместо JSON может быть любой другой формат данных. Смысл утверждений от этого не меняется.{ "begin_date" }
(т.е. отсутствие значения у поля в JSON-объекте) означает, что в поле находится именно то, что ожидается — т.е. в данном примере какая-то дата начала.I. Проектирование API
Глава 7. Пирамида контекстов API
+Глава 11. Описание конечных интерфейсов
-
+
-
set_entity
/ get_entity
в пользу одного метода entity
с опциональным параметром. Важно только проявить последовательность в её применении — если такая конвенция вводится, то абсолютно все методы API должны иметь подобную полиморфную сигнатуру, или по крайней мере должен существовать принцип именования, отличающий такие комбинированные методы от обычных вызовов.0. Правила — это всего лишь обобщения
+
-
set_entity
/ get_entity
в пользу одного метода entity
с опциональным параметром. Важно только проявить последовательность в её применении — если такая конвенция вводится, то абсолютно все методы API должны иметь подобную полиморфную сигнатуру, или по крайней мере должен существовать принцип именования, отличающий такие комбинированные методы от обычных вызовов.1. Явное лучше неявного
+
-
GET /orders/cancellation
отменяет заказ
-— неочевидно, что достаточно просто обращения к сущности cancellation
(что это?), тем более немодифицирующим методом GET
, чтобы отменить заказ;
-хорошо: POST /orders/cancel
;GET /orders/statistics
агрегирует статистику заказов за всё время
-— даже если операция немодифицирующая, но вычислительно дорогая — следует об этом явно индицировать, особенно если вычислительные ресурсы тарифицируются для пользователя; тем более не стоит подбирать значения по умолчанию так, чтобы операция без параметров максимально расходовала ресурсы;
-хорошо: POST /orders/statistics/aggregate
с обязательным указанием периода агрегации в запросе.
+GET /orders/cancellation
+отменяет заказ
+
cancellation
(что это?), тем более немодифицирующим методом GET
, чтобы отменить заказ; POST /orders/cancel
+ отменяет заказ
+
+GET /orders/statistics
+Возвращает агрегированную статистику заказов за всё время
+
POST /orders/statistics/aggregate
+{ "start_date", "end_date" }
+Возвращает агрегированную статистику заказов за указанный период
+
2. Сущности должны именоваться конкретно
+
-
-user.get()
-— неочевидно, что конкретно будет возвращено;
-хорошо: user.get_id()
;user.get()
— неочевидно, что конкретно будет возвращено;
+хорошо: user.get_id()
;3. Не экономьте буквы
+
-
-order.time()
-— неясно, о каком времени идёт речь: время создания заказа, время готовности заказа, время ожидания заказа?…
+order.time()
— неясно, о каком времени идёт речь: время создания заказа, время готовности заказа, время ожидания заказа?…
хорошо: order.get_estimated_delivery_time()
strpbrk
ищет вхождение любого из списка символов в строке
-— возможно, автору этого API казалось, что аббревиатура pbrk
что-то значит для читателя, но он явно ошибся;
-хорошо: string_search_for_characters
; однако необходимость существования такого метода вообще вызывает сомнения, достаточно было бы иметь удобную функцию поиска подстроки с нужными параметрами.objects
, children
; если это невозможно (термин неисчисляемый), следует добавить префикс или постфикс, не оставляющий сомнений:
+strpbrk (str1, str2)
+возвращает положение первого вхождения в строку str2 любого символа из строки str2
+
+Возможно, автору этого API казалось, что аббревиатура pbrk
что-то значит для читателя, но он явно ошибся. К тому же, невозможно сходу понять, какая из строк str1
, str2
является набором символов для поиска.
+Хорошо:
+
+str_search_for_characters(lookup_character_set, str)
+
+Однако необходимость существования такого метода вообще вызывает сомнения, достаточно было бы иметь удобную функцию поиска подстроки с нужными параметрами. Аналогично сокращение string
до str
выглядит совершенно бессмысленным, но, увы, является устоявшимся для большого количества предметных областей.4. Тип поля должен быть ясен из его названия
+recipe
— мы ожидаем, что его значением является сущность типа Recipe
. Если поле называется recipe_id
— мы ожидаем, что его значением является идентификатор, который я могу найти в составе сущности Recipe
.objects
, children
; если это невозможно (термин неисчисляемый), следует добавить префикс или постфикс, не оставляющий сомнений:
-
-GET /news
-— неясно, будет ли получена какая-то конкретная новость или массив новостей;
-хорошо: GET /news-list
.is_ready
, open_now
:GET /news
— неясно, будет ли получена какая-то конкретная новость или массив новостей;
+хорошо: GET /news-list
.is_ready
, open_now
:
-
-"task.status": true
-— неочевидно, что статус бинарен, плюс такое API будет нерасширяемым
-хорошо: "task.is_finished": true
"task.status": true
— неочевидно, что статус бинарен, плюс такое API будет нерасширяемым;
+хорошо: "task.is_finished": true
Date
, если таковые имеются, разумно индицировать с помощью, например, постфикса _at
(created_at
, occurred_at
, etc).5. Подобные сущности должны называться подобно и вести себя подобным образом
-
-
-begin_transition
/ stop_transition
+begin_transition
/ stop_transition
— begin
и stop
— непарные термины; разработчик будет вынужден рыться в документации;
-хорошо: begin_transition
/ end_transition
либо start_transition
/ stop_transition
;
-strpos(haystack, needle)
— ищет позицию строки needle
внутри строки haystack
;
-str_replace(needle, replace, haystack)
— заменяет вхождения строки needle
внутри строки haystack
на строку replace
;
-— здесь нарушены сразу несколько правил: написание неконсистентно в части знака подчеркивания; близкие по смыслу методы имеют разный порядок аргументов needle
/haystack
; наконец, один из методов находит первое вхождение, а другой — все вхождения, и это никак не отражено в именовании.begin_transition
/ end_transition
либо start_transition
/ stop_transition
;
плохо:
+strpos(haystack, needle)
+Находит первую позицию позицию строки `needle` внутри строки `haystack`
+
+str_replace(needle, replace, haystack)
+Находит и заменяет все вхождения строки `needle` внутри строки `haystack` на строку `replace`
+
+Здесь нарушены сразу несколько правил:
+needle
/haystack
; needle
, а другой — все вхождения, и об этом поведении никак нельзя узнать из сигнатуры функций.Упражнение «как сделать эти интерфейсы хорошо» предоставим читателю.
К сожалению, человечество не в состоянии договориться о таких простейших вещах, как «начинать ли неделю с понедельника или с воскресенья», что уж говорить о каких-то более сложных стандартах.
+Поэтому всегда указывайте, по какому конкретно стандарту вы отдаёте те или иные величины. Исключения возможны только там, где вы на 100% уверены, что в мире существует только один стандарт для этой сущности, и всё население земного шара о нём в курсе.
+"date":"11/12/2020"
— стандартов записи дат существует огромное количество, плюс из этой записи невозможно даже понять, что здесь число, а что месяц;"iso_date":"2020-11-12"
."duration":5000
— пять тысяч чего?"duration_ms":5000
"duration":"5000ms"
"duration":{"unit":"ms","value":5000}
Отдельное следствие из этого правила — денежные величины всегда должны сопровождаться указанием кода валюты.
+Также следует отметить, что в некоторых областях ситуация со стандартами настолько плоха, что как ни сделай — кто-то останется недовольным. Классический пример такого рода — порядок геокоординат ("широта-долгота" против "долгота-широта"). Здесь, увы, вам остаётся только смириться и проявлять выдержку при нападках на ваше API.
+Там, где это позволено протоколом, дробные числа с фиксированной запятой — такие, как денежные суммы, например — должны передаваться в виде специально предназначенных для этого объектов, например, Decimal или аналогичных.
+Если в протоколе нет Decimal-типов (в частности, в JSON нет чисел с фиксированной запятой), следует либо привести к целому, либо использовать строковый тип.