You've already forked The-API-Book
mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-08-10 21:51:42 +02:00
Детализация раздела Уровни абстракции + style fix
This commit is contained in:
145
API.ru.md
145
API.ru.md
@@ -16,7 +16,7 @@
|
||||
|
||||
Первые два будут интересны скорее разработчикам, третий — и разработчикам, и менеджерам. При этом мы настаиваем, что как раз третий раздел — самый важный для разработчика API. Ввиду того, что API - продукт для разработчиков, перекладывать ответственность за его развитие и поддержку на не-разработчиков неправильно: никто кроме вас самих не понимает так хорошо продуктовые свойства вашего API.
|
||||
|
||||
Автор этой книги терпеть не может распространенный подход к написанию технических книг, когда первая мысль по сути вопроса появляется на сотой странице, а предыдущие девяносто девять страниц посвящены пространному введению и подробному описанию того, что же ждёт читателя дальше. Поэтому предисловие мы на этом заканчиваем и переходим к сути вопроса.
|
||||
На этом переходим к делу.
|
||||
|
||||
## Введение. Определение API
|
||||
|
||||
@@ -62,11 +62,9 @@
|
||||
|
||||
### Обратная совместимость
|
||||
|
||||
Обратная совместимость — это некоторая _временная_ характеристика качества вашего API. Необходимость поддержания обратной совместимости во многом отличает разработки API от разработки программного обеспечения вообще.
|
||||
Обратная совместимость — это некоторая _временна́я_ характеристика качества вашего API. Именно необходимость поддержания обратной совместимости отличает разработку API от разработки программного обеспечения вообще.
|
||||
|
||||
Разумеется, обратная совместимость не абсолютна. В некоторых предметных областях выпуск новых обратно несовместимых версий API является вполне рутинной процедурой.
|
||||
|
||||
Тем не менее, каждый раз, когда выпускается новая обратно несовместимая версия API, всем разработчикам приходится инвестировать какое-то ненулевое количество усилий, чтобы адаптировать свой код к новой версии. В этом плане выпуск новых версий API является некоторого рода «налогом» на потребителей — им нужно тратить вполне осязаемые деньги только для того, чтобы их продукт продолжал работать.
|
||||
Разумеется, обратная совместимость не абсолютна. В некоторых предметных областях выпуск новых обратно несовместимых версий API является вполне рутинной процедурой. Тем не менее, каждый раз, когда выпускается новая обратно несовместимая версия API, всем разработчикам приходится инвестировать какое-то ненулевое количество усилий, чтобы адаптировать свой код к новой версии. В этом плане выпуск новых версий API является некоторого рода «налогом» на потребителей — им нужно тратить вполне осязаемые деньги только для того, чтобы их продукт продолжал работать.
|
||||
|
||||
Конечно, крупные компании с прочным положением на рынке могут позволить себе такой налог взымать. Более того, они могут вводить какие-то санкции за отказ от перехода на новые версии API, вплоть до отключения приложений.
|
||||
|
||||
@@ -76,12 +74,23 @@
|
||||
|
||||
Более подробно о политиках версионирования будет рассказано в разделе II.
|
||||
|
||||
### О версионировании
|
||||
|
||||
Здесь и далее мы будем придерживаться принципов версионирования ((https://semver.org/ semver)):
|
||||
|
||||
1. Версия API задаётся тремя цифрами, вида `1.2.3`
|
||||
2. Первая цифра (мажорная версия) увеличивается при обратно несовместимых изменениях в API
|
||||
3. Вторая цифра (минорная версия) увеличивается при добавлении новой функциональности с сохранением обратной совместимости
|
||||
4. Третья цифра (патч) увеличивается при выпуске новых версий, содержащих только исправление ошибок
|
||||
|
||||
Выражения «мажорная версия API» и «версия API, содержащая обратно несовместимые изменения функциональности» тем самым следует считать эквивалентными.
|
||||
|
||||
### Замечание о терминологии
|
||||
|
||||
Разработка программного обеспечения характеризуется, помимо прочего, существованием множества различных парадигм разработки, адепты которых зачастую настроены весьма воинственно по отношению к адептам других парадигм. Поэтому при написании этой книги мы намеренно избегаем слов «метод», «объект», «функция» и так далее, используя нейтральный термин «сущность». Под «сущностью» понимается некоторая атомарная единица функциональности — класс, метод, объект, монада, прототип (нужное подчеркнуть).
|
||||
|
||||
## Проектирование API
|
||||
### Пирамида контекстов API
|
||||
## I. Проектирование API
|
||||
### 1. Пирамида контекстов API
|
||||
|
||||
Подход, который мы используем для проектирования, состоит из четырёх шагов:
|
||||
* определение области применения;
|
||||
@@ -95,7 +104,7 @@
|
||||
|
||||
_NB_. Здесь и далее мы будем рассматривать концепции разработки API на примере некоторого гипотетического API заказа кофе в городских кофейнях. На всякий случай сразу уточним, что пример является синтетическим; в реальной ситуации, если бы такое API пришлось проектировать, оно вероятно было бы совсем не похоже на наш выдуманный пример.
|
||||
|
||||
### Определение области применения
|
||||
### 2. Определение области применения
|
||||
|
||||
Ключевой вопрос, который вы должны задать себе четыре раза, выглядит так: какую проблему мы решаем? Задать его следует четыре раза с ударением на каждом из четырёх слов.
|
||||
|
||||
@@ -113,16 +122,16 @@ _NB_. Здесь и далее мы будем рассматривать кон
|
||||
* Возможно, мы хотим решить проблему выбора и знания? Чтобы человек наиболее полно знал о доступных ему здесь и сейчас опциях.
|
||||
* Возможно, мы оптимизируем время ожидания? Чтобы человеку не пришлось ждать, пока его заказ готовится.
|
||||
* Возможно, мы хотим минимизировать ошибки? Чтобы человек получил именно то, что хотел заказть, не потеряв информацию при разговорном общении либо при настройке незнакомого интерфейса кофе-машины.
|
||||
|
||||
Вопрос «зачем» — самый важный из тех вопросов, которые вы должны задавать себе. Не только глобально в отношении целей всего проекта, но и локально в отношении каждого кусочка функциональности. **Если вы не можете коротко и понятно ответить на вопрос «зачем эта сущность нужна» — значит, она не нужна**.
|
||||
|
||||
Вопрос «зачем» — самый важный из тех вопросов, которые вы должны задавать себе. Не только глобально в отношении целей всего проекта, но и локально в отношении каждого кусочка функциональности. **Если вы не можете коротко и понятно ответить на вопрос «зачем эта сущность нужна» — значит, она не нужна**.
|
||||
Здесь и далее предположим (в целях придания нашему примеру глубины и некоторой упоротости), что мы оптимизируем все три фактора в порядке убывания важности.
|
||||
|
||||
Здесь и далее предположим (в целях придания нашему примеру глубины и некоторой упоротости), что мы оптимизируем все три фактора в порядке убывания важности.
|
||||
2. Правда ли решаемая проблема существует? Дейсвительно ли мы наблюдаем неравномерную загрузку кофейных автоматов по утрам? Правда ли люди страдают от того, что не могут найти поблизости нужный им латте с ореховым сиропом? Действительно ли людям важны те минуты, которые они теряют, стоя в очередях?
|
||||
|
||||
2. Правда ли решаемая проблема существует? Дейсвительно ли мы наблюдаем неравномерную загрузку кофейных автоматов по утрам? Правда ли люди страдают от того, что не могут найти поблизости нужный им латте с ореховым сиропом? Действительно ли людям важны те минуты, которые они теряют, стоя в очередях?
|
||||
3. Действительно ли мы обладаем достаточным ресурсом, чтобы решить эту проблему? Есть ли у нас доступ к достаточному количеству кофемашин и клиентов, чтобы обеспечить работоспособность системы?
|
||||
|
||||
3. Действительно ли мы обладаем достаточным ресурсом, чтобы решить эту проблему? Есть ли у нас доступ к достаточному количеству кофемашин и клиентов, чтобы обеспечить работоспособность системы?
|
||||
|
||||
4. Наконец, правда ли мы решим проблему? Как мы поймём, что оптимизировали перечисленные факторы?
|
||||
4. Наконец, правда ли мы решим проблему? Как мы поймём, что оптимизировали перечисленные факторы?
|
||||
|
||||
На все эти вопросы, в общем случае, простого ответа нет. В идеале ответы на эти вопросы должны даваться с цифрами в руках. Сколько конкретно времени тратится неоптимально, и какого значения мы рассчитываем добиться, располагая какой плотностью кофемашин? Заметим также, что в реальной жизни просчитать такого рода цифры можно в основном для проектов, которые пытаются влезть на уже устоявшийся рынок; если вы пытаетесь сделать что-то новое, то, вероятно, вам придётся ориентироваться в основном на свою интуицию.
|
||||
|
||||
@@ -140,13 +149,13 @@ _NB_. Здесь и далее мы будем рассматривать кон
|
||||
|
||||
Закончив со всеми теоретическими упражнениями, мы должны перейти непосредственно к дизайну и разработки API, имея понимание по двум пунктам:
|
||||
|
||||
1. Что конкретно мы делаем
|
||||
2. Как мы это делаем
|
||||
1. Что конкретно мы делаем
|
||||
2. Как мы это делаем
|
||||
|
||||
В случае нашего кофепримера мы:
|
||||
|
||||
1. Предоставляем сервисам с большой пользовательской аудиторией API для того, чтобы их потребители могли максимально удобно для себя заказать кофе.
|
||||
2. Для этого мы абстрагируем за нашим HTTP API доступ к «железу» и предоставим методы для выбора вида напитка и места его приготовления и для непосредственно исполнения заказа.
|
||||
1. Предоставляем сервисам с большой пользовательской аудиторией API для того, чтобы их потребители могли максимально удобно для себя заказать кофе.
|
||||
2. Для этого мы абстрагируем за нашим HTTP API доступ к «железу» и предоставим методы для выбора вида напитка и места его приготовления и для непосредственно исполнения заказа.
|
||||
|
||||
С этими вводными мы можем переходить непосредственно к разработке.
|
||||
|
||||
@@ -158,41 +167,47 @@ _NB_. Здесь и далее мы будем рассматривать кон
|
||||
|
||||
Вспомним, что программный продукт - это средство связи контекстов, средство преобразования терминов и операций одной предметной области в другую. Чем дальше друг от друга эти области отстоят - тем большее число промежуточных передаточных звеньев нам придётся ввести. Вернёмся к нашему примеру с кофейнями. Какие уровни сущностей мы видим?
|
||||
|
||||
1. Непосредственно состояние кофе-машины и шаги приготовления кофе. Температура, давление, объём воды.
|
||||
2. У кофе есть мета-характерстики: сорт, вкус, вид напитка.
|
||||
3. Мы готовим с помощью нашего API *заказ* — один или несколько стаканов кофе с определенной стоимостью.
|
||||
4. Наши кофе-машины как-то распределены в пространстве (и времени).
|
||||
5. Кофе-машина принадлежит какой-то сети кофеен, каждая из которых обладает какой-то айдентикой и специальными возможностями.
|
||||
1. Непосредственно состояние кофе-машины и шаги приготовления кофе. Температура, давление, объём воды.
|
||||
2. У кофе есть мета-характерстики: сорт, вкус, вид напитка.
|
||||
3. Мы готовим с помощью нашего API *заказ* — один или несколько стаканов кофе с определенной стоимостью.
|
||||
4. Наши кофе-машины как-то распределены в пространстве (и времени).
|
||||
5. Кофе-машина принадлежит какой-то сети кофеен, каждая из которых обладает какой-то айдентикой и специальными возможностями.
|
||||
|
||||
Каждый из этих уровней задаёт некоторый срез нашего API, с которым будет работать потребитель. Выделяя иерархию абстракций мы прежде всего стремимся снизить связность различных сущностей нашего API. Это позволит нам добиться нескольких целей:
|
||||
|
||||
1. Упрощение работы разработчика и легкость обучения: в каждый момент времени разработчику достаточно будет оперировать только теми сущностями, которые нужны для решения его задачи; и наоборот, плохо выстроенная изоляция приводит к тому, что разработчику нужно держать в голове множество концепций, не имеющих прямого отношения к решаемой задаче.
|
||||
1. Упрощение работы разработчика и легкость обучения: в каждый момент времени разработчику достаточно будет оперировать только теми сущностями, которые нужны для решения его задачи; и наоборот, плохо выстроенная изоляция приводит к тому, что разработчику нужно держать в голове множество концепций, не имеющих прямого отношения к решаемой задаче.
|
||||
|
||||
2. Возможность поддерживать обратную совместимость; правильно подобранные уровни абстракции позволят нам в дальнейшем добавлять новую функциональность, не меняя интерфейс.
|
||||
2. Возможность поддерживать обратную совместимость; правильно подобранные уровни абстракции позволят нам в дальнейшем добавлять новую функциональность, не меняя интерфейс.
|
||||
|
||||
3. Поддержание интероперабельности. Правильно выделенные низкоуровневые абстракции позволят нам адаптировать наше API к другим платформам, не меняя высокоуровневый интерфейс.
|
||||
3. Поддержание интероперабельности. Правильно выделенные низкоуровневые абстракции позволят нам адаптировать наше API к другим платформам, не меняя высокоуровневый интерфейс.
|
||||
|
||||
Допустим, мы имеем следующий интерфейс:
|
||||
|
||||
* `GET /recipes/lungo` возвращает рецепт лунго;
|
||||
* `POST /coffee-machine/order?machine_id={id}` `{recipe:"lungo"}` размещает на указанной кофе-машине заказ на приготовление лунго и возвращает идентификатор заказа;
|
||||
* `GET /orders?order_id={id}` возвращает состояние заказа;
|
||||
* `GET /recipes/lungo` возвращает рецепт лунго;
|
||||
* `POST /coffee-machines/order?machine_id={id}` `{recipe:"lungo"}` размещает на указанной кофе-машине заказ на приготовление лунго и возвращает идентификатор заказа;
|
||||
* `GET /orders?order_id={id}` возвращает состояние заказа;
|
||||
|
||||
И зададимся вопросом, каким образом клиент поймёт, что его заказ готов. Допустим, мы сделаем так: добавим в рецепт лунго эталонный объём, а в состояние заказа — количество уже налитого кофе. Тогда разработчику нужно будет проверить совпадение этих двух цифр, чтобы убедиться, что кофе готов.
|
||||
И зададимся вопросом, каким образом разработчик определит, что заказ клиента готов. Допустим, мы сделаем так: добавим в рецепт лунго эталонный объём, а в состояние заказа — количество уже налитого кофе. Тогда разработчику нужно будет проверить совпадение этих двух цифр, чтобы убедиться, что кофе готов.
|
||||
|
||||
Такое решение выглядит интуитивно плохим, и это действительно так: оно нарушает все вышеперечисленные принципы:
|
||||
|
||||
1. Для решения задачи «заказать лунго» разработчику нужно обратиться к сущности «рецепт» и выяснить, что у каждого рецепта есть объём. Далее, нужно принять концепцию, что приготовление кофе заканчивается в тот момент, когда объём сравнялся с эталонным. Нет никакого способа об этой конвенции догадаться: она неочевидна и её нужно найти в документации. При этом никакой пользы для разработчика в этом знании нет.
|
||||
1. Для решения задачи «заказать лунго» разработчику нужно обратиться к сущности «рецепт» и выяснить, что у каждого рецепта есть объём. Далее, нужно принять концепцию, что приготовление кофе заканчивается в тот момент, когда объём сравнялся с эталонным. Нет никакого способа об этой конвенции догадаться: она неочевидна и её нужно найти в документации. При этом никакой пользы для разработчика в этом знании нет.
|
||||
|
||||
2. Мы автоматически лишаем себя возможности варьировать объём кофе. Нам придётся или заводить ложные рецепты типа «большой лунго», «средний лунго», «маленький лунго», либо вводить ещё одну неявную конвенцию: если при создании заказа указать объём кофе, то готовность надо определять именно по нему. Обратите внимание, что тем самым при введении концепции объёма кофе в заказе разработчику так же надо будет изменить код функции, определяющей готовность кофе — а это уже совсем контринтуитивно, и обязательно приведёт к проблемам в будущем.
|
||||
2. Мы автоматически получаем проблемы, если захотим варьировать размер кофе. Допустим, в какой-то момент мы захотим представить пользователю выбор, сколько конкретно миллилитров лунго он желает. Тогда нам придётся проделать один из следующих трюков:
|
||||
* или мы фиксируем список допустимых объёмов и заводим фиктивные рецепты типа `/recipes/small-lungo`, `recipes/large-lungo`. Почему фиктивные? Потому что рецепт один и тот же, меняется только объём. Нам придётся либо тиражировать одинаковые рецепты, отличающиеся только объёмом, либо вводить какое-то «наследование» рецептов, чтобы можно было указать базовый рецепт и только переопределить объём;
|
||||
* или мы модифицируем интерфейс, объявляя объём кофе, указанный в рецепте, значением по умолчанию; при размещении заказа мы разрешаем указать объём, отличный от эталонного:
|
||||
`POST /coffee-machines/order?machine_id={id}` `{recipe:"lungo","volume":"800ml"}`
|
||||
Для таких кофе произвольного объёма нужно будет получать требуемый объём не из `GET /recipes`, а из `GET /orders`. Сделав так, мы сразу получаем клубок из связанных проблем:
|
||||
* разработчик, которому придётся поддержать эту функциональность, имеет высокие шансы сделать ошибку: добавив поддержку произвольного объёма кофе в код, работающий с `POST /coffee-machines/order` нужно не забыть переписать код проверки готовности заказа;
|
||||
* мы получим классическую ситуацию, когда одно и то же поле (объём кофе) значит разные вещи в разных интерфейсах. В `GET /recipes` поле «объём» теперь значит «объём, который будет запрошен, если не передать его явно в `POST /coffee-machines/orders`; переименовать его в «объём по умолчанию» уже не получиться, с этой проблемой теперь придётся жить.
|
||||
|
||||
3. Вся эта схема полностью неработоспособна, если разные модели кофе-машин производят лунго разного объёма. Для решения задачи «объём лунго зависит от вида машины» нам придётся сделать совсем неприятную вещь: сделать рецепт зависимым от id машины. Тем самым мы начнём активно смешивать уровни абстракции: одной частью нашего API (рецептов) станет невозможно пользоваться без другой части (информации о кофе-машинах). Если разработчик хочет сделать какое-то абстрактное приложение про кофе (без его приготовления) — у него не получится это сделать.
|
||||
3. Вся эта схема полностью неработоспособна, если разные модели кофе-машин производят лунго разного объёма. Для решения задачи «объём лунго зависит от вида машины» нам придётся сделать совсем неприятную вещь: сделать рецепт зависимым от id машины. Тем самым мы начнём активно смешивать уровни абстракции: одной частью нашего API (рецептов) станет невозможно пользоваться без другой части (информации о кофе-машинах). Что немаловажно, от разработчиков потребуется изменить логику своего приложения: если раньше они могли предлагать сначала выбрать объём, а потом кофе-машину, то теперь им придётся полностью изменить этот шаг.
|
||||
|
||||
Хорошо, допустим, мы поняли, как сделать плохо. Но как же тогда сделать *хорошо*? Разделение уровней абстракции должно происходить вдоль трёх направлений:
|
||||
|
||||
1. От сценариев использования к их внутренней реализации: высокоуровневые сущности и номенклатура их методов должны напрямую отражать сценарии использования API; низкоуровневый - отражать декомпозицию сценариев на составные части.
|
||||
|
||||
Здесь мы должны явно обратиться к выписанному нами ранее «что» и «как». В идеальном мире высший уровень абстракции вашего API должен быть просто переводом записанной человекочитаемой фразы на машинный язык. Если нужно узнать, готов ли заказ — значит, должен быть метод `is-order-ready` (если мы считаем эту операцию действительно важной и частотной) или хотя бы `GET /order/{id}/status` для того, чтобы явно узнать статус заказа. Эту логику требуется прорастить вниз до самых мелких и частных сценариев типа определения температуры напитка или наличия у исполнителя картонного держателя нужного размера.
|
||||
Здесь мы должны явно обратиться к выписанному нами ранее «что» и «как». В идеальном мире высший уровень абстракции вашего API должен быть просто переводом записанной человекочитаемой фразы на машинный язык. Если нужно узнать, готов ли заказ — значит, должен быть метод `is-order-ready` (если мы считаем эту операцию действительно важной и частотной) или хотя бы `GET /orders/{id}/status` для того, чтобы явно узнать статус заказа. Эту логику требуется прорастить вниз до самых мелких и частных сценариев типа определения температуры напитка или наличия у исполнителя картонного держателя нужного размера.
|
||||
|
||||
2. От терминов предметной области пользователя к терминам предметной области исходных данных — в нашем случае от высокоуровневых понятий «рецепт», «заказ», «бренд», «кофейня» к низкоуровневым «температура напитка» и «координаты кофе-машины»
|
||||
|
||||
@@ -200,19 +215,37 @@ _NB_. Здесь и далее мы будем рассматривать кон
|
||||
|
||||
Чем дальше находятся друг от друга программные контексты, которые соединяет наше API - тем более глубокая иерархия сущностей должна получиться у нас в итоге.
|
||||
|
||||
В нашем примере с определением готовности кофе мы явно пришли к тому, что нам требуется промежуточный уровень абстракции:
|
||||
|
||||
* с одной стороны, «заказ» не должен содержать информацию о датчиках и сенсорах кофе-машины;
|
||||
* с другой стороны, кофе-машина не должна хранить информацию о свойствах заказа (да и вероятно её API такой возможности и не предоставляет).
|
||||
|
||||
Введём промежуточный уровень: нам нужно звено, которое одновременно знает о заказе, рецепте и кофе-машине. Назовём его «уровнем исполнения»: его ответственностью является интерпретация заказа, превращение его в набор команд кофе-машине. Самый простой вариант — ввести абстрактную сущность «задание» `task`:
|
||||
|
||||
* заказ порождает одно или несколько заданий, указывая для задания конкретный рецепт и кофе-машину;
|
||||
* задание в свою очередь оперирует командами кофе-машины и отвечает за интерпретацию состояния датчиков.
|
||||
|
||||
Таким образом, нам необходим по крайней мере один новый метод API:
|
||||
|
||||
* `GET /tasks?order_id={order_id}` — позволяет получить список заданий по заказу.
|
||||
|
||||
NB: важно заметить, что с дальнейшей проработкой уровень исполнения, скорее всего, сам должен будет разделиться на два и более уровня, т.к. от «задача» по сути — просто сущность-зонтик, связывающая в рамках заказа несколько высокоуровневых сущностей, и до манипуляции командами кофе-машины и состоянием сенсоров всё ещё далеко с точки зрения абстрагирования. Но мы пока оставим в таком виде, для удобства дальнейшего изложения.
|
||||
|
||||
#### Изоляция уровней абстракции
|
||||
|
||||
Важное свойство правильно подобранных уровней абстракции, и отсюда требование к их проектированию — это требование изоляции: взамодействие возможно только между сущностями соседних уровней. Если при проектировании выясняется, что для выполнения того или иного действия требуется «перепрыгнуть» уровень абстракции, это явный признак того, что в проекте допущены ошибки.
|
||||
Важное свойство правильно подобранных уровней абстракции, и отсюда требование к их проектированию — это требование изоляции: **взамодействие возможно только между сущностями соседних уровней абстракции**. Если при проектировании выясняется, что для выполнения того или иного действия требуется «перепрыгнуть» уровень абстракции, это явный признак того, что в проекте допущены ошибки.
|
||||
|
||||
Возвращаясь к нашему примеру с готовностью кофе: чтобы выяснить статус заказа (сущность высшего уровня, максимально приближенная к пользователю) необходимо сравнить миллилитры приготовленного кофе (сущность самого низшего уровня, физического, максимально приближенного к железу). Так быть не должно.
|
||||
Возвращаясь к нашему примеру с готовностью кофе: как быть, если для определения готовности кофе *действительно* нужно сравнить объём приготовленного напитка? Предположим, что кофемашина работает именно так — определяет готовность по объёму?
|
||||
|
||||
Но как быть, если для определения готовности кофе *действительно* нужно сравнить объём приготовленного напитка? Предположим, что кофемашина работает именно так — определяет готовность по объёму?
|
||||
Для этого нам как раз пригодится наша промежуточная сущность «задание».
|
||||
|
||||
В этой ситуации необходимо «прорастить» статус готовности через все уровни абстракции, например так:
|
||||
|
||||
1. При проверке готовности заказа — обратиться к заданию на приготовление конкретного рецепта на конкретной кофе-машине и опросить его статус.
|
||||
2. При проверке статуса задания — обратиться к спецификациям конкретной кофе-машины и выполнить команду сверки приготовленного объёма с эталонным.
|
||||
3. При выполнении команды сверки — обратиться к физическим датчикам и считать конкретные физические значения.
|
||||
1. При проверке готовности заказа — обратиться к заданию на приготовление конкретного рецепта на конкретной кофе-машине и опросить его статус.
|
||||
|
||||
2. При проверке статуса задания — обратиться к спецификациям конкретной кофе-машины и выполнить команду сверки приготовленного объёма с эталонным.
|
||||
|
||||
3. При выполнении команды сверки — обратиться к физическим датчикам и считать конкретные физические значения.
|
||||
|
||||
На каждом уровне абстракции понятие «заказ готов» переформулируется в терминах нижележащей предметной области, и так вплоть до физического уровня.
|
||||
|
||||
@@ -226,35 +259,31 @@ _NB_. Здесь и далее мы будем рассматривать кон
|
||||
|
||||
Дублирование функций на каждом уровне абстракций позволяет добиться важной вещи: возможности сменить нижележащие уровни без необходимости переписывать верхнеуровневый код. Мы можем добавить другие виды кофе-машин с принципиально другими физическими способами определения готовности напитка, и наш метод `GET /orders?order_id={id}` продолжит работать, как работал.
|
||||
|
||||
Да, код, который работал с физическим уровнем, придётся переписать. Но, во-первых, это неизбежно: изменение принципов работы физического уровня автоматически означает необходимость переписать код. Во-вторых, такое разделение ставит перед нами четкий вопрос: до какого момента API должно предоставлять публичный доступ? Стоило ли предоставлять пользователю методы физического уровня? В-третьих, грамотное проектирование интерфейсов помогает избежать и этой проблемы, о чем будет подробнее рассказано ниже.
|
||||
Да, код, который работал с физическим уровнем, придётся переписать. Но, во-первых, это неизбежно: изменение принципов работы физического уровня автоматически означает необходимость переписать код. Во-вторых, такое разделение ставит перед нами четкий вопрос: до какого момента API должно предоставлять публичный доступ? Стоило ли предоставлять пользователю методы физического уровня?
|
||||
|
||||
### Разграничение областей ответственности
|
||||
|
||||
Исходя из описанного в предыдущей главе, мы понимаем, что иерархия абстракций в нашем гипотетическом проекте должна выглядеть примерно так:
|
||||
|
||||
* Пользовательский уровень (те сущности, с которыми непосредственно взаимодействует пользователь и сформулированы в понятных для него терминах; например, заказы и виды кофе).
|
||||
* Исполнительный уровень (конвейер выполнения заказа).
|
||||
* Физический уровень (непосредственно сами датчики машины).
|
||||
* Пользовательский уровень (те сущности, с которыми непосредственно взаимодействует пользователь и сформулированы в понятных для него терминах; например, заказы и виды кофе).
|
||||
* Физический уровень (непосредственно сами датчики машины).
|
||||
|
||||
Здесь «исполнительский» уровень будет очевидно далее детализирован и возможно разделён на несколько других, но мы пока не будем обращать на это внимание, чтобы не усложнять примеры.
|
||||
|
||||
// связанность
|
||||
Теперь нам необходимо определить ответственность каждой сущности: в чём смысл её существования в рамках нашего API, какие действия можно выполнять с самой сущностью, а какие — делегировать другим объектам. Фактически, нам нужно применить «зачем-принцип» к каждой отдельной сущности нашего API.
|
||||
|
||||
Каким же образом нам нужно организовать связи между этими объектами так, чтобы, с одной стороны, позволить нашему API быть максимально гибким и одновременно избежать проблем сильной связанности объектов?
|
||||
Для этого нам нужно пройти по нашему API и сформулировать в терминах предметной области, что представляет из себя каждый объект. Напомню, что из концепции уровней абстракции следует, что каждый уровень иерархии — это некоторая собственная промежуточная предметная область, ступенька, по которой мы переходим от описания задачи в терминах одного связываемого контекста («заказанный пользователем лунго») к описанию в терминах второго («задание кофе-машине на выполнение указанной программы»).
|
||||
|
||||
Для этого необходимо, в первую очередь, определить ответственность каждой сущности: в чём смысл её существования в рамках нашего API, какие действия объект должен уметь выполнять сам а какие - делегировать другим объектам. Фактически, нам нужно применить "зачем-принцип" к каждой отдельной сущности нашего API.
|
||||
В нашем умозрительном примере получится примерно так:
|
||||
|
||||
Из предыдущей главы мы выяснили, например, что ответственностью source является возврат пар идентификатор-координаты, и ничего более. Попробуем воспроизвести подобные рассуждения в отношении других объектов.
|
||||
1. Заказ `order` — описывает некоторую логическую единицу взаимодействия с пользователем. Заказ можно:
|
||||
* создавать
|
||||
* проверять статус
|
||||
* получать или отменять
|
||||
2. Рецепт `recipe` — описывает «идеальную модель» вида кофе, его потребительские свойства. Рецепт в данном контексте для нас неизменяемая сущность, которую можно только просмотреть и выбрать.
|
||||
3. Кофе-машина `cofee-machine` — модель объекта реального мира. Мы можем:
|
||||
* получать статус машины
|
||||
* получать и изменять состояние машины
|
||||
|
||||
Для этого нам нужно пройти по нашему API и сформулировать в терминах предметной области, что представляет из себя каждый объект. Напомню, что из концепции уровней абстракции следует, что каждый уровень иерархии - это некоторая собственная промежуточная предметная область, ступенька, по которой мы переходим от описания задачи в терминах одного связываемого контекста ("карта погоды с движущимися по ней транспортными средствами") к описанию в терминах второго ("SVG-объект, заданный в вычисляемых извне пиксельных координатах").
|
||||
|
||||
В этом смысле проще всего описать объект Vehicle - это программная сущность, представляющая собой абстракцию над объектом реального мира. Её задачей, очевидно, является описание характеристик реального объекта, которые нужны для решения поставленных задач. В нашем случае это идентификатор (или иной способ опознания объекта) и его географическое положение. Если в нашем API появятся какие-то расширенные сведения об реальном объекте - скажем, название или скорость, - очевидно, они будут привязаны к тому же объекту Vehicle.
|
||||
|
||||
Кроме того, ещё одной задачей Vehicle является описание собственного представления в интерфейсе - скажем, параметров иконки, если наш Vehicle должен выглядеть для пользователя интерфейса как значок на карте.
|
||||
|
||||
С оверлеем всё тоже вполне ясно - это некоторый графический примитив, который должен уметь интерпретировать опции Vehicle и отображать заданный значок. Кроме того, оверлей должен уметь отображаться в произвольных пиксельных координатах в контексте карты.
|
||||
|
||||
Наконец, что такое "карта" в терминах решаемой задачи? Это схематическое изображение фрагмента земной поверхности, согласно запрошенным координатам и масштабу. В чём заключается ответственность карты в нашей иерархии? Очевидно, в предоставлении данных о наблюдаемой пользователем области (координаты и масштаб) и информации об изменениях этих параметров. Кроме того, ответственностью карты в том или ином виде является пересчёт координат (очевидно, карта "знает", в какой проекции она нарисована) и предоставление возможности отрисовать поверх себя графические фигуры (оверлеи).
|
||||
|
||||
Если внимательно посмотреть на каждый объект, то мы увидим, что, в итоге, каждый объект оказался в смысле своей ответственности составным: Vehicle одновременно "знает" и про объект реального мира, и про его виртуальное отображение на карте; карта "знает" свою область картографирования - фактически, область на реальной Земле, которую схематически отображает, - и при этом должна предоставлять некоторый контекст для отображения виртуальных графических фигур, и так далее.
|
||||
|
||||
|
Reference in New Issue
Block a user