From b543b9c0d213bc41d5112f13e65eb339b70e73be Mon Sep 17 00:00:00 2001 From: Cliffart Date: Thu, 2 Jun 2022 13:48:50 +0300 Subject: [PATCH] Various visual improvements --- .../02-Раздел I. Проектирование API/01.md | 2 +- .../02-Раздел I. Проектирование API/02.md | 4 ++-- .../02-Раздел I. Проектирование API/03.md | 10 +++++----- .../02-Раздел I. Проектирование API/04.md | 4 ++-- .../02-Раздел I. Проектирование API/05.md | 16 ++++++++-------- .../03-Раздел II. Обратная совместимость/01.md | 4 ++-- .../03-Раздел II. Обратная совместимость/02.md | 8 ++++---- .../03-Раздел II. Обратная совместимость/03.md | 8 ++++---- .../03-Раздел II. Обратная совместимость/04.md | 8 ++++---- .../03-Раздел II. Обратная совместимость/05.md | 16 ++++++++-------- 10 files changed, 40 insertions(+), 40 deletions(-) diff --git a/src/ru/clean-copy/02-Раздел I. Проектирование API/01.md b/src/ru/clean-copy/02-Раздел I. Проектирование API/01.md index 2f6c626..267f29e 100644 --- a/src/ru/clean-copy/02-Раздел I. Проектирование API/01.md +++ b/src/ru/clean-copy/02-Раздел I. Проектирование API/01.md @@ -6,7 +6,7 @@ * разграничение областей ответственности; * описание конечных интерфейсов. -Этот алгоритм строит API сверху вниз, от общих требований и сценариев использования до конкретной номенклатуры сущностей; фактически, двигаясь этим путем, вы получите на выходе готовый API — чем этот подход и ценен. +Этот алгоритм строит API сверху вниз, от общих требований и сценариев использования до конкретной номенклатуры сущностей; фактически, двигаясь этим путём, вы получите на выходе готовый API — чем этот подход и ценен. Может показаться, что наиболее полезные советы приведены в последнем разделе, однако это не так; цена ошибки, допущенной на разных уровнях весьма различна. Если исправить плохое именование довольно просто, то исправить неверное понимание того, зачем вообще нужен API, практически невозможно. diff --git a/src/ru/clean-copy/02-Раздел I. Проектирование API/02.md b/src/ru/clean-copy/02-Раздел I. Проектирование API/02.md index 0bc51cf..2980248 100644 --- a/src/ru/clean-copy/02-Раздел I. Проектирование API/02.md +++ b/src/ru/clean-copy/02-Раздел I. Проектирование API/02.md @@ -12,7 +12,7 @@ Итак, предположим, что мы хотим предоставить API автоматического заказа кофе в городских кофейнях. Попробуем применить к нему этот принцип. - 1. Зачем кому-то может потребоваться API для приготовления кофе? В чем неудобство заказа кофе через интерфейс, человек-человек или человек-машина? Зачем нужна возможность заказа машина-машина? + 1. Зачем кому-то может потребоваться API для приготовления кофе? В чём неудобство заказа кофе через интерфейс, человек-человек или человек-машина? Зачем нужна возможность заказа машина-машина? * Возможно, мы хотим решить проблему выбора и знания? Чтобы человек наиболее полно знал о доступных ему здесь и сейчас опциях. * Возможно, мы оптимизируем время ожидания? Чтобы человеку не пришлось ждать, пока его заказ готовится. @@ -34,7 +34,7 @@ Поскольку наша книга посвящена не просто разработке программного обеспечения, а разработке API, то на все эти вопросы мы должны взглянуть под другим ракурсом: а почему для решения этих задач требуется именно API, а не просто программное обеспечение? В нашем вымышленном примере мы должны спросить себя: зачем нам нужно предоставлять сервис для других разработчиков, чтобы они могли готовить кофе своим клиентам, а не сделать своё приложение для конечного потребителя? -Иными словами, должна иметься веская причина, по которой два домена разработки ПО должны быть разделены: есть оператор(ы), предоставляющий API; есть оператор(ы), предоставляющий сервисы пользователям. Их интересы в чем-то различны настолько, что объединение этих двух ролей в одном лице нежелательно. Более подробно мы изложим причины и мотивации делать именно API в разделе III. +Иными словами, должна иметься веская причина, по которой два домена разработки ПО должны быть разделены: есть оператор(ы), предоставляющий API; есть оператор(ы), предоставляющий сервисы пользователям. Их интересы в чём-то различны настолько, что объединение этих двух ролей в одном лице нежелательно. Более подробно мы изложим причины и мотивации делать именно API в разделе III. Заметим также следующее: вы должны браться делать API тогда и только тогда, когда в ответе на второй вопрос написали «потому что в этом состоит наша экспертиза». Разрабатывая API, вы занимаетесь некоторой мета-разработкой: вы пишете ПО для того, чтобы другие могли разрабатывать ПО для решения задачи пользователя. Не обладая экспертизой в обоих этих доменах (API и конечные продукты) написать хороший API сложно. diff --git a/src/ru/clean-copy/02-Раздел I. Проектирование API/03.md b/src/ru/clean-copy/02-Раздел I. Проектирование API/03.md index 1fffbc6..12198f3 100644 --- a/src/ru/clean-copy/02-Раздел I. Проектирование API/03.md +++ b/src/ru/clean-copy/02-Раздел I. Проектирование API/03.md @@ -1,10 +1,10 @@ ### Разделение уровней абстракции -«Разделите свой код на уровни абстракции» - пожалуй, самый общий совет для разработчиков программного обеспечения. Однако будет вовсе не преувеличением сказать, что изоляция уровней абстракции — самая сложная задача, стоящая перед разработчиком API. +«Разделите свой код на уровни абстракции» — пожалуй, самый общий совет для разработчиков программного обеспечения. Однако будет вовсе не преувеличением сказать, что изоляция уровней абстракции — самая сложная задача, стоящая перед разработчиком API. Прежде чем переходить к теории, следует чётко сформулировать, _зачем_ нужны уровни абстракции и каких целей мы хотим достичь их выделением. -Вспомним, что программный продукт - это средство связи контекстов, средство преобразования терминов и операций одной предметной области в другую. Чем дальше друг от друга эти области отстоят - тем большее число промежуточных передаточных звеньев нам придётся ввести. Вернёмся к нашему примеру с кофейнями. Какие уровни сущностей мы видим? +Вспомним, что программный продукт — это средство связи контекстов, средство преобразования терминов и операций одной предметной области в другую. Чем дальше друг от друга эти области отстоят — тем большее число промежуточных передаточных звеньев нам придётся ввести. Вернёмся к нашему примеру с кофейнями. Какие уровни сущностей мы видим? 1. Мы готовим с помощью нашего API *заказ* — один или несколько стаканов кофе — и взымаем за это плату. 2. Каждый стакан кофе приготовлен по определённому *рецепту*, что подразумевает наличие разных ингредиентов и последовательности выполнения шагов приготовления. @@ -66,13 +66,13 @@ POST /v1/orders Хорошо, допустим, мы поняли, как сделать плохо. Но как же тогда сделать *хорошо*? Разделение уровней абстракции должно происходить вдоль трёх направлений: - 1. От сценариев использования к их внутренней реализации: высокоуровневые сущности и номенклатура их методов должны напрямую отражать сценарии использования API; низкоуровневый - отражать декомпозицию сценариев на составные части. + 1. От сценариев использования к их внутренней реализации: высокоуровневые сущности и номенклатура их методов должны напрямую отражать сценарии использования API; низкоуровневый — отражать декомпозицию сценариев на составные части. 2. От терминов предметной области пользователя к терминам предметной области исходных данных — в нашем случае от высокоуровневых понятий «рецепт», «заказ», «кофейня» к низкоуровневым «температура напитка» и «координаты кофе-машины» - 3. Наконец, от структур данных, в которых удобно оперировать пользователю к структурам данных, максимально приближенных к «сырым» - в нашем случае от «лунго» и «сети кофеен "Ромашка"» - к сырым байтовый данным, описывающим состояние кофе-машины марки «Доброе утро» в процессе приготовления напитка. + 3. Наконец, от структур данных, в которых удобно оперировать пользователю к структурам данных, максимально приближенных к «сырым» — в нашем случае от «лунго» и «сети кофеен "Ромашка"» — к сырым байтовый данным, описывающим состояние кофе-машины марки «Доброе утро» в процессе приготовления напитка. -Чем дальше находятся друг от друга программные контексты, которые соединяет наш API - тем более глубокая иерархия сущностей должна получиться у нас в итоге. +Чем дальше находятся друг от друга программные контексты, которые соединяет наш API — тем более глубокая иерархия сущностей должна получиться у нас в итоге. В нашем примере с определением готовности кофе мы явно пришли к тому, что нам требуется промежуточный уровень абстракции: diff --git a/src/ru/clean-copy/02-Раздел I. Проектирование API/04.md b/src/ru/clean-copy/02-Раздел I. Проектирование API/04.md index a8aaa9f..7867e38 100644 --- a/src/ru/clean-copy/02-Раздел I. Проектирование API/04.md +++ b/src/ru/clean-copy/02-Раздел I. Проектирование API/04.md @@ -189,7 +189,7 @@ POST /v1/orders Получив такую ошибку, клиент должен проверить её род (что-то с предложением), проверить конкретную причину ошибки (срок жизни оффера истёк) и отправить повторный запрос цены. При этом если бы `checks_failed` показал другую причину ошибки — например, указанный `offer_id` не принадлежит данному пользователю — действия клиента были бы иными (отправить пользователя повторно авторизоваться, а затем перезапросить цену). Если же обработка такого рода ошибок в коде не предусмотрена — следует показать пользователю сообщение `localized_message` и вернуться к обработке ошибок по умолчанию. -Важно также отметить, что неустранимые ошибки в моменте для клиента бесполезны (не зная причины ошибки клиент не может ничего разумного предложить пользователю), но это не значит, что у них не должно быть расширенной информации: их все равно будет просматривать разработчик, когда будет исправлять эту проблему в коде. Подробнее об этом в пп. 12-13 следующей главы. +Важно также отметить, что неустранимые ошибки в моменте для клиента бесполезны (не зная причины ошибки клиент не может ничего разумного предложить пользователю), но это не значит, что у них не должно быть расширенной информации: их всё равно будет просматривать разработчик, когда будет исправлять эту проблему в коде. Подробнее об этом в пп. 12-13 следующей главы. #### Декомпозиция интерфейсов. Правило «7±2» @@ -285,4 +285,4 @@ POST /v1/orders Важно, что читабельность достигается не просто снижением количества сущностей на одном уровне. Декомпозиция должна производиться таким образом, чтобы разработчик при чтении интерфейса сразу понимал: так, вот здесь находится описание заведения, оно мне пока неинтересно и углубляться в эту ветку я пока не буду. Если перемешать данные, которые нужны в моменте одновременно для выполнения действия по разным композитам — это только ухудшит читабельность, а не улучшит. -Дополнительно правильная декомпозиция поможет нам в решении задачи расширения и развития API, о чем мы поговорим в разделе II. +Дополнительно правильная декомпозиция поможет нам в решении задачи расширения и развития API, о чём мы поговорим в разделе II. diff --git a/src/ru/clean-copy/02-Раздел I. Проектирование API/05.md b/src/ru/clean-copy/02-Раздел I. Проектирование API/05.md index 6905445..63a56ca 100644 --- a/src/ru/clean-copy/02-Раздел I. Проектирование API/05.md +++ b/src/ru/clean-copy/02-Раздел I. Проектирование API/05.md @@ -125,7 +125,7 @@ strpbrk (str1, str2) **Хорошо**: `"task.is_finished": true`. -Отдельно следует оговорить, что на разных платформах эти правила следует дополнить по-своему с учетом специфики first-class citizen-типов. Например, объекты типа `Date`, если таковые имеются, разумно индицировать с помощью, например, постфикса `_at` (`created_at`, `occurred_at` и т.д.) или `_date`. +Отдельно следует оговорить, что на разных платформах эти правила следует дополнить по-своему с учётом специфики first-class citizen-типов. Например, объекты типа `Date`, если таковые имеются, разумно индицировать с помощью, например, постфикса `_at` (`created_at`, `occurred_at` и т.д.) или `_date`. Если наименование сущности само по себе является каким-либо термином, способным смутить разработчика, лучше добавить лишний префикс или постфикс во избежание непонимания. @@ -157,7 +157,7 @@ strpos(haystack, needle) str_replace(needle, replace, haystack) ``` Здесь нарушены сразу несколько правил: - * написание неконсистентно в части знака подчеркивания; + * написание неконсистентно в части знака подчёркивания; * близкие по смыслу методы имеют разный порядок аргументов `needle`/`haystack`; * первый из методов находит только первое вхождение строки `needle`, а другой — все вхождения, и об этом поведении никак нельзя узнать из сигнатуры функций. @@ -232,7 +232,7 @@ GET /v1/users/{id}/orders — люди в целом плохо считывают двойные отрицания. Это провоцирует ошибки. **Лучше**: `"prohibit_calling": true` или `"avoid_calling": true` -— читается лучше, хотя обольщаться все равно не следует. Насколько это возможно откажитесь от семантически двойных отрицаний, даже если вы придумали «негативное» слово без явной приставки «не». +— читается лучше, хотя обольщаться всё равно не следует. Насколько это возможно откажитесь от семантически двойных отрицаний, даже если вы придумали «негативное» слово без явной приставки «не». Стоит также отметить, что в использовании [законов де Моргана](https://ru.wikipedia.org/wiki/%D0%97%D0%B0%D0%BA%D0%BE%D0%BD%D1%8B_%D0%B4%D0%B5_%D0%9C%D0%BE%D1%80%D0%B3%D0%B0%D0%BD%D0%B0) ошибиться ещё проще, чем в двойных отрицаниях. Предположим, что у вас есть два флага: @@ -720,7 +720,7 @@ GET /v1/price?recipe=lungo GET /v1/records?limit=10&offset=100 ``` На первый взгляд это самый что ни на есть стандартный способ организации пагинации в API. Однако зададим себе три вопроса. - 1. Каким образом клиент узнает о появлении новых записей в начале списка? + 1. Каким образом клиент узнает о появлении новых записей в начале списка? Легко заметить, что клиент может только попытаться повторить первый запрос и сверить идентификаторы с запомненным началом списка. Но что делать, если добавленное количество записей превышает `limit`? Представим себе ситуацию: * клиент обрабатывает записи в порядке поступления; * произошла какая-то проблема, и накопилось большое количество необработанных записей; @@ -848,7 +848,7 @@ POST /v1/coffee-machines/search → 400 Bad Request {} ``` -— да, конечно, допущенные ошибки (опечатка в `"lngo"` и неправильные координаты) очевидны. Но раз наш сервер все равно их проверяет, почему не вернуть описание ошибок в читаемом виде? +— да, конечно, допущенные ошибки (опечатка в `"lngo"` и неправильные координаты) очевидны. Но раз наш сервер всё равно их проверяет, почему не вернуть описание ошибок в читаемом виде? **Хорошо**: ``` @@ -905,7 +905,7 @@ POST /v1/orders "reason": "recipe_unknown" } ``` -— какой был смысл получать новый `offer`, если заказ все равно не может быть создан? +— какой был смысл получать новый `offer`, если заказ всё равно не может быть создан? **Во-вторых**, соблюдайте такой порядок разрешимых ошибок, который приводит к наименьшему раздражению пользователя и разработчика. В частности, следует начинать с более значимых ошибок, решение которых требует более глобальных изменений. @@ -934,7 +934,7 @@ POST /v1/orders "localized_message": "Лимит заказов превышен" } ``` -— какой был смысл показывать пользователю диалог об изменившейся цене, если и с правильной ценой заказ он сделать все равно не сможет? Пока один из его предыдущих заказов завершится и можно будет сделать следующий заказ, цену, наличие и другие параметры заказа всё равно придётся корректировать ещё раз. +— какой был смысл показывать пользователю диалог об изменившейся цене, если и с правильной ценой заказ он сделать всё равно не сможет? Пока один из его предыдущих заказов завершится и можно будет сделать следующий заказ, цену, наличие и другие параметры заказа всё равно придётся корректировать ещё раз. **В-третьих**, постройте таблицу: разрешение какой ошибки может привести к появлению другой, иначе вы можете показать одну и ту же ошибку несколько раз, а то и вовсе зациклить разрешение ошибок. @@ -974,7 +974,7 @@ POST /v1/orders } ``` -Легко заметить, что в этом примере нет способа разрешить ошибку в один шаг — эту ситуацию требуется предусмотреть отдельно, и либо изменить параметры расчета (минимальная сумма заказа не учитывает скидки), либо ввести специальную ошибку для такого кейса. +Легко заметить, что в этом примере нет способа разрешить ошибку в один шаг — эту ситуацию требуется предусмотреть отдельно, и либо изменить параметры расчёта (минимальная сумма заказа не учитывает скидки), либо ввести специальную ошибку для такого кейса. ##### Отсутствие результата — тоже результат diff --git a/src/ru/clean-copy/03-Раздел II. Обратная совместимость/01.md b/src/ru/clean-copy/03-Раздел II. Обратная совместимость/01.md index e9fbedb..31759b9 100644 --- a/src/ru/clean-copy/03-Раздел II. Обратная совместимость/01.md +++ b/src/ru/clean-copy/03-Раздел II. Обратная совместимость/01.md @@ -10,7 +10,7 @@ 2. Что значит «длительное время»? - С нашей точки зрения длительность поддержания обратной совместимости следует увязывать с длительностью жизненных циклов приложений в соответствующей предметной области. Хороший ориентир в большинстве случаев — это LTS-периоды платформ. Так как приложение все равно будет переписано в связи с окончанием поддержки платформы, нормально предложить также и переход на новую версию API. В основных предметных областях (десктопные и мобильные операционные системы) этот срок исчисляется несколькими годами. + С нашей точки зрения длительность поддержания обратной совместимости следует увязывать с длительностью жизненных циклов приложений в соответствующей предметной области. Хороший ориентир в большинстве случаев — это LTS-периоды платформ. Так как приложение всё равно будет переписано в связи с окончанием поддержки платформы, нормально предложить также и переход на новую версию API. В основных предметных областях (десктопные и мобильные операционные системы) этот срок исчисляется несколькими годами. Почему обратную совместимость необходимо поддерживать (в том числе предпринимать необходимые меры ещё на этапе проектирования API) — понятно из определения. Прекращение работы приложения (полное или частичное) по вине поставщика API — крайне неприятное событие, а то и катастрофа, для любого разработчика, особенно если он платит за этот API деньги. @@ -38,7 +38,7 @@ 1. Если платформа поддерживает on-demand получение кода, как старый-добрый Веб, и вы не поленились это получение кода реализовать (в виде платформенного SDK, например, JS API), то развитие API более или менее находится под вашим контролем. Поддержание обратной совместимости сводится к поддержанию обратной совместимости *клиентской библиотеки*, а вот в части сервера и клиент-серверного взаимодействия вы свободны. - Это не означает, что вы не можете нарушить обратную совместимость — всё ещё можно напортачить с заголовками кэширования SDK или банально допустить баг в коде. Кроме того, даже on-demand системы все равно не обновляются мгновенно — автор сталкивался с ситуацией, когда пользователи намеренно держали вкладку браузера открытой *неделями*, чтобы не обновляться на новые версии. Тем не менее, вам почти не придётся поддерживать более двух (последней и предпоследней) минорных версий клиентского SDK. Более того, вы можете попытаться в какой-то момент переписать предыдущую мажорную версию библиотеки, имплементировав её на основе API новой версии. + Это не означает, что вы не можете нарушить обратную совместимость — всё ещё можно напортачить с заголовками кэширования SDK или банально допустить баг в коде. Кроме того, даже on-demand системы всё равно не обновляются мгновенно — автор сталкивался с ситуацией, когда пользователи намеренно держали вкладку браузера открытой *неделями*, чтобы не обновляться на новые версии. Тем не менее, вам почти не придётся поддерживать более двух (последней и предпоследней) минорных версий клиентского SDK. Более того, вы можете попытаться в какой-то момент переписать предыдущую мажорную версию библиотеки, имплементировав её на основе API новой версии. 2. Если поддержка on-demand кода платформой не поддерживается или запрещена условиями, как это произошло с современными мобильными платформами, то ситуация становится гораздо сложнее. По сути, каждый клиент — это «слепок» кода, который работает с вашим API, зафиксированный в том состоянии, в котором он был на момент компиляции. Обновление клиентских приложений по времени растянуто гораздо дольше, нежели Web-приложений; самое неприятное здесь состоит в том, что некоторые клиенты *не обновятся вообще никогда* — по одной из трёх причин: diff --git a/src/ru/clean-copy/03-Раздел II. Обратная совместимость/02.md b/src/ru/clean-copy/03-Раздел II. Обратная совместимость/02.md index 8846a22..9f5ab1d 100644 --- a/src/ru/clean-copy/03-Раздел II. Обратная совместимость/02.md +++ b/src/ru/clean-copy/03-Раздел II. Обратная совместимость/02.md @@ -14,14 +14,14 @@ ##### Избегайте серых зон и недосказанности -Ваши обязательства по поддержанию функциональности должны быть оговорены настолько четко, насколько это возможно. Особенно это касается тех сред и платформ, где нет способа нативно ограничить доступ к недокументированной функциональности. К сожалению, разработчики часто считают, что, если они «нашли» какую-то непубличную особенность, то они могут ей пользоваться — а производитель API, соответственно, обязан её поддерживать. Поэтому политика компании относительно таких «находок» должна быть явно сформулирована. Тогда в случае несанкционированного использования скрытой функциональности вы по крайней мере сможете сослаться на документацию и быть формально правы в глазах комьюнити. +Ваши обязательства по поддержанию функциональности должны быть оговорены настолько чётко, насколько это возможно. Особенно это касается тех сред и платформ, где нет способа нативно ограничить доступ к недокументированной функциональности. К сожалению, разработчики часто считают, что, если они «нашли» какую-то непубличную особенность, то они могут ей пользоваться — а производитель API, соответственно, обязан её поддерживать. Поэтому политика компании относительно таких «находок» должна быть явно сформулирована. Тогда в случае несанкционированного использования скрытой функциональности вы по крайней мере сможете сослаться на документацию и быть формально правы в глазах комьюнити. Однако достаточно часто разработчики API сами легитимизируют такие серые зоны, например: * отдают недокументированные поля в ответах эндпойнтов; * используют непубличную функциональность в примерах кода — в документации, в ответ на обращения пользователей, в выступлениях на конференциях и т.д. -Нельзя принять обязательства наполовину. Или вы гарантируете работу этого кода всегда, или не подавайте никаких намеков на то, что такая функциональность существует. +Нельзя принять обязательства наполовину. Или вы гарантируете работу этого кода всегда, или не подавайте никаких намёков на то, что такая функциональность существует. ##### Фиксируйте неявные договорённости @@ -123,7 +123,7 @@ GET /v1/orders/{id}/events/history Допустим, в какой-то момент вы решили надёжным клиентам с хорошей историей заказов предоставлять кофе «в кредит», не дожидаясь подтверждения платежа. Т.е. заказ перейдёт в статус `"preparing_started"`, а может и `"ready"`, вообще без события `"payment_approved"`. Вам может показаться, что это изменение является обратно-совместимым — в самом деле, вы же и не обещали никакого конкретного порядка событий. Но это, конечно, не так. -Предположим, что у разработчика (вероятно, бизнес-партнера вашей компании) написан какой-то код, выполняющий какую-то полезную бизнес функцию поверх этих событий — например, строит аналитику по затратам и доходам. Вполне логично ожидать, что этот код будет оперировать какой-то машиной состояний, которая будет переходить в то или иное состояние в зависимости от получения или неполучения события. Аналитический код наверняка сломается вследствие изменения порядка событий. В лучшем случае разработчик увидит какие-то исключения и будет вынужден разбираться с причиной; в худшем случае партнер будет оперировать неправильной статистикой неопределённое время, пока не найдёт в ней ошибку. +Предположим, что у разработчика (вероятно, бизнес-партнёра вашей компании) написан какой-то код, выполняющий какую-то полезную бизнес функцию поверх этих событий — например, строит аналитику по затратам и доходам. Вполне логично ожидать, что этот код будет оперировать какой-то машиной состояний, которая будет переходить в то или иное состояние в зависимости от получения или неполучения события. Аналитический код наверняка сломается вследствие изменения порядка событий. В лучшем случае разработчик увидит какие-то исключения и будет вынужден разбираться с причиной; в худшем случае партнёр будет оперировать неправильной статистикой неопределённое время, пока не найдёт в ней ошибку. Правильным решением было бы во-первых, изначально задокументировать порядок событий и допустимые состояния; во-вторых, продолжать генерировать событие `"payment_approved"` перед `"preparing_started"` (если вы приняли решение исполнять такой заказ — значит, по сути, подтвердили платёж) и добавить расширенную информацию о платеже. @@ -133,6 +133,6 @@ GET /v1/orders/{id}/events/history Такие критичные вещи, как граф переходов между статусами, порядок событий и возможные причины тех или иных изменений — должны быть документированы. Далеко не все детали бизнес-логики можно выразить в форме контрактов на эндпойнты, а некоторые вещи нельзя выразить вовсе. -Представьте, что в один прекрасный день вы заводите специальный номер телефона, по которому клиент может позвонить в колл-центр и отменить заказ. Вы даже можете сделать это *технически* обратно-совместимым образом, добавив новых необязательных полей в сущность «заказ». Но конечный потребитель может просто *знать* нужный номер телефона, и позвонить по нему, даже если приложение его не показало. При этом код бизнес-аналитика партнера всё так же может сломаться или начать показывать погоду на Марсе, т.к. он был написан когда-то, ничего не зная о возможности отменить заказ, сделанный в приложении партнера, каким-то иным образом, не через самого партнёра же. +Представьте, что в один прекрасный день вы заводите специальный номер телефона, по которому клиент может позвонить в колл-центр и отменить заказ. Вы даже можете сделать это *технически* обратно-совместимым образом, добавив новых необязательных полей в сущность «заказ». Но конечный потребитель может просто *знать* нужный номер телефона, и позвонить по нему, даже если приложение его не показало. При этом код бизнес-аналитика партнёра всё так же может сломаться или начать показывать погоду на Марсе, т.к. он был написан когда-то, ничего не зная о возможности отменить заказ, сделанный в приложении партнёра, каким-то иным образом, не через самого партнёра же. *Технически* корректным решением в данной ситуации могло бы быть добавление параметра «разрешено отменять через колл-центр» в функцию создания заказа — и, соответственно, запрет операторам колл-центра отменять заказы, если флаг не был указан при их создании. Но это в свою очередь плохое решение *с точки зрения продукта*. «Хорошее» решение здесь только одно — изначально предусмотреть возможность внешних отмен в API; если же вы её не предвидели — остаётся воспользоваться «блокнотом душевного спокойствия», речь о котором пойдёт в последней главе настоящего раздела. diff --git a/src/ru/clean-copy/03-Раздел II. Обратная совместимость/03.md b/src/ru/clean-copy/03-Раздел II. Обратная совместимость/03.md index 86f2414..70b5782 100644 --- a/src/ru/clean-copy/03-Раздел II. Обратная совместимость/03.md +++ b/src/ru/clean-copy/03-Раздел II. Обратная совместимость/03.md @@ -2,7 +2,7 @@ В предыдущих разделах мы старались приводить теоретические правила и иллюстрировать их на практических примерах. Однако понимание принципов проектирования API, устойчивого к изменениям, как ничто другое требует прежде всего практики. Знание о том, куда стоит «постелить соломку» — оно во многом «сын ошибок трудных». Нельзя предусмотреть всего — но можно выработать необходимый уровень технической интуиции. -Поэтому в этом разделе мы поступим следующим образом: возьмём наш [модельный API](#chapter-12) из предыдущего раздела, и проверим его на устойчивость в каждой возможной точке — проведём некоторый «вариационный анализ» наших интерфейсов. Ещё более конкретно — к каждой сущности мы подойдём с вопросом «что, если?» — что, если нам потребуется предоставить партнерам возможность написать свою независимую реализацию этого фрагмента логики. +Поэтому в этом разделе мы поступим следующим образом: возьмём наш [модельный API](#chapter-12) из предыдущего раздела, и проверим его на устойчивость в каждой возможной точке — проведём некоторый «вариационный анализ» наших интерфейсов. Ещё более конкретно — к каждой сущности мы подойдём с вопросом «что, если?» — что, если нам потребуется предоставить партнёрам возможность написать свою независимую реализацию этого фрагмента логики. **NB**. В рассматриваемых нами примерах мы будем выстраивать интерфейсы так, чтобы связывание разных сущностей происходило динамически в реальном времени; на практике такие интеграции будут делаться на стороне сервера путём написания ad hoc кода и формирования конкретных договорённостей с конкретным клиентом, однако мы для целей обучения специально будем идти более сложным и абстрактным путём. Динамическое связывание в реальном времени применимо скорее к сложным программным конструктам типа API операционных систем или встраиваемых библиотек; приводить обучающие примеры на основе систем подобной сложности было бы, однако, чересчур затруднительно. @@ -39,8 +39,8 @@ PUT /v1/partners/{partnerId}/coffee-machines ``` Таким образом механика следующая: - * партнер описывает свои виды API, кофе-машины и поддерживаемые рецепты; - * при получении заказа, который необходимо выполнить на конкретной кофе машине, наш сервер обратится к функции обратного вызова, передав ей данные о заказе в оговоренном формате. + * партнёр описывает свои виды API, кофе-машины и поддерживаемые рецепты; + * при получении заказа, который необходимо выполнить на конкретной кофе машине, наш сервер обратится к функции обратного вызова, передав ей данные о заказе в оговорённом формате. Теперь партнёры могут динамически подключать свои кофе-машины и обрабатывать заказы. Займёмся теперь, однако, вот каким упражнением: * перечислим все неявные предположения, которые мы допустили; @@ -82,7 +82,7 @@ PUT /v1/partners/{partnerId}/coffee-machines Часто вместо добавления нового метода можно добавить просто необязательный параметр к существующему интерфейсу — в нашем случае, можно добавить необязательный параметр `options` к вызову `PUT /cofee-machines`. -**NB**. Когда мы говорим о фиксации договоренностей, действующих в настоящий момент — речь идёт о *внутренних* договорённостях. Мы должны были потребовать от партнеров поддерживать указанный список опций, когда обговаривали формат взаимодействия. Если же мы этого не сделали изначально, а потом решили зафиксировать договорённости в ходе расширения функциональности внешнего API — это очень серьёзная заявка на нарушение обратной совместимости, и так делать ни в коем случае не надо, см. [главу 14](#chapter-14). +**NB**. Когда мы говорим о фиксации договоренностей, действующих в настоящий момент — речь идёт о *внутренних* договорённостях. Мы должны были потребовать от партнёров поддерживать указанный список опций, когда обговаривали формат взаимодействия. Если же мы этого не сделали изначально, а потом решили зафиксировать договорённости в ходе расширения функциональности внешнего API — это очень серьёзная заявка на нарушение обратной совместимости, и так делать ни в коем случае не надо, см. [главу 14](#chapter-14). #### Границы применимости diff --git a/src/ru/clean-copy/03-Раздел II. Обратная совместимость/04.md b/src/ru/clean-copy/03-Раздел II. Обратная совместимость/04.md index 334a168..f69af8a 100644 --- a/src/ru/clean-copy/03-Раздел II. Обратная совместимость/04.md +++ b/src/ru/clean-copy/03-Раздел II. Обратная совместимость/04.md @@ -1,6 +1,6 @@ ### Сильная связность и сопутствующие проблемы -Для демонстрации проблем сильной связности перейдём теперь к *действительно интересным* вещам. Продолжим наш «вариационный анализ»: что, если партнёры хотят не просто готовить кофе по стандартным рецептам, но и предлагать свои авторские напитки? Вопрос этот с подвохом: в том виде, как мы описали партнёрскый API в предыдущей главе, факт существования партнерской сети никак не отражен в нашем API с точки зрения продукта, предлагаемого пользователю, а потому представляет собой довольно простой кейс. Если же мы пытаемся предоставить не какую-то дополнительную возможность, а модифицировать саму базовую функциональность API, то мы быстро столкнёмся с проблемами совсем другого порядка. +Для демонстрации проблем сильной связности перейдём теперь к *действительно интересным* вещам. Продолжим наш «вариационный анализ»: что, если партнёры хотят не просто готовить кофе по стандартным рецептам, но и предлагать свои авторские напитки? Вопрос этот с подвохом: в том виде, как мы описали партнёрскый API в предыдущей главе, факт существования партнёрской сети никак не отражён в нашем API с точки зрения продукта, предлагаемого пользователю, а потому представляет собой довольно простой кейс. Если же мы пытаемся предоставить не какую-то дополнительную возможность, а модифицировать саму базовую функциональность API, то мы быстро столкнёмся с проблемами совсем другого порядка. Итак, добавим ещё один эндпойнт — для регистрации собственного рецепта партнёра. @@ -48,7 +48,7 @@ POST /v1/recipes Корректный ответ — потому что существует некоторое представление, UI для выбора типа напитка. По-видимому, `name` и `description` — это просто два описания напитка, короткое (для показа в общем прейскуранте) и длинное (для показа расширенной информации о продукте). Получается, что мы устанавливаем требования на API исходя из вполне конкретного дизайна. Но что, если партнёр сам делает UI для своего приложения? Мало того, что ему могут быть не нужны два описания, так мы по сути ещё и вводим его в заблуждение: `name` — это не «какое-то» название, оно предполагает некоторые ограничения. Во-первых, у него есть некоторая рекомендованная длина, оптимальная для конкретного UI; во-вторых, оно должно консистентно выглядеть в одном списке с другими напитками. В самом деле, будет очень странно смотреться, если среди «Капучино», «Лунго» и «Латте» вдруг появится «Бодрящая свежесть» или «Наш самый качественный кофе». -Эта проблема разворачивается и в другую сторону — UI (наш или партнера) обязательно будет развиваться, в нём будут появляться новые элементы (картинка для кофе, его пищевая ценность, информация об аллергенах и так далее). `product_properties` со временем превратится в свалку из большого количества необязательных полей, и выяснить, задание каких из них приведёт к каким эффектам в каком приложении можно будет только методом проб и ошибок. +Эта проблема разворачивается и в другую сторону — UI (наш или партнёра) обязательно будет развиваться, в нём будут появляться новые элементы (картинка для кофе, его пищевая ценность, информация об аллергенах и так далее). `product_properties` со временем превратится в свалку из большого количества необязательных полей, и выяснить, задание каких из них приведёт к каким эффектам в каком приложении можно будет только методом проб и ошибок. Проблемы, с которыми мы столкнулись — это проблемы *сильной связности*. Каждый раз, предлагая интерфейс, подобный вышеприведённому, мы фактически описываем имплементацию одной сущности (рецепта) через имплементации других (визуального макета, правил локализации). Этот подход противоречит самому принципу проектирования API «сверху вниз», поскольку **низкоуровневые сущности не должны определять высокоуровневые**. @@ -64,7 +64,7 @@ l10n.volume.format(value, language_code, country_code) // l10n.formatVolume('300ml', 'en', 'US') → '10 fl oz' ``` -Чтобы наш API корректно заработал с новым языком или регионом, партнер должен или задать эту функцию, или указать, какую из существующих локализаций необходимо использовать. Для этого мы абстрагируем-и-расширяем API, в соответствии с описанной в предыдущей главе процедурой, и добавляем новый эндпойнт — настройки форматирования: +Чтобы наш API корректно заработал с новым языком или регионом, партнёр должен или задать эту функцию, или указать, какую из существующих локализаций необходимо использовать. Для этого мы абстрагируем-и-расширяем API, в соответствии с описанной в предыдущей главе процедурой, и добавляем новый эндпойнт — настройки форматирования: ``` // Добавляем общее правило форматирования @@ -181,7 +181,7 @@ POST /v1/recipe-builder } ``` -Заметим, что передача идентификатора вновь создаваемой сущности клиентом — не лучший паттерн. Но раз уж мы с самого начала решили, что идентификаторы рецептов — не просто случайные наборы символов, а значимые строки, то нам теперь придётся с этим как-то жить. Очевидно, в такой ситуации мы рискуем многочисленными коллизиями между названиями рецептов разных партнёров, поэтому операцию, на самом деле, следует модифицировать: либо для партнерских рецептов всегда пользоваться парой идентификаторов (партнера и рецепта), либо ввести составные идентификаторы, как мы ранее рекомендовали в [главе 11](#chapter-11-paragraph-8). +Заметим, что передача идентификатора вновь создаваемой сущности клиентом — не лучший паттерн. Но раз уж мы с самого начала решили, что идентификаторы рецептов — не просто случайные наборы символов, а значимые строки, то нам теперь придётся с этим как-то жить. Очевидно, в такой ситуации мы рискуем многочисленными коллизиями между названиями рецептов разных партнёров, поэтому операцию, на самом деле, следует модифицировать: либо для партнёрских рецептов всегда пользоваться парой идентификаторов (партнёра и рецепта), либо ввести составные идентификаторы, как мы ранее рекомендовали в [главе 11](#chapter-11-paragraph-8). ``` POST /v1/recipes/custom diff --git a/src/ru/clean-copy/03-Раздел II. Обратная совместимость/05.md b/src/ru/clean-copy/03-Раздел II. Обратная совместимость/05.md index 0cd2ee9..ad0f25d 100644 --- a/src/ru/clean-copy/03-Раздел II. Обратная совместимость/05.md +++ b/src/ru/clean-copy/03-Раздел II. Обратная совместимость/05.md @@ -90,7 +90,7 @@ PUT /v1/api-types/{api_type} запуска программы на его кофе-машинах */ registerProgramRunHandler(apiType, (context) => { // Инициализируем запуск исполнения - // программы на стороне партнера + // программы на стороне партнёра let execution = initExecution(context, …); // Подписываемся на события // изменения контекста @@ -112,7 +112,7 @@ registerProgramRunHandler(apiType, (context) => { Внимательный читатель может возразить нам, что фактически, если мы посмотрим на номенклатуру возникающих сущностей, мы ничего не изменили в постановке задачи, и даже усложнили её: * вместо вызова метода `takeout` мы теперь генерируем пару событий `takeout_requested`/`takeout_ready`; - * вместо длинного списка методов, которые необходимо реализовать для интеграции API партнера, появляются длинные списки полей сущности `context` и событий, которые она генерирует; + * вместо длинного списка методов, которые необходимо реализовать для интеграции API партнёра, появляются длинные списки полей сущности `context` и событий, которые она генерирует; * проблема устаревания технологии не меняется, вместо устаревших методов мы теперь имеем устаревшие поля и события. Это замечание совершенно верно. Изменение формата API само по себе не решает проблем, связанных с эволюцией функциональности и нижележащей технологии. Формат API решает другую проблему: как оставить при этом код читаемым и поддерживаемым. Почему в примере с интеграцией через методы код становится нечитаемым? Потому что обе стороны *вынуждены* имплементировать функциональность, которая в их контексте бессмысленна; и эта имплементация будет состоять из какого-то (хорошо если явного!) способа ответить, что данная функциональность не поддерживается (или, наоборот, поддерживается всегда и безусловно). @@ -125,7 +125,7 @@ registerProgramRunHandler(apiType, (context) => { Важно также отметить, что, хотя количество сущностей (полей, событий) эффективно удваивается по сравнению с сильно связанным API, это удвоение является качественным, а не количественным. Контекст `program` содержит описание задания в своих терминах (вид напитка, объём, посыпка корицей); контекст `execution` должен эти термины переформулировать для своей предметной области (чтобы быть, в свою очередь, таким же информационным контекстом для ещё более низкоуровневого API). Что важно, `execution`-контекст имеет право эти термины конкретизировать, поскольку его нижележащие объекты будут уже работать в рамках какого-то конкретного API, в то время как `program`-контекст обязан выражаться в общих терминах, применимых к любой возможной нижележащей технологии. -Ещё одним важным свойством слабой связности является то, что она позволяет сущности иметь несколько родительских контекстов. В обычных предметных областях такая ситуация выглядела бы ошибкой дизайна API, но в сложных системах, где присутствуют одновременно несколько агентов, влияющих на состояние системы, такая ситуация не является редкостью. В частности, вы почти наверняка столкнётесь с такого рода проблемами при разработке пользовательского UI. Более подробно о подобных двойных иерархиях мы расскажем в разделе, посвященном разработке SDK. +Ещё одним важным свойством слабой связности является то, что она позволяет сущности иметь несколько родительских контекстов. В обычных предметных областях такая ситуация выглядела бы ошибкой дизайна API, но в сложных системах, где присутствуют одновременно несколько агентов, влияющих на состояние системы, такая ситуация не является редкостью. В частности, вы почти наверняка столкнётесь с такого рода проблемами при разработке пользовательского UI. Более подробно о подобных двойных иерархиях мы расскажем в разделе, посвящённом разработке SDK. #### Инверсия ответственности @@ -136,7 +136,7 @@ registerProgramRunHandler(apiType, (context) => { запуска программы на его кофе-машинах */ registerProgramRunHandler(apiType, (context) => { // Инициализируем запуск исполнения - // программы на стороне партнера + // программы на стороне партнёра let execution = initExecution(context, …); // Подписываемся на события // изменения контекста @@ -159,10 +159,10 @@ registerProgramRunHandler(apiType, (context) => { } ``` -Вновь такое решение выглядит контринтуитивным, ведь мы снова вернулись к сильной связи двух уровней через жестко определённые методы. Однако здесь есть важный момент: мы городим весь этот огород потому, что ожидаем появления альтернативных реализаций *нижележащего* уровня абстракции. Ситуации, когда появляются альтернативные реализации *вышележащего* уровня абстракции, конечно, возможны, но крайне редки. Обычно дерево альтернативных реализаций растёт сверху вниз. +Вновь такое решение выглядит контринтуитивным, ведь мы снова вернулись к сильной связи двух уровней через жёстко определённые методы. Однако здесь есть важный момент: мы городим весь этот огород потому, что ожидаем появления альтернативных реализаций *нижележащего* уровня абстракции. Ситуации, когда появляются альтернативные реализации *вышележащего* уровня абстракции, конечно, возможны, но крайне редки. Обычно дерево альтернативных реализаций растёт сверху вниз. Другой аспект заключается в том, что, хотя серьёзные изменения концепции возможны на любом из уровней абстракции, их вес принципиально разный: - * если меняется технический уровень, это не должно существенно влиять на продукт, а значит — на написанный партнерами код; + * если меняется технический уровень, это не должно существенно влиять на продукт, а значит — на написанный партнёрами код; * если меняется сам продукт, ну например мы начинаем продавать билеты на самолёт вместо приготовления кофе на заказ, сохранять обратную совместимость на промежуточных уровнях API *бесполезно*. Мы вполне можем продавать билеты на самолёт тем же самым API программ и контекстов, да только написанный партнёрами код всё равно надо будет полностью переписывать с нуля. В конечном итоге это приводит к тому, что API вышележащих сущностей меняется медленнее и более последовательно по сравнению с API нижележащих уровней, а значит подобного рода «обратная» жёсткая связь зачастую вполне допустима и даже желательна исходя из соотношения «цена-качество». @@ -179,7 +179,7 @@ execution.prepareTakeout(() => { }); ``` -Надо отметить, что такой подход *в принципе* не противоречит описанному принципу, но нарушает другой — изоляцию уровней абстракции, а поэтому плохо подходит для написания сложных API, в которых не гарантирована жесткая иерархия компонентов. При этом использовать глобальный (или квази-глобальный) менеджер состояния в таких системах вполне возможно, но требуется имплементировать более сложную пропагацию сообщений по иерархии, а именно: подчинённый объект всегда вызывает методы только ближайшего вышестоящего объекта, а уже тот решает, как и каким образом этот вызов передать выше по иерархии. +Надо отметить, что такой подход *в принципе* не противоречит описанному принципу, но нарушает другой — изоляцию уровней абстракции, а поэтому плохо подходит для написания сложных API, в которых не гарантирована жёсткая иерархия компонентов. При этом использовать глобальный (или квази-глобальный) менеджер состояния в таких системах вполне возможно, но требуется имплементировать более сложную пропагацию сообщений по иерархии, а именно: подчинённый объект всегда вызывает методы только ближайшего вышестоящего объекта, а уже тот решает, как и каким образом этот вызов передать выше по иерархии. ``` execution.prepareTakeout(() => { @@ -245,7 +245,7 @@ PUT /v1/partners/{partnerId}/coffee-machines POST /v1/programs/{id}/run ``` -будет работать и с партнерскими кофе-машинами (читай, с третьим видом API). +будет работать и с партнёрскими кофе-машинами (читай, с третьим видом API). #### Делегируй!