1
0
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:
Sergey Konstantinov
2020-10-27 22:32:27 +03:00
parent 64695b7f2f
commit 2bcd2b7948

145
API.ru.md
View File

@@ -16,7 +16,7 @@
Первые два будут интересны скорее разработчикам, третий — и разработчикам, и менеджерам. При этом мы настаиваем, что как раз третий раздел — самый важный для разработчика API. Ввиду того, что API - продукт для разработчиков, перекладывать ответственность за его развитие и поддержку на не-разработчиков неправильно: никто кроме вас самих не понимает так хорошо продуктовые свойства вашего API. Первые два будут интересны скорее разработчикам, третий — и разработчикам, и менеджерам. При этом мы настаиваем, что как раз третий раздел — самый важный для разработчика API. Ввиду того, что API - продукт для разработчиков, перекладывать ответственность за его развитие и поддержку на не-разработчиков неправильно: никто кроме вас самих не понимает так хорошо продуктовые свойства вашего API.
Автор этой книги терпеть не может распространенный подход к написанию технических книг, когда первая мысль по сути вопроса появляется на сотой странице, а предыдущие девяносто девять страниц посвящены пространному введению и подробному описанию того, что же ждёт читателя дальше. Поэтому предисловие мы на этом заканчиваем и переходим к сути вопроса. На этом переходим к делу.
## Введение. Определение API ## Введение. Определение API
@@ -62,11 +62,9 @@
### Обратная совместимость ### Обратная совместимость
Обратная совместимость — это некоторая _временная_ характеристика качества вашего API. Необходимость поддержания обратной совместимости во многом отличает разработки API от разработки программного обеспечения вообще. Обратная совместимость — это некоторая _временна́я_ характеристика качества вашего API. Именно необходимость поддержания обратной совместимости отличает разработку API от разработки программного обеспечения вообще.
Разумеется, обратная совместимость не абсолютна. В некоторых предметных областях выпуск новых обратно несовместимых версий API является вполне рутинной процедурой. Разумеется, обратная совместимость не абсолютна. В некоторых предметных областях выпуск новых обратно несовместимых версий API является вполне рутинной процедурой. Тем не менее, каждый раз, когда выпускается новая обратно несовместимая версия API, всем разработчикам приходится инвестировать какое-то ненулевое количество усилий, чтобы адаптировать свой код к новой версии. В этом плане выпуск новых версий API является некоторого рода «налогом» на потребителей — им нужно тратить вполне осязаемые деньги только для того, чтобы их продукт продолжал работать.
Тем не менее, каждый раз, когда выпускается новая обратно несовместимая версия API, всем разработчикам приходится инвестировать какое-то ненулевое количество усилий, чтобы адаптировать свой код к новой версии. В этом плане выпуск новых версий API является некоторого рода «налогом» на потребителей — им нужно тратить вполне осязаемые деньги только для того, чтобы их продукт продолжал работать.
Конечно, крупные компании с прочным положением на рынке могут позволить себе такой налог взымать. Более того, они могут вводить какие-то санкции за отказ от перехода на новые версии API, вплоть до отключения приложений. Конечно, крупные компании с прочным положением на рынке могут позволить себе такой налог взымать. Более того, они могут вводить какие-то санкции за отказ от перехода на новые версии API, вплоть до отключения приложений.
@@ -76,12 +74,23 @@
Более подробно о политиках версионирования будет рассказано в разделе II. Более подробно о политиках версионирования будет рассказано в разделе II.
### О версионировании
Здесь и далее мы будем придерживаться принципов версионирования ((https://semver.org/ semver)):
1. Версия API задаётся тремя цифрами, вида `1.2.3`
2. Первая цифра (мажорная версия) увеличивается при обратно несовместимых изменениях в API
3. Вторая цифра (минорная версия) увеличивается при добавлении новой функциональности с сохранением обратной совместимости
4. Третья цифра (патч) увеличивается при выпуске новых версий, содержащих только исправление ошибок
Выражения «мажорная версия API» и «версия API, содержащая обратно несовместимые изменения функциональности» тем самым следует считать эквивалентными.
### Замечание о терминологии ### Замечание о терминологии
Разработка программного обеспечения характеризуется, помимо прочего, существованием множества различных парадигм разработки, адепты которых зачастую настроены весьма воинственно по отношению к адептам других парадигм. Поэтому при написании этой книги мы намеренно избегаем слов «метод», «объект», «функция» и так далее, используя нейтральный термин «сущность». Под «сущностью» понимается некоторая атомарная единица функциональности — класс, метод, объект, монада, прототип (нужное подчеркнуть). Разработка программного обеспечения характеризуется, помимо прочего, существованием множества различных парадигм разработки, адепты которых зачастую настроены весьма воинственно по отношению к адептам других парадигм. Поэтому при написании этой книги мы намеренно избегаем слов «метод», «объект», «функция» и так далее, используя нейтральный термин «сущность». Под «сущностью» понимается некоторая атомарная единица функциональности — класс, метод, объект, монада, прототип (нужное подчеркнуть).
## Проектирование API ## I. Проектирование API
### Пирамида контекстов API ### 1. Пирамида контекстов API
Подход, который мы используем для проектирования, состоит из четырёх шагов: Подход, который мы используем для проектирования, состоит из четырёх шагов:
* определение области применения; * определение области применения;
@@ -95,7 +104,7 @@
_NB_. Здесь и далее мы будем рассматривать концепции разработки API на примере некоторого гипотетического API заказа кофе в городских кофейнях. На всякий случай сразу уточним, что пример является синтетическим; в реальной ситуации, если бы такое API пришлось проектировать, оно вероятно было бы совсем не похоже на наш выдуманный пример. _NB_. Здесь и далее мы будем рассматривать концепции разработки API на примере некоторого гипотетического API заказа кофе в городских кофейнях. На всякий случай сразу уточним, что пример является синтетическим; в реальной ситуации, если бы такое API пришлось проектировать, оно вероятно было бы совсем не похоже на наш выдуманный пример.
### Определение области применения ### 2. Определение области применения
Ключевой вопрос, который вы должны задать себе четыре раза, выглядит так: какую проблему мы решаем? Задать его следует четыре раза с ударением на каждом из четырёх слов. Ключевой вопрос, который вы должны задать себе четыре раза, выглядит так: какую проблему мы решаем? Задать его следует четыре раза с ударением на каждом из четырёх слов.
@@ -113,16 +122,16 @@ _NB_. Здесь и далее мы будем рассматривать кон
* Возможно, мы хотим решить проблему выбора и знания? Чтобы человек наиболее полно знал о доступных ему здесь и сейчас опциях. * Возможно, мы хотим решить проблему выбора и знания? Чтобы человек наиболее полно знал о доступных ему здесь и сейчас опциях.
* Возможно, мы оптимизируем время ожидания? Чтобы человеку не пришлось ждать, пока его заказ готовится. * Возможно, мы оптимизируем время ожидания? Чтобы человеку не пришлось ждать, пока его заказ готовится.
* Возможно, мы хотим минимизировать ошибки? Чтобы человек получил именно то, что хотел заказть, не потеряв информацию при разговорном общении либо при настройке незнакомого интерфейса кофе-машины. * Возможно, мы хотим минимизировать ошибки? Чтобы человек получил именно то, что хотел заказть, не потеряв информацию при разговорном общении либо при настройке незнакомого интерфейса кофе-машины.
Вопрос «зачем» — самый важный из тех вопросов, которые вы должны задавать себе. Не только глобально в отношении целей всего проекта, но и локально в отношении каждого кусочка функциональности. **Если вы не можете коротко и понятно ответить на вопрос «зачем эта сущность нужна» — значит, она не нужна**.
Вопрос «зачем» — самый важный из тех вопросов, которые вы должны задавать себе. Не только глобально в отношении целей всего проекта, но и локально в отношении каждого кусочка функциональности. **Если вы не можете коротко и понятно ответить на вопрос «зачем эта сущность нужна» — значит, она не нужна**. Здесь и далее предположим (в целях придания нашему примеру глубины и некоторой упоротости), что мы оптимизируем все три фактора в порядке убывания важности.
Здесь и далее предположим (в целях придания нашему примеру глубины и некоторой упоротости), что мы оптимизируем все три фактора в порядке убывания важности. 2. Правда ли решаемая проблема существует? Дейсвительно ли мы наблюдаем неравномерную загрузку кофейных автоматов по утрам? Правда ли люди страдают от того, что не могут найти поблизости нужный им латте с ореховым сиропом? Действительно ли людям важны те минуты, которые они теряют, стоя в очередях?
2. Правда ли решаемая проблема существует? Дейсвительно ли мы наблюдаем неравномерную загрузку кофейных автоматов по утрам? Правда ли люди страдают от того, что не могут найти поблизости нужный им латте с ореховым сиропом? Действительно ли людям важны те минуты, которые они теряют, стоя в очередях? 3. Действительно ли мы обладаем достаточным ресурсом, чтобы решить эту проблему? Есть ли у нас доступ к достаточному количеству кофемашин и клиентов, чтобы обеспечить работоспособность системы?
3. Действительно ли мы обладаем достаточным ресурсом, чтобы решить эту проблему? Есть ли у нас доступ к достаточному количеству кофемашин и клиентов, чтобы обеспечить работоспособность системы? 4. Наконец, правда ли мы решим проблему? Как мы поймём, что оптимизировали перечисленные факторы?
4. Наконец, правда ли мы решим проблему? Как мы поймём, что оптимизировали перечисленные факторы?
На все эти вопросы, в общем случае, простого ответа нет. В идеале ответы на эти вопросы должны даваться с цифрами в руках. Сколько конкретно времени тратится неоптимально, и какого значения мы рассчитываем добиться, располагая какой плотностью кофемашин? Заметим также, что в реальной жизни просчитать такого рода цифры можно в основном для проектов, которые пытаются влезть на уже устоявшийся рынок; если вы пытаетесь сделать что-то новое, то, вероятно, вам придётся ориентироваться в основном на свою интуицию. На все эти вопросы, в общем случае, простого ответа нет. В идеале ответы на эти вопросы должны даваться с цифрами в руках. Сколько конкретно времени тратится неоптимально, и какого значения мы рассчитываем добиться, располагая какой плотностью кофемашин? Заметим также, что в реальной жизни просчитать такого рода цифры можно в основном для проектов, которые пытаются влезть на уже устоявшийся рынок; если вы пытаетесь сделать что-то новое, то, вероятно, вам придётся ориентироваться в основном на свою интуицию.
@@ -140,13 +149,13 @@ _NB_. Здесь и далее мы будем рассматривать кон
Закончив со всеми теоретическими упражнениями, мы должны перейти непосредственно к дизайну и разработки API, имея понимание по двум пунктам: Закончив со всеми теоретическими упражнениями, мы должны перейти непосредственно к дизайну и разработки API, имея понимание по двум пунктам:
1. Что конкретно мы делаем 1. Что конкретно мы делаем
2. Как мы это делаем 2. Как мы это делаем
В случае нашего кофепримера мы: В случае нашего кофепримера мы:
1. Предоставляем сервисам с большой пользовательской аудиторией API для того, чтобы их потребители могли максимально удобно для себя заказать кофе. 1. Предоставляем сервисам с большой пользовательской аудиторией API для того, чтобы их потребители могли максимально удобно для себя заказать кофе.
2. Для этого мы абстрагируем за нашим HTTP API доступ к «железу» и предоставим методы для выбора вида напитка и места его приготовления и для непосредственно исполнения заказа. 2. Для этого мы абстрагируем за нашим HTTP API доступ к «железу» и предоставим методы для выбора вида напитка и места его приготовления и для непосредственно исполнения заказа.
С этими вводными мы можем переходить непосредственно к разработке. С этими вводными мы можем переходить непосредственно к разработке.
@@ -158,41 +167,47 @@ _NB_. Здесь и далее мы будем рассматривать кон
Вспомним, что программный продукт - это средство связи контекстов, средство преобразования терминов и операций одной предметной области в другую. Чем дальше друг от друга эти области отстоят - тем большее число промежуточных передаточных звеньев нам придётся ввести. Вернёмся к нашему примеру с кофейнями. Какие уровни сущностей мы видим? Вспомним, что программный продукт - это средство связи контекстов, средство преобразования терминов и операций одной предметной области в другую. Чем дальше друг от друга эти области отстоят - тем большее число промежуточных передаточных звеньев нам придётся ввести. Вернёмся к нашему примеру с кофейнями. Какие уровни сущностей мы видим?
1. Непосредственно состояние кофе-машины и шаги приготовления кофе. Температура, давление, объём воды. 1. Непосредственно состояние кофе-машины и шаги приготовления кофе. Температура, давление, объём воды.
2. У кофе есть мета-характерстики: сорт, вкус, вид напитка. 2. У кофе есть мета-характерстики: сорт, вкус, вид напитка.
3. Мы готовим с помощью нашего API *заказ* — один или несколько стаканов кофе с определенной стоимостью. 3. Мы готовим с помощью нашего API *заказ* — один или несколько стаканов кофе с определенной стоимостью.
4. Наши кофе-машины как-то распределены в пространстве (и времени). 4. Наши кофе-машины как-то распределены в пространстве (и времени).
5. Кофе-машина принадлежит какой-то сети кофеен, каждая из которых обладает какой-то айдентикой и специальными возможностями. 5. Кофе-машина принадлежит какой-то сети кофеен, каждая из которых обладает какой-то айдентикой и специальными возможностями.
Каждый из этих уровней задаёт некоторый срез нашего API, с которым будет работать потребитель. Выделяя иерархию абстракций мы прежде всего стремимся снизить связность различных сущностей нашего API. Это позволит нам добиться нескольких целей: Каждый из этих уровней задаёт некоторый срез нашего API, с которым будет работать потребитель. Выделяя иерархию абстракций мы прежде всего стремимся снизить связность различных сущностей нашего API. Это позволит нам добиться нескольких целей:
1. Упрощение работы разработчика и легкость обучения: в каждый момент времени разработчику достаточно будет оперировать только теми сущностями, которые нужны для решения его задачи; и наоборот, плохо выстроенная изоляция приводит к тому, что разработчику нужно держать в голове множество концепций, не имеющих прямого отношения к решаемой задаче. 1. Упрощение работы разработчика и легкость обучения: в каждый момент времени разработчику достаточно будет оперировать только теми сущностями, которые нужны для решения его задачи; и наоборот, плохо выстроенная изоляция приводит к тому, что разработчику нужно держать в голове множество концепций, не имеющих прямого отношения к решаемой задаче.
2. Возможность поддерживать обратную совместимость; правильно подобранные уровни абстракции позволят нам в дальнейшем добавлять новую функциональность, не меняя интерфейс. 2. Возможность поддерживать обратную совместимость; правильно подобранные уровни абстракции позволят нам в дальнейшем добавлять новую функциональность, не меняя интерфейс.
3. Поддержание интероперабельности. Правильно выделенные низкоуровневые абстракции позволят нам адаптировать наше API к другим платформам, не меняя высокоуровневый интерфейс. 3. Поддержание интероперабельности. Правильно выделенные низкоуровневые абстракции позволят нам адаптировать наше API к другим платформам, не меняя высокоуровневый интерфейс.
Допустим, мы имеем следующий интерфейс: Допустим, мы имеем следующий интерфейс:
* `GET /recipes/lungo` возвращает рецепт лунго; * `GET /recipes/lungo` возвращает рецепт лунго;
* `POST /coffee-machine/order?machine_id={id}` `{recipe:"lungo"}` размещает на указанной кофе-машине заказ на приготовление лунго и возвращает идентификатор заказа; * `POST /coffee-machines/order?machine_id={id}` `{recipe:"lungo"}` размещает на указанной кофе-машине заказ на приготовление лунго и возвращает идентификатор заказа;
* `GET /orders?order_id={id}` возвращает состояние заказа; * `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; низкоуровневый - отражать декомпозицию сценариев на составные части. 1. От сценариев использования к их внутренней реализации: высокоуровневые сущности и номенклатура их методов должны напрямую отражать сценарии использования API; низкоуровневый - отражать декомпозицию сценариев на составные части.
Здесь мы должны явно обратиться к выписанному нами ранее «что» и «как». В идеальном мире высший уровень абстракции вашего API должен быть просто переводом записанной человекочитаемой фразы на машинный язык. Если нужно узнать, готов ли заказ — значит, должен быть метод `is-order-ready` (если мы считаем эту операцию действительно важной и частотной) или хотя бы `GET /order/{id}/status` для того, чтобы явно узнать статус заказа. Эту логику требуется прорастить вниз до самых мелких и частных сценариев типа определения температуры напитка или наличия у исполнителя картонного держателя нужного размера. Здесь мы должны явно обратиться к выписанному нами ранее «что» и «как». В идеальном мире высший уровень абстракции вашего API должен быть просто переводом записанной человекочитаемой фразы на машинный язык. Если нужно узнать, готов ли заказ — значит, должен быть метод `is-order-ready` (если мы считаем эту операцию действительно важной и частотной) или хотя бы `GET /orders/{id}/status` для того, чтобы явно узнать статус заказа. Эту логику требуется прорастить вниз до самых мелких и частных сценариев типа определения температуры напитка или наличия у исполнителя картонного держателя нужного размера.
2. От терминов предметной области пользователя к терминам предметной области исходных данных — в нашем случае от высокоуровневых понятий «рецепт», «заказ», «бренд», «кофейня» к низкоуровневым «температура напитка» и «координаты кофе-машины» 2. От терминов предметной области пользователя к терминам предметной области исходных данных — в нашем случае от высокоуровневых понятий «рецепт», «заказ», «бренд», «кофейня» к низкоуровневым «температура напитка» и «координаты кофе-машины»
@@ -200,19 +215,37 @@ _NB_. Здесь и далее мы будем рассматривать кон
Чем дальше находятся друг от друга программные контексты, которые соединяет наше API - тем более глубокая иерархия сущностей должна получиться у нас в итоге. Чем дальше находятся друг от друга программные контексты, которые соединяет наше API - тем более глубокая иерархия сущностей должна получиться у нас в итоге.
В нашем примере с определением готовности кофе мы явно пришли к тому, что нам требуется промежуточный уровень абстракции:
* с одной стороны, «заказ» не должен содержать информацию о датчиках и сенсорах кофе-машины;
* с другой стороны, кофе-машина не должна хранить информацию о свойствах заказа (да и вероятно её API такой возможности и не предоставляет).
Введём промежуточный уровень: нам нужно звено, которое одновременно знает о заказе, рецепте и кофе-машине. Назовём его «уровнем исполнения»: его ответственностью является интерпретация заказа, превращение его в набор команд кофе-машине. Самый простой вариант — ввести абстрактную сущность «задание» `task`:
* заказ порождает одно или несколько заданий, указывая для задания конкретный рецепт и кофе-машину;
* задание в свою очередь оперирует командами кофе-машины и отвечает за интерпретацию состояния датчиков.
Таким образом, нам необходим по крайней мере один новый метод API:
* `GET /tasks?order_id={order_id}` — позволяет получить список заданий по заказу.
NB: важно заметить, что с дальнейшей проработкой уровень исполнения, скорее всего, сам должен будет разделиться на два и более уровня, т.к. от «задача» по сути — просто сущность-зонтик, связывающая в рамках заказа несколько высокоуровневых сущностей, и до манипуляции командами кофе-машины и состоянием сенсоров всё ещё далеко с точки зрения абстрагирования. Но мы пока оставим в таком виде, для удобства дальнейшего изложения.
#### Изоляция уровней абстракции #### Изоляция уровней абстракции
Важное свойство правильно подобранных уровней абстракции, и отсюда требование к их проектированию — это требование изоляции: взамодействие возможно только между сущностями соседних уровней. Если при проектировании выясняется, что для выполнения того или иного действия требуется «перепрыгнуть» уровень абстракции, это явный признак того, что в проекте допущены ошибки. Важное свойство правильно подобранных уровней абстракции, и отсюда требование к их проектированию — это требование изоляции: **взамодействие возможно только между сущностями соседних уровней абстракции**. Если при проектировании выясняется, что для выполнения того или иного действия требуется «перепрыгнуть» уровень абстракции, это явный признак того, что в проекте допущены ошибки.
Возвращаясь к нашему примеру с готовностью кофе: чтобы выяснить статус заказа (сущность высшего уровня, максимально приближенная к пользователю) необходимо сравнить миллилитры приготовленного кофе (сущность самого низшего уровня, физического, максимально приближенного к железу). Так быть не должно. Возвращаясь к нашему примеру с готовностью кофе: как быть, если для определения готовности кофе *действительно* нужно сравнить объём приготовленного напитка? Предположим, что кофемашина работает именно так — определяет готовность по объёму?
Но как быть, если для определения готовности кофе *действительно* нужно сравнить объём приготовленного напитка? Предположим, что кофемашина работает именно так — определяет готовность по объёму? Для этого нам как раз пригодится наша промежуточная сущность «задание».
В этой ситуации необходимо «прорастить» статус готовности через все уровни абстракции, например так: В этой ситуации необходимо «прорастить» статус готовности через все уровни абстракции, например так:
1. При проверке готовности заказа — обратиться к заданию на приготовление конкретного рецепта на конкретной кофе-машине и опросить его статус. 1. При проверке готовности заказа — обратиться к заданию на приготовление конкретного рецепта на конкретной кофе-машине и опросить его статус.
2. При проверке статуса задания — обратиться к спецификациям конкретной кофе-машины и выполнить команду сверки приготовленного объёма с эталонным.
3. При выполнении команды сверки — обратиться к физическим датчикам и считать конкретные физические значения. 2. При проверке статуса задания — обратиться к спецификациям конкретной кофе-машины и выполнить команду сверки приготовленного объёма с эталонным.
3. При выполнении команды сверки — обратиться к физическим датчикам и считать конкретные физические значения.
На каждом уровне абстракции понятие «заказ готов» переформулируется в терминах нижележащей предметной области, и так вплоть до физического уровня. На каждом уровне абстракции понятие «заказ готов» переформулируется в терминах нижележащей предметной области, и так вплоть до физического уровня.
@@ -226,35 +259,31 @@ _NB_. Здесь и далее мы будем рассматривать кон
Дублирование функций на каждом уровне абстракций позволяет добиться важной вещи: возможности сменить нижележащие уровни без необходимости переписывать верхнеуровневый код. Мы можем добавить другие виды кофе-машин с принципиально другими физическими способами определения готовности напитка, и наш метод `GET /orders?order_id={id}` продолжит работать, как работал. Дублирование функций на каждом уровне абстракций позволяет добиться важной вещи: возможности сменить нижележащие уровни без необходимости переписывать верхнеуровневый код. Мы можем добавить другие виды кофе-машин с принципиально другими физическими способами определения готовности напитка, и наш метод `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 одновременно "знает" и про объект реального мира, и про его виртуальное отображение на карте; карта "знает" свою область картографирования - фактически, область на реальной Земле, которую схематически отображает, - и при этом должна предоставлять некоторый контекст для отображения виртуальных графических фигур, и так далее. Если внимательно посмотреть на каждый объект, то мы увидим, что, в итоге, каждый объект оказался в смысле своей ответственности составным: Vehicle одновременно "знает" и про объект реального мира, и про его виртуальное отображение на карте; карта "знает" свою область картографирования - фактически, область на реальной Земле, которую схематически отображает, - и при этом должна предоставлять некоторый контекст для отображения виртуальных графических фигур, и так далее.