1
0
mirror of https://github.com/twirl/The-API-Book.git synced 2025-01-05 10:20:22 +02:00

Введение в проектирование API

This commit is contained in:
Sergey Konstantinov 2020-10-23 17:11:39 +03:00
parent 4ab571e807
commit 4070313067

173
API.ru.md
View File

@ -44,6 +44,24 @@ API — _это обязательство_. Формальное обязате
## I. Проектирование API
Прежде чем излагать рекомендации, нам следует определиться с тем, что мы считаем «хорошим» API, и какую пользу мы получаем от того, что наше API «хорошее».
Начнём со второго вопроса. Очевидно, «хорошесть» API определяется в первую очередь тем, насколько хорошо он помогает разработчикам решать стоящие перед ними задачи. (Можно резонно возразить, что решение задач, стоящих перед разработчиками, не обязательно влечёт за собой выполнение целей, которые мы ставим перед собой как разработчики API. Однако манипуляция общественным мнением не входит в область интересов автора этой книги: здесь и далее я предполагаю, что API существует для того, чтобы разработчики решали с его помощью свои задачи, а не для чего-то ещё.)
Как же дизайн API может помочь разработчику? Очень просто: API должно *решать задачи*, и делать это максимально удобно и понятно. Путь разработчика от формулирования своей задачи до написания работающего кода должен быть максимально коротким. Это, в том числе, означает, что:
* из структуры вашего API должно быть максимально очевидно, как решить ту или иную задачу; в идеале разработчику должно быть достаточно одного взгляда на документацию, чтобы понять, с помощью каких сущностей следует решать его задачу;
* API должно быть читаемым: в идеале разработчик, просто глядя в номенклатуру методов, сразу пишет правильный код, не углубляясь в детали (особенно — детали реализации!); немаловажно уточнить, что из интерфейсов объектов должно быть понятно не только решение задачи, но и возможные ошибки и исключения;
* API должно быть консистентно: при разработке новой функциональности, т.е. при обращении к каким-то незнакомым сущностям в API, разработчик может действовать по аналогии с уже известными ему концепциями API, и его код будет работать.
Однако статическое удобство и понятность API — это простая часть. В конце концов, никто не стремится специально сделать API нелогичным и нечитаемым — всегда при разработке мы начинаем с каких-то понятных концепций и пытаемся их развивать. При минимальном опыте проектирования сложно сделать ядро API, не удовлетворяющее критериям очевидности, читаемости и консистентности.
Проблемы начинаются, когда мы начинаем API развивать. Добавление новой фунциональности рано или поздно приводит к тому, что некогда простое и понятное API становится наслоением разных концепций, а попытки сохранить обратную совместимость приводят к нелогичным, неочевидным и попросту плохим решениям. Отчасти это связано так же и с тем, что невозможно обладать полным знанием о будущем: ваше понимание о «правильном» API тоже будет меняться со временем, как в объективной части (какие задачи решает API и как лучше это сделать), так и в субъективной — что такое очевидность, читабельность и консистентность для вашего API.
Принципы, которые я буду излагать ниже, во многом ориентированы именно на то, чтобы API правильно развивалось во времени и не превращалось в нагромождение разнородных неконсистентных интерфейсов. Важно понимать, что такой подход тоже небесплатен: необходимость держать в голове варианты развития событий и закладывать возможность изменений в API означает избыточность интерфейсов и возможно излишнее абстрагирование. И то, и другое, помимо прочего, усложняет и работу программиста, пользующегося вашим API; усложнение API, закладывание перспектив «на будущее» имеет смысл, только если это будущее у API есть, иначе это просто оверинжиниринг.
#### Обратная совсместимость
### 1. Пирамида контекстов API
Подход, который мы используем для проектирования, состоит из четырёх шагов:
@ -58,7 +76,7 @@ API — _это обязательство_. Формальное обязате
**NB**. Здесь и далее мы будем рассматривать концепции разработки API на примере некоторого гипотетического API заказа кофе в городских кофейнях. На всякий случай сразу уточним, что пример является синтетическим; в реальной ситуации, если бы такое API пришлось проектировать, оно вероятно было бы совсем не похоже на наш выдуманный пример.
=== Определение области применения
### Определение области применения
Ключевой вопрос, который вы должны задать себе четыре раза, выглядит так: какую проблему мы решаем? Задать его следует четыре раза с ударением на каждом из четырёх слов.
@ -89,7 +107,7 @@ API — _это обязательство_. Формальное обязате
На все эти вопросы, в общем случае, простого ответа нет. В идеале ответы на эти вопросы должны даваться с цифрами в руках. Сколько конкретно времени тратится неоптимально, и какого значения мы рассчитываем добиться, располагая какой плотностью кофемашин? Заметим также, что в реальной жизни просчитать такого рода цифры можно в основном для проектов, которые пытаются влезть на уже устоявшийся рынок; если вы пытаетесь сделать что-то новое, то, вероятно, вам придётся ориентироваться в основном на свою интуицию.
==== Почему API?
#### Почему API?
Т.к. наша книга посвящена не просто разработке программного обеспечения, а разработке API, то на все эти вопросы мы должны взглянуть под другим ракурсом: а почему для решения этих задач требуется именно API, а не просто программное обеспечение? В нашем вымышленном примере мы должны спросить себя: зачем нам нужно предоставлять сервис для других разработчиков, чтобы они могли готовить кофе своим клиентам, а не сделать своё приложение для конечного потребителя?
@ -99,7 +117,7 @@ API — _это обязательство_. Формальное обязате
Для нашего умозрительного примера предположим, что в недалеком будущем произошло разделение рынка кофе на две группы игроков: одни предоставляют само железо, кофейные аппараты, а другие имеют доступ к потребителю — примерно как это произошло, например, с рынком авиабилетов, где есть собственно авиакомпании, осуществляющие перевозку, и сервисы планирования путешествий, где люди выбирают варианты перелётов. Мы хотим агрегировать доступ к железу, чтобы владельцы приложений могли встраивать заказ кофе.
==== Что и как
#### Что и как
Закончив со всеми теоретическими упражнениями, мы должны перейти непосредственно к дизайну и разработки API, имея понимание по двум пунктам:
@ -113,140 +131,95 @@ API — _это обязательство_. Формальное обязате
С этими вводными мы можем переходить непосредственно к разработке.
=== Уровни абстракции
### Разделение уровней абстракции
«Разделите свой код на уровни абстракции» - пожалуй, самый общий совет для разработчиков программного обеспечения. Что под этим обычно подразумевается?
«Разделите свой код на уровни абстракции» - пожалуй, самый общий совет для разработчиков программного обеспечения. Однако будет вовсе не преувеличением сказать, что изоляция уровней абстрации — самая сложная задача, стоящая перед разработчиком API.
Вспомним, что программный продукт - это средство связи контекстов, средство преобразования терминов и операций одной предметной области в другую. Чем дальше друг от друга эти области отстоят - тем большее число промежуточных контекстов нужно ввести. Если говорить о разделении уровней абстракции именно с точки зрения API, то оно очень желательно по нескольким причинам:
Прежде чем переходить к теории, следует чётко сформулировать, _зачем_ нужны уровни абстракции и каких целей мы хотим достичь их выделением.
* разделение проекта на несколько независимых частей; архитектура каждой из них, таким образом, становится проще, и упрощается интеграция;
* с помощью такого разделения гораздо легче добиваться кроссплатформенности путём отделения платформо-зависимой логики в отдельный уровень (или уровни) абстракции.
Вспомним, что программный продукт - это средство связи контекстов, средство преобразования терминов и операций одной предметной области в другую. Чем дальше друг от друга эти области отстоят - тем большее число промежуточных передаточных звеньев нам придётся ввести. Вернёмся к нашему примеру с кофейнями. Какие уровни сущностей мы видим?
И главное:
1. Непосредственно состояние кофе-машины и шаги приготовления кофе. Температура, давление, объём воды.
2. У кофе есть мета-характерстики: сорт, вкус, вид напитка.
3. Мы готовим с помощью нашего API *заказ* — один или несколько стаканов кофе с определенной стоимостью.
4. Наши кофе-машины как-то распределены в пространстве (и времени).
5. Кофе-машина принадлежит какой-то сети кофеен, каждая из которых обладает какой-то айдентикой и специальными возможностями.
* упрощается задача для ваших клиентов; правильно разделённые уровни абстракции означают, что разработчикам не придется разбираться со всей номенклатурой сущностей вашего API - им достаточно будет работать только с объектами высокого уровня, отвечающими непосредственно за решение их задач.
Каждый из этих уровней задаёт некоторый срез нашего API, с которым будет работать потребитель. Выделяя иерархию абстракций мы прежде всего стремимся снизить связность различных сущностей нашего API. Это позволит нам добиться нескольких целей:
Вернёмся к нашему примеру с кофейнями. Какие уровни абстракции мы видим?
1. Упрощение работы разработчика и легкость обучения: в каждый момент времени разработчику достаточно будет оперировать только теми сущностями, которые нужны для решения его задачи; и наоборот, плохо выстроенная изоляция приводит к тому, что разработчику нужно держать в голове множество концепций, не имеющих прямого отношения к решаемой задаче.
1. Железный. Непосредственно состояние кофе-машины и шаги приготовления кофе. Температура, давление, объём воды
2. Вкусный. У кофе есть мета-характерстики: сорт, вкус, вид напитка.
3. Торговый. Мы готовим с помощью нашего API заказ — один или несколько стаканов кофе с определенной стоимостью.
4. Геометрический. Наши кофе-машины как-то распределены в пространстве (и времени).
5. Брендовый. Кофе-машина принадлежит какой-то сети кофеен, каждая из которых обладает какой-то айдентикой и специальными возможностями.
2. Возможность поддерживать обратную совместимость; правильно подобранные уровни абстракции позволят нам в дальнейшем добавлять новую функциональность, не меняя интерфейс.
Как мы видим даже на нашем умозрительном примере, строгая иерархия в виде уровней абстракции в реальной жизни обычно не существует. Скорее мы говорим о некоторых частично пересекающихся доменах, отвечающих за разное представление одного и того же. Какие-то из них вложены друг в друга: например, высокоуровневый термин «капучино» однозначно транслируется в набор пошаговых низкоуровневых инструкций. Другие же равноправны: например, рецепт кофе (капучино) и место его приготовления (координаты кофейни) никаких иерархий не образуют.
3. Поддержание интероперабельности. Правильно выделенные низкоуровневые абстракции позволят нам адаптировать наше API к другим платформам, не меняя высокоуровневый интерфейс.
#### Построение иерархии абстракций
Допустим, мы имеем следующий интерфейс:
Как обычно, мы начнём с вопроса "зачем" - если у нас не получается строгая иерархия, зачем нужна нестрогая? В первую очередь для того, чтобы вашим разработчикам было удобнее пользоваться вашим API. Представим, что мы спроектировали вот такой интерфейс для нашего http API:
* `GET /recipes/lungo` возвращает рецепт лунго;
* `POST /coffee-machine/order?machine_id={id}` `{recipe:"lungo"}` размещает на указанной кофе-машине заказ на приготовление лунго и возвращает идентификатор заказа;
* `GET /orders?order_id={id}` возвращает состояние заказа;
* GET /weather/temperature/{city_id} - возвращает текущую температуру в городе с указанным идентификатором;
* GET /weather/temperature/{city_id}/forecast - возвращает прогноз изменения температуры;
* GET /weather/pressure/{city_id} - возвращает атмосферное давление.
И зададимся вопросом, каким образом клиент поймёт, что его заказ готов. Допустим, мы сделаем так: добавим в рецепт лунго эталонный объём, а в состояние заказа — количество уже налитого кофе. Тогда разработчику нужно будет проверить совпадение этих двух цифр, чтобы убедиться, что кофе готов.
Покрывает ли такой интерфейс наши сценарии использования? С одной стороны кажется, что да - каждый потребитель сможет получить нужный ему набор информации.
Такое решение выглядит интуитивно плохим, и это действительно так: оно нарушает все вышеперечисленные принципы:
Но, с другой стороны, будет ли удобно вебмастеру, который хочет просто установить на свой сайт "прогноз погоды", пользоваться таким API? Понятно, что ему не нужны по отдельности ни температура, ни давление - ему нужна ровно та информация, которую захочет увидеть пользователь его ресурса.
1. Для решения задачи «заказать лунго» разработчику нужно обратиться к сущности «рецепт» и выяснить, что у каждого рецепта есть объём. Далее, нужно принять концепцию, что приготовление кофе заканчивается в тот момент, когда объём сравнялся с эталонным. Нет никакого способа об этой конвенции догадаться: она неочевидна и её нужно найти в документации. При этом никакой пользы для разработчика в этом знании нет.
Оптимально было бы ввести ещё один метод:
2. Мы автоматически лишаем себя возможности варьировать объём кофе. Нам придётся или заводить ложные рецепты типа «большой лунго, средний лунго, маленький лунго», либо вводить ещё одну неявную конвенцию: если при создании заказа указать объём кофе, то готовность надо определять именно по нему. Обратите внимание, что тем самым при введении концепции объёма кофе в заказе разработчику так же надо будет изменить код функции, определяющей готовность кофе — а это уже совсем контринтуитивно, и обязательно приведёт к проблемам в будущем.
* GET /weather/{city_id} - возвращает параметры погоды в указанном городе: температуру, давление, прогноз.
3. Вся эта схема полностью неработоспособна, если разные модели кофе-машин производят лунго разного объёма. Для решения задачи «объём лунго зависит от вида машины» нам придётся сделать совсем неприятную вещь: сделать рецепт зависимым от id машины. Тем самым мы начнём активно смешивать уровни абстракции: одной частью нашего API (рецептов) станет невозможно пользоваться без другой части (информации о кофе-машинах). Если разработчик хочет сделать какое-то абстрактное приложение про кофе (без его приготовления) — у него не получится это сделать.
Этот новый ресурс позволит пользователям API оперировать не в метеорологических терминах - а в терминах задачи, которую они решают. Тем самым мы спроектировали интерфейс высокого уровня, опирающийся на интерфейсы нижнего уровня абстракции.
Хорошо, допустим, мы поняли, как сделать плохо. Но как же тогда сделать *хорошо*? Разделение уровней абстракции должно происходить вдоль трёх направлений:
Аналогично, если мы будем проектировать API для новостных ресурсов, то придём к необходимости выделения сущности "картина атмосферных фронтов"; при проектировании API для диспетчерских служб - к сущности "план опасных для полета зон", и так далее.
1. От сценариев использования к их внутренней реализации: высокоуровневые сущности и номенклатура их методов должны напрямую отражать сценарии использования API; низкоуровневый - отражать декомпозицию сценариев на составные части.
Разделение уровней абстракции должно происходить вдоль трёх направлений:
Здесь мы должны явно обратиться к выписанному нами ранее «что» и «как». В идеальном мире высший уровень абстракции вашего API должен быть просто переводом записанной человекочитаемой фразы на машинный язык. Если нужно узнать, готов ли заказ — значит, должен быть метод `is-order-ready` (если мы считаем эту операцию действительно важной и частотной) или хотя бы `GET /order/{id}/status` для того, чтобы явно узнать статус заказа. Эту логику требуется прорастить вниз до самых мелких и частных сценариев типа определения температуры напитка или наличия у исполнителя картонного держателя нужного размера.
* от сценариев использования к их внутренней реализации: высокоуровневые сущности и номенклатура их методов должны напрямую отражать сценарии использования API; низкоуровневый - отражать декомпозицию сценариев на составные части;
* от терминов предметной области пользователя к терминам предметной области исходных данных - в нашем случае от высокоуровневых понятий "прогноз погоды" и "движение атмосферных фронтов" к базовым "температура", "давление", "скорость ветра";
* наконец, от структур данных, в которых удобно оперировать пользователю к структурам данных, максимально приближенных к "сырым" - в нашем случае от "атмосферных фронтов" и "магнитных бурь" - к сырым байтовый данным, описывающим поле атмосферного давления или магнитное поле Земли.
2. От терминов предметной области пользователя к терминам предметной области исходных данных — в нашем случае от высокоуровневых понятий «рецепт», «заказ», «бренд», «кофейня» к низкоуровневым «температура напитка» и «координаты кофе-машины»
Чем дальше находятся друг от друга программные контексты, которые соединяет наше API - тем более глубокая иерархия сущностей должна получиться у нас в итоге. Обращаю ваше внимание, что иерархия не должна быть самоцелью; цель - введение промежуточных ступеней погружения в тонкости предметной области. Избыточная сложность здесь не менее вредна, чем избыточная простота.
3. Наконец, от структур данных, в которых удобно оперировать пользователю к структурам данных, максимально приближенных к «сырым» - в нашем случае от «лунго» и «сети кофеен "Ромашка"» - к сырым байтовый данным, описывающим состояние кофе-машины марки «Доброе утро» в процессе приготовления напитка.
### Уровни абстракции и декомпозиция
Чем дальше находятся друг от друга программные контексты, которые соединяет наше API - тем более глубокая иерархия сущностей должна получиться у нас в итоге.
Приближённость к сценариям использования - не единственная польза правильно построенной иерархии абстракций. Другим критерием качества архитектуры API здесь является снижение связности и декомпозиция объектов API.
#### Изоляция уровней абстракции
Рассмотрим следующий пример организации функциональности карты атмосферных фронтов.
Важное свойство правильно подобранных уровней абстракции, и отсюда требование к их проектированию — это требование изоляции: взамодействие возможно только между сущностями соседних уровней. Если при проектировании выясняется, что для выполнения того или иного действия требуется «перепрыгнуть» уровень абстракции, это явный признак того, что в проекте допущены ошибки.
Первый способ:
Возвращаясь к нашему примеру с готовностью кофе: чтобы выяснить статус заказа (сущность высшего уровня, максимально приближенная к пользователю) необходимо сравнить миллилитры приготовленного кофе (сущность самого низшего уровня, физического, максимально приближенного к железу). Так быть не должно.
* GET /map - возвращает множество геометрий атмосферных объектов, попадающих в заданную область;
* GET /geometry/{geometry_id} - возвращает метаданные о геометрии, в том числе тип объекта, описываемого геометрией (область повышенного или пониженного давления, атмосферный фронт);
* GET /area/{area_id}, GET /front/{front_id} - возвращает метаданные конечного объекта.
Но как быть, если для определения готовности кофе *действительно* нужно сравнить объём приготовленного напитка? Предположим, что кофемашина работает именно так — определяет готовность по объёму?
Порядок работы, вроде бы, ясен: разработчик получает карту и отрисовывает её, опираясь, если это необходимо, на нужные метаданные.
В этой ситуации необходимо «прорастить» статус готовности через все уровни абстракции, например так:
Однако, очевидно, с точки зрения иерархии здесь допущена ошибка: высокоуровневые сущности (карта, атмосферные фронты и области повышенного и пониженного давления) связаны посредством технической сущности - визуальной геометрии объекта, которая по всем критериям должна находиться где-то в самом низу иерархии. Чем это может быть плохо для нас?
1. При проверке готовности заказа — обратиться к заданию на приготовление конкретного рецепта на конкретной кофе-машине и опросить его статус.
2. При проверке статуса задания — обратиться к спецификациям конкретной кофе-машины и выполнить команду сверки приготовленного объёма с эталонным.
3. При выполнении команды сверки — обратиться к физическим датчикам и считать конкретные физические значения.
Предположим, завтра у нас появится клиент, которого не устраивает векторное описание геометрии, и которому нужна растровая картинка. Мы окажемся в сложном положении: либо мы создаем параллельное API именно для работы с растром, либо клиенту придётся забирать и растровую картинку, и векторную - вторую только для того, чтобы через нее получить идентификаторы погодных объектов. Если бы иерархия была выстроена правильно (карта погоды отдает идентификаторы объектов, а уже по идентификаторам можно получить геометрии) - достаточно было бы добавить режим "растровое изображение".
На каждом уровне абстракции понятие «заказ готов» переформулируется в терминах нижележащей предметной области, и так вплоть до физического уровня.
Аналогичные проблемы нас ждут, если в какой-то момент появятся объекты, содержащие более одной геометрии - либо, наоборот, несколько объектов научатся делить одну геометрию. В правильной архитектуре мы просто добавим новые типы ресурсов; в неправильной нам вообще не удастся решить эту ситуацию без слома обратной совместимости.
Если в нашем API мы предоставляем доступ к более низким уровням абстракции, то это будет означать необходимость иметь на каждом уровне свою версию одного и того же метода. Условно:
Оба этих примера эксплуатируют одну и ТЦ же мысль: в иерархии уровней абстракции верхние уровни являются, как правило, более устойчивыми к разного рода изменениям. В самом деле, если объекты верхнего уровня описывают сценарии использования - они гораздо менее вероятно изменятся с развитием технологии. Потребность "мне нужно узнать прогноз погоды на завтра" никак не меняется уже много тысяч лет.
* `GET /orders?order_id={id}` — возвращает статус заказа *и* идентификаторы заданий, созданных в его рамках
* `GET /tasks?task_id={id}` — возращает статус конкретного задания, в том числе идентификатор кофе-машины и идентификаторы рецепта (или иного способа получить эталонные значения объема)
* `GET /machine?machine_id={id}&sensor=volume` — возращает состояние конкретного сенсора конкретной кофе-машины.
Чем универсальнее ваше API в смысле широты тех контекстов, которые вы связываете, тем больше альтернативных реализаций одного и того функционала вам придётся реализовать (см. Пример с растровыми и векторными изображениями) и тем "шире" окажется ваша иерархия уровней абстракции: у вас появится множество объектов, принадлежащих одному уровню; в достаточно большом API неизбежно возникнут поддеревья иерархии - каждая реализация будет опираться на свой, полностью или частично, независимый слой логики.
Может показаться, что соблюдение правила изоляции уровней абстракции является избыточным и заставляет усложнять интерфейс. И это в действительности так: важно понимать, что никакая гибкость, логичность, читабельность и расширяемость не бывает бесплатной. Можно построить API так, чтобы оно выполняло свою функцию с минимальными накладными расходами, по сути — дать интерфейс к микроконтроллерам кофе-машины. Однако пользоваться им будет крайне неудобно, и расширяемость такого API будет нулевой.
### Изоляция уровней абстракции
Дублирование функций на каждом уровне абстракций позволяет добиться важной вещи: возможности сменить нижележащие уровни без необходимости переписывать верхнеуровневый код. Мы можем добавить другие виды кофе-машин с принципиально другими физическими способами определения готовности напитка, и наш метод `GET /orders?order_id={id}` продолжит работать, как работал.
Для эффективной работы с такой сложной и разветвлённой иерархией необходимо добиваться изоляции её уровней, как вертикальной, так и горизонтальной. Попробуем разобраться на примере.
Да, код, который работал с физическим уровнем, придётся переписать. Но, во-первых, это неизбежно: изменение принципов работы физического уровня стэка автоматически означает необходимость переписать код. Во-вторых, такое разделение ставит перед нами четкий вопрос: до какого момента API должно предоставлять публичный доступ? Стоило ли предоставлять пользователю методы физического уровня? В-третьих, грамотное проектирование интерфейсов помогает избежать и этой проблемы, о чем будет подробнее рассказано ниже.
Допустим, мы разрабатываем набор компонент для диспетчерских служб. У нас есть задача отражать на карте погоды движение транспортных средств клиента, и мы спроектировали примерно такие интерфейсы:
* Map - класс собственно карты;
* Vehicle - класс-диспетчеризируемый объект;
* Overlay - графическое представление объекта.
Далее у нас возникает задача отображать перемещение транспортных средств по карте. Так эти данные поступают от клиента, нам необходимо предоставить внешний интерфейс для этого. Как это сделать? Мы примерно представляем, что у клиента будет реализован какой-то поток уведомлений о смене местоположения его транспортных объектов, и мы предоставим ему специальный объект source - сущность, чьей ответственностью является обновление информации об объектах.
Возникает вопрос, каким образом мы свяжем source — объект, хранящий знание о положениях объектов — и overlay — объект непосредственно это знание реализующий. Наивное решение выглядит примерно так:
`source.addOverlay(overlay)`
При вызове этого метода source сохранит ссылку на overlay; при поступлении свежих данных опросит все оверлеи, выяснит идентификатор их родительского vehicle, и, если для данного vehicle пришла обновленная позиция, установит её исходному оверлею.
Хотя на бумаге всё выглядит гладко, нарушение принципов построения уровней абстракции очевидно:
* объект source, имплементирующий логику в терминах клиента, должен быть объектом высшего уровня в иерархии, наравне с Map;
* соответственно, source не должен работать с объектами низшего уровня - оверлеями, - которые, к тому же, инстанцирует не он;
* кроме того, знание о пиксельных координатах объекта является совершенно лишним для source - как объект высокого уровня он должен работать с информацией в терминах клиента - то есть с идентификаторами и географическими координатами.
Чем же нам грозит подобный неправильный интерфейс? Давайте разберёмся.
Мы заставляем высокоуровневый объект преобразовывать геокоординаты в пиксельные. Очевидно, для этого source должен будет обратиться в map и выяснить множество информации, совершенно source ненужной - проекцию и масштаб карты, например. Таким образом, в нашей схеме source обладает знанием о всех прочих объектах нашего API - map, vehicle, overlay.
Чем плоха такая сильная связность? Перечислим основные неприятности, которые поджидают нас на этом пути.
Во-первых, мы задумали source как объект доступа к пользовательским данным. Так как у каждого пользователя обязательно будет своя специфика доступа к данным, то ситуации, когда пользователю нужно будет написать полностью или частично собственную реализацию source, будут возникать регулярно. Прикиньте теперь объём кода, который для этого потребуется: вместо того, чтобы написать адаптер для выдачи пар идентификатор-координаты, пользователю нужно будет реализовать взаимодействие с каждым компонентом API!
В той же ситуации окажемся и мы, если решим сами реализовать какие-то типовые варианты source. В лучшем случае нам придется заняться редактором для выделения общих компонент по взаимодействию с другими сущностями, в худшем - пользоваться методом copy-paste.
Хуже всего здесь то, операция преобразования координат - логически сложная вещь, вряд ли разработчик будет доставать учебник по сферической тригонометрии и, скорее всего, просто найдёт где-нибудь готовый кусок кода. Ошибки в этом коде, которые там неизбежно есть, будут приводить к трудно отлаживаемым артефактам, опять же в силу слабого знакомства разработчика с кодом.
Во-вторых, любые изменения в интерфейсах любого компонента приведут нас к необходимости переписать и source тоже. Если, скажем, мы реализуем для карты возможность вращаться - нам придётся переписать и source, ведь преобразование геокоординат в пиксели сцены является его работой.
В-третьих, любая ошибка в реализации любой из компонент грозит транзитом через source затронуть работу других, напрямую несвязанных компонентов системы. Например, если где-то в получении параметров карты есть ошибка, то при пересчёте координат оверлеев произойдёт исключение, и обновление координат vehicle не произойдёт или произойдёт частично - несмотря на то, что эти операции логически никак не связаны. Таким образом, отказоустойчивость системы и её плавная деградация при отказе одного из компонентов существенно снижается.
В-четвёртых, тестировать такой код будет существенно сложнее - и нам при его имплементации, и сторонним разработчикам при написании своих компонент, поскольку гораздо сложнее тестировать и сам source (много разнородной функциональности и множество зависимостей, которые придётся подменять "заглушками"-mock-ами), и связанные с ним объекты (поскольку написать mock на source тоже нетривиально).
Если суммировать вышесказанное, то можно выделить основные проблемы, которые несёт за собой неправильное выделение уровней абстракции:
* чрезмерное усложнение реализации альтернативных вариантов чересчур сильно связанных сущностей;
* распространение ошибок по связанным сущностям и общее снижение отказоустойчивости системы;
* усложнение работы с системой для внешних разработчиков, необходимость обладания экспертизой в областях, вообще говоря, напрямую не связанных с решаемой задачей;
* усложнение тестирования кода как самого API, так и написанного поверх него;
* необходимость постоянных рефакторингов и копи-паста кода.
В следующем разделе мы попробуем спроектировать конкретные интерфейсы так, чтобы избежать перечисленных проблем.
## Разграничение областей ответственности
### Разграничение областей ответственности
Исходя из описанного в предыдущей главе, мы понимаем, что иерархия абстракций в нашем гипотетическом проекте должна выглядеть примерно так:
* Map, source - сущности верхнего уровня;
* Vehicle - среднего;
* Overlay - нижнего.
* Пользовательский уровень (те сущности, с которыми непосредственно взаимодействует пользователь и сформулированы в понятных для него терминах; например, заказы и виды кофе).
* Исполнительный уровень (конвейер выполнения заказа).
* Физический уровень (непосредственно сами датчики машины).
Здесь «исполнительский» уровень будет очевидно далее детализирован и возможно разделён на несколько других, но мы пока не будем обращать на это внимание, чтобы не усложнять примеры.
// связанность
Каким же образом нам нужно организовать связи между этими объектами так, чтобы, с одной стороны, позволить нашему API быть максимально гибким и одновременно избежать проблем сильной связанности объектов?