From a9876815a9a8d0eb816496d2949e5a30eec7745c Mon Sep 17 00:00:00 2001 From: Sergey Konstantinov Date: Mon, 15 Jun 2020 23:36:29 +0300 Subject: [PATCH] =?UTF-8?q?html=20=E2=86=92=20md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + API.ru.html | 469 ---------------------------------------------------- API.ru.md | 378 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 379 insertions(+), 469 deletions(-) create mode 100644 .gitignore delete mode 100644 API.ru.html create mode 100644 API.ru.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..600d2d3 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vscode \ No newline at end of file diff --git a/API.ru.html b/API.ru.html deleted file mode 100644 index e86cf55..0000000 --- a/API.ru.html +++ /dev/null @@ -1,469 +0,0 @@ - - - - API - - -
-

Сергей Константинов

-

API

-
-

Об авторе

-

Введение

- -

Прежде чем говорить о разработке API, необходимо для начала договориться о том, что же такое API. Энциклопедия скажет нам, что API — это программный интерфейс приложений. Это точное определение, но бессмысленное. Примерно как определение человека по Платону: «двуногое без перьев» — определение точное, но никоим образом не дающее нам представление о том, чем на самом деле человек примечателен. (Да и не очень-то и точное: Диоген Синопский как-то ощипал петуха и заявил, что это человек Платона; пришлось дополнить определение уточнением «с плоскими ногтями».)

- -

Что же такое API по смыслу, а не по формальному определению?

- -

Вероятно, вы сейчас читаете эту книгу посредством браузера. Чтобы браузер смог отобразить эту страничку, должны корректно отработать: разбор URL согласно спецификации; служба DNS; соединение по протоколу TLS; передача данных по протоколу HTTP; разбор HTML-документа; разбор CSS-документа; корректный рендеринг HTML+CSS.

- -

Но это только верхушка айсберга. Для работы HTTP необходима корректная работа всего сетевого стека, который состоит из 4-5, а то и больше, протоколов разных уровней. Разбор HTML-документа производится согласно сотням различных спецификаций. Рендеринг документа обращается к нижележащему API операционной системы, а также напрямую к API видеокарты. И так далее, и тому подобное — вплоть до того, что наборы команд современных CISC-процессоров имплементируются поверх API микрокоманд.

- -

Иными словами, десятки, если не сотни, различных API должны отработать корректно для выполнения простейших действий типа просмотра web-страницы; без надёжной работы каждого из них современные информационные технологии попросту не могли бы существовать.

- -

API — это обязательство. Формальное обязательство связывать между собой различные программируемые контексты.

- -

Об относительности критериев качества

-

Несмотря на всё написанное выше, я вовсе не призываю вас подходить к каждой задаче с дополнительными наборами требований. Вовсе даже наоборот: я скорее призываю эти требования сокращать.

- -

У каждого инструмента, включая и API, есть своя область применимости, и это важно понимать. Допустим, вы пишете, скажем, скрипт импорта данных из Википедии для того, чтобы залить в базу данных вашего продукта какие-то тестовые данные, и вы собираетесь воспользоваться им один раз. В этом случае, очевидно, совершенно неважно, правильная ли у него архитектура и понятен ли его API. Нет, разумеется, вы можете спроектировать и реализовать этот скрипт «правильно», гибко и универсально, чтобы вы могли им гордиться. Но зачем?

- -

Любая красота сложна. Правильная архитектура требует больших затрат времени и сил, часто — неоднократного полного переписывания базовых модулей. Правильное API требует времени и сил ещё больше.

- -

Понимать, где ваши усилия полезнее и нужнее — пожалуй, не менее важно, чем их (усилий) приложение.

- -

Эти вещи кажутся очевидными на простых примерах; со сложными же проектами, поверьте, то же самое. Достоинства и недостатки любой методологии, как правило, отлично известны, однако редко когда выбор конкретного фреймворка, паттерна или подхода опирается на анализ соответствия их достоинств и недостатков проекту.

- -

Есть ещё один момент, который хочется подчеркнуть: инструменты должны соответствовать конкретным обстоятельствам — предметной области, планируемому жизненному циклу продукта и, что немаловажно, компетенциям команды. Во многих случаях неидеальный инструмент, с которым команда привыкла работать, предпочтительнее неидеального, который придётся осваивать по ходу.

- -

О структуре этой книги

- -

Книга, которую вы держите в руках, состоит из трех больших разделов.

- -

В первом разделе мы поговорим о проектировании API на стадии разработки концепции - как грамотно выстроить архитектуру, от крупноблочного планирования до конечных интерфейсов.

- -

Второй раздел будет посвящён жизненному циклу API - как интерфейсы эволюционируют со временем и как развивать продукт так, чтобы отвечать потребностям пользователей.

- -

Наконец, третий раздел будет касаться больше не-разработческих сторон жизни API - поддержки, маркетинга, достижения контакта с аудиторией.

- -

Первые два будут интересны скорее разработчикам, третий — и разработчикам, и менеджерам. При этом я настаиваю, что как раз третий раздел — самый важный для разработчика API. Ввиду того, что API - продукт для разработчиков, перекладывать ответственность за его развитие и поддержку на не-разработчиков неправильно: никто кроме вас самих не понимает так хорошо продуктовые свойства вашего API.

- -

Проектирование API

- -

Подход, который мы используем для проектирования, состоит из четырёх шагов:

- - - -

Этот алгоритм строит API сверху вниз, от общих требований и сценариев использования до конкретной номенклатуры сущностей; фактически, двигаясь этим путем, вы получите на выходе готовое API - за что мы и ценим этот подход.

- -

Поскольку я являюсь убеждённым приверженцем практического подхода, здесь и далее я буду рассматривать проектирование API на конкретных примерах. В качестве такого примера я выбрал гипотетическое API некоей метеорологической службы. На всякий случай, предупрежу заранее, что пример является выдуманным от начала до конца; если бы мне предложили заниматься таким API в реальности - я бы прежде всего потратил время на детальное исследование предметной области, и наверняка пришёл бы совсем к другим решениям в итоге. Но для наших учебных целей такой умозрительный пример вполне допустим, поскольку я ставлю своей целью, прежде всего, объяснение того, как надо думать и не стремлюсь вложить в голову читателя какие-то конкретные решения и рецепты успеха.

- -

Определение области применения

- -

Первый вопрос, на который мы должны ответить, принимая решение о разработке API - "зачем?" Кто и как будет пользоваться нашей программной связью контекстов?

- -

Рассмотрим следующий пример. Допустим, мы умеем предсказывать погоду и располагаем возможностью предоставлять данные о погоде в машиночитаемом виде. Стоит ли нам разрабатывать API? Давайте подумаем.

- -

Во-первых, необходимо понять, какую такую функциональность мы можем предоставлять потребителям, что они воспользуются API, а не будут разрабатывать сами?

- -

По возрастанию сложности самостоятельной реализации такого функционала это будут: получение информации о текущей погоде; получение информации о прогнозе; получение информации о климате какой-то местности; получение информации о "внутреннем устройстве" предметной области - зоны высокого и низкого давления, атмосферные фронты и их движение.

- -

Зачем такого рода функциональность может понадобиться? Вот несколько очевидных ответов: - -

- -

Для удовлетворения первой группы потребностей потребители будут сами приходить на ресурсы, принадлежащие нашим потенциальным клиентам - популярные локальные веб-сайты (например, на городские порталы за краткосрочным прогнозом погоды или на туристические агрегаторы за информацией о климате) или разного рода приложения для "умных" устройств (телефонов, часов, планшетов, телевизоров и даже домов) или специализированные новостные ресурсы. Вторую группу потребностей будут закрывать компании, производящие программное обеспечение для диспетчерских служб - либо, возможно, проприетарные решения самих транспортных компаний.

- -

Допустим мы исследовали и рынок, и собственные технические ресурсы и пришли к выводу, что (а) мы готовы эту функциональность предоставлять, (б) пользователям она необходима и (в) мы способны извлечь для себя какую-то выгоду из предоставления такого API (подробнее обо всем этом я расскажу в третьем разделе). Тем самым мы обозначили пункт "что" - какую функциональность мы физически можем предоставить и зачем.

- -

Следующий этап - описание пользовательских сценариев. Ответив на вопросы "что" и "зачем", нужно теперь описать ответ на вопрос "как" - как потребители API будут им пользоваться.

- -

Очевидное решение, которое приходит в голову сразу - это выполнить наше API в виде веб-сервиса: разработчик обращается по сформированному определённым образом url и получает погодную информацию. В общем-то, всю имеющуюся у нас информацию можно через такой сервис предоставлять; он будет универсален - такое обращение можно реализовать на любой платформе, и, таким образом, оно покроет все кейсы. Разве нет?

- -

Теперь давайте подойдём к вопросу с другой стороны. А как было бы удобно пользоваться нашим API, скажем, веб-мастеру-администратору небольшого локального форума? Скорее всего, ему требуется решение, которое не требует навыков разработки, чтобы было достаточно мышкой нащёлкать параметры информатора о прогнозе погоды. Нет, мы можем, конечно презрительно отмахнуться от таких непрофессионалов; но, тем самым, мы лишим сами себя значительной доли рынка.

- -

Аналогично, если мы посмотрим на потребности крупных диспетчерских компаний, то мы, вероятно, выясним, что у них уже есть какое-то автоматизированной рабочее место диспетчера, и идеальным для них решением был бы набор компонентов, которые можно интегрировать в их программный комплекс. Аналогично, у новостных ресурсов также есть какое-то готовое решение для управления контентом, и они предпочтут не заказывать дорогостоящую разработку модуля визуализации погоды, а готовое решение.

- -

Иными словами, в начале разработки API перед нами всегда стоит вопрос: насколько приближено оно должно быть к конечному пользователю? Чем более высок уровень вашего API, чем проще оно позволяет реализовать пользовательские сценарии - тем более интересно оно будет разработчикам, но тем дороже оно обойдётся вам самим. Понятно, что невозможно написать модули и компоненты для всех существующих cms и фреймворков, и вам придётся где-то остановиться - исходя из ваших представлений о массовости тех или иных кейсов. Вполне возможно, что в нашей гипотетической ситуации мы в итоге можем прийти к решению делать только http API, так как выясним, что получим от виджетов меньше дохода, чем потратим на разработку, а у каждого из крупных клиентов есть свой отдел разработки и они не доверяют в business-critical задачах разработкам сторонних компаний.

- -

Однако, чтобы нам было, о чем говорить в дальнейших разделах, предположим, что мы решили закрывать следующие сценарии:

- - - -

Перейдём теперь непосредственно к проектированию API.

- -

Уровни абстракции

- -

"Разделите свой код на уровни абстракции" - пожалуй, самый общий совет для разработчиков программного обеспечения. Что под этим обычно подразумевается?

- -

Вспомним, что программный продукт - это средство связи контекстов, средство преобразования терминов и операций одной предметной области в другую. Чем дальше друг от друга эти области отстоят - тем большее число промежуточных контекстов можно ввести. Например, модель OSI, которую часто приводят как эталон разделения уровней абстракции, насчитывает семь промежуточных этапов по дороге от аппаратного обеспечения к протоколам уровня приложений.

- -

Если говорить о разделении уровней абстракции именно с точки зрения API, то оно очень желательно по нескольким причинам:

- - -

И главное:

- - -

Если мы вернёмся к нашему примеру с погодным API, то увидим, что один уровень абстракции выделился автоматически: http API. Мы можем построить прочие виды API поверх базового http. Можем ли мы выделить ещё какие-то уровни, скажем, внутри самого http API?

- -

Построение иерархии абстракций

- -

Как обычно, мы начнём с вопроса "зачем" - если у нас не получается строгая иерархия, зачем нужна нестрогая? В первую очередь для того, чтобы вашим разработчикам было удобнее пользоваться вашим API. Представим, что мы спроектировали вот такой интерфейс для нашего http API:

- - - -

Покрывает ли такой интерфейс наши сценарии использования? С одной стороны кажется, что да - каждый потребитель сможет получить нужный ему набор информации.

- -

Но, с другой стороны, будет ли удобно вебмастеру, который хочет просто установить на свой сайт "прогноз погоды", пользоваться таким API? Понятно, что ему не нужны по отдельности ни температура, ни давление - ему нужна ровно та информация, которую захочет увидеть пользователь его ресурса.

- -

Оптимально было бы ввести ещё один метод:

- - -

Этот новый ресурс позволит пользователям API оперировать не в метеорологических терминах - а в терминах задачи, которую они решают. Тем самым мы спроектировали интерфейс высокого уровня, опирающийся на интерфейсы нижнего уровня абстракции.

- -

Аналогично, если мы будем проектировать API для новостных ресурсов, то придём к необходимости выделения сущности "картина атмосферных фронтов"; при проектировании API для диспетчерских служб - к сущности "план опасных для полета зон", и так далее.

- -

Разделение уровней абстракции должно происходить вдоль трёх направлений:

- - -

Чем дальше находятся друг от друга программные контексты, которые соединяет наше API - тем более глубокая иерархия сущностей должна получиться у нас в итоге. Обращаю ваше внимание, что иерархия не должна быть самоцелью; цель - введение промежуточных ступеней погружения в тонкости предметной области. Избыточная сложность здесь не менее вредна, чем избыточная простота.

- -

Уровни абстракции и декомпозиция

- -

Приближённость к сценариям использования - не единственная польза правильно построенной иерархии абстракций. Другим критерием качества архитектуры API здесь является снижение связности и декомпозиция объектов API.

- -

Рассмотрим следующий пример организации функциональности карты атмосферных фронтов.

- -

Первый способ:

- - - -

Порядок работы, вроде бы, ясен: разработчик получает карту и отрисовывает её, опираясь, если это необходимо, на нужные метаданные.

- -

Однако, очевидно, с точки зрения иерархии здесь допущена ошибка: высокоуровневые сущности (карта, атмосферные фронты и области повышенного и пониженного давления) связаны посредством технической сущности - визуальной геометрии объекта, которая по всем критериям должна находиться где-то в самом низу иерархии. Чем это может быть плохо для нас?

- -

Предположим, завтра у нас появится клиент, которого не устраивает векторное описание геометрии, и которому нужна растровая картинка. Мы окажемся в сложном положении: либо мы создаем параллельное API именно для работы с растром, либо клиенту придётся забирать и растровую картинку, и векторную - вторую только для того, чтобы через нее получить идентификаторы погодных объектов. Если бы иерархия была выстроена правильно (карта погоды отдает идентификаторы объектов, а уже по идентификаторам можно получить геометрии) - достаточно было бы добавить режим "растровое изображение".

- -

Аналогичные проблемы нас ждут, если в какой-то момент появятся объекты, содержащие более одной геометрии - либо, наоборот, несколько объектов научатся делить одну геометрию. В правильной архитектуре мы просто добавим новые типы ресурсов; в неправильной нам вообще не удастся решить эту ситуацию без слома обратной совместимости.

- -

Оба этих примера эксплуатируют одну и ТЦ же мысль: в иерархии уровней абстракции верхние уровни являются, как правило, более устойчивыми к разного рода изменениям. В самом деле, если объекты верхнего уровня описывают сценарии использования - они гораздо менее вероятно изменятся с развитием технологии. Потребность "мне нужно узнать прогноз погоды на завтра" никак не меняется уже много тысяч лет.

- -

Чем универсальнее ваше API в смысле широты тех контекстов, которые вы связываете, тем больше альтернативных реализаций одного и того функционала вам придётся реализовать (см. Пример с растровыми и векторными изображениями) и тем "шире" окажется ваша иерархия уровней абстракции: у вас появится множество объектов, принадлежащих одному уровню; в достаточно большом API неизбежно возникнут поддеревья иерархии - каждая реализация будет опираться на свой, полностью или частично, независимый слой логики.

- -

Изоляция уровней абстракции

- -

Для эффективной работы с такой сложной и разветвлённой иерархией необходимо добиваться изоляции её уровней, как вертикальной, так и горизонтальной. Попробуем разобраться на примере.

- -

Допустим, мы разрабатываем набор компонент для диспетчерских служб. У нас есть задача отражать на карте погоды движение транспортных средств клиента, и мы спроектировали примерно такие интерфейсы:

- - - -

Далее у нас возникает задача отображать перемещение транспортных средств по карте. Так эти данные поступают от клиента, нам необходимо предоставить внешний интерфейс для этого. Как это сделать? Мы примерно представляем, что у клиента будет реализован какой-то поток уведомлений о смене местоположения его транспортных объектов, и мы предоставим ему специальный объект source - сущность, чьей ответственностью является обновление информации об объектах.

- -

Возникает вопрос, каким образом мы свяжем source — объект, хранящий знание о положениях объектов — и overlay — объект непосредственно это знание реализующий. Наивное решение выглядит примерно так:

- -source.addOverlay(overlay) - -

При вызове этого метода source сохранит ссылку на overlay; при поступлении свежих данных опросит все оверлеи, выяснит идентификатор их родительского vehicle, и, если для данного vehicle пришла обновленная позиция, установит её исходному оверлею.

- -

Хотя на бумаге всё выглядит гладко, нарушение принципов построения уровней абстракции очевидно:

- - - -

Чем же нам грозит подобный неправильный интерфейс? Давайте разберёмся.

- -

Мы заставляем высокоуровневый объект преобразовывать геокоординаты в пиксельные. Очевидно, для этого source должен будет обратиться в map и выяснить множество информации, совершенно source ненужной - проекцию и масштаб карты, например. Таким образом, в нашей схеме source обладает знанием о всех прочих объектах нашего API - map, vehicle, overlay.

- -

Чем плоха такая сильная связность? Перечислим основные неприятности, которые поджидают нас на этом пути.

- -

Во-первых, мы задумали source как объект доступа к пользовательским данным. Так как у каждого пользователя обязательно будет своя специфика доступа к данным, то ситуации, когда пользователю нужно будет написать полностью или частично собственную реализацию source, будут возникать регулярно. Прикиньте теперь объём кода, который для этого потребуется: вместо того, чтобы написать адаптер для выдачи пар идентификатор-координаты, пользователю нужно будет реализовать взаимодействие с каждым компонентом API!

- -

В той же ситуации окажемся и мы, если решим сами реализовать какие-то типовые варианты source. В лучшем случае нам придется заняться редактором для выделения общих компонент по взаимодействию с другими сущностями, в худшем - пользоваться методом copy-paste.

- -

Хуже всего здесь то, операция преобразования координат - логически сложная вещь, вряд ли разработчик будет доставать учебник по сферической тригонометрии и, скорее всего, просто найдёт где-нибудь готовый кусок кода. Ошибки в этом коде, которые там неизбежно есть, будут приводить к трудно отлаживаемым артефактам, опять же в силу слабого знакомства разработчика с кодом.

- -

Во-вторых, любые изменения в интерфейсах любого компонента приведут нас к необходимости переписать и source тоже. Если, скажем, мы реализуем для карты возможность вращаться - нам придётся переписать и source, ведь преобразование геокоординат в пиксели сцены является его работой.

- -

В-третьих, любая ошибка в реализации любой из компонент грозит транзитом через source затронуть работу других, напрямую несвязанных компонентов системы. Например, если где-то в получении параметров карты есть ошибка, то при пересчёте координат оверлеев произойдёт исключение, и обновление координат vehicle не произойдёт или произойдёт частично - несмотря на то, что эти операции логически никак не связаны. Таким образом, отказоустойчивость системы и её плавная деградация при отказе одного из компонентов существенно снижается.

- -

В-четвёртых, тестировать такой код будет существенно сложнее - и нам при его имплементации, и сторонним разработчикам при написании своих компонент, поскольку гораздо сложнее тестировать и сам source (много разнородной функциональности и множество зависимостей, которые придётся подменять "заглушками"-mock-ами), и связанные с ним объекты (поскольку написать mock на source тоже нетривиально).

- -

Если суммировать вышесказанное, то можно выделить основные проблемы, которые несёт за собой неправильное выделение уровней абстракции:

- - - -

В следующем разделе мы попробуем спроектировать конкретные интерфейсы так, чтобы избежать перечисленных проблем.

- -

Разграничение областей ответственности

- -

Исходя из описанного в предыдущей главе, мы понимаем, что иерархия абстракций в нашем гипотетическом проекте должна выглядеть примерно так:

- - - -

Каким же образом нам нужно организовать связи между этими объектами так, чтобы, с одной стороны, позволить нашему API быть максимально гибким и одновременно избежать проблем сильной связанности объектов?

- -

Для этого необходимо, в первую очередь, определить ответственность каждой сущности: в чём смысл её существования в рамках нашего API, какие действия объект должен уметь выполнять сам а какие - делегировать другим объектам. Фактически, нам нужно применить "зачем-принцип" к каждой отдельной сущности нашего API.

- -

Из предыдущей главы мы выяснили, например, что ответственностью source является возврат пар идентификатор-координаты, и ничего более. Попробуем воспроизвести подобные рассуждения в отношении других объектов.

- -

Для этого нам нужно пройти по нашему API и сформулировать в терминах предметной области, что представляет из себя каждый объект. Напомню, что из концепции уровней абстракции следует, что каждый уровень иерархии - это некоторая собственная промежуточная предметная область, ступенька, по которой мы переходим от описания задачи в терминах одного связываемого контекста ("карта погоды с движущимися по ней транспортными средствами") к описанию в терминах второго ("SVG-объект, заданный в вычисляемых извне пиксельных координатах").

- -

В этом смысле проще всего описать объект Vehicle - это программная сущность, представляющая собой абстракцию над объектом реального мира. Её задачей, очевидно, является описание характеристик реального объекта, которые нужны для решения поставленных задач. В нашем случае это идентификатор (или иной способ опознания объекта) и его географическое положение. Если в нашем API появятся какие-то расширенные сведения об реальном объекте - скажем, название или скорость, - очевидно, они будут привязаны к тому же объекту Vehicle.

- -

Кроме того, ещё одной задачей Vehicle является описание собственного представления в интерфейсе - скажем, параметров иконки, если наш Vehicle должен выглядеть для пользователя интерфейса как значок на карте.

- -

С оверлеем всё тоже вполне ясно - это некоторый графический примитив, который должен уметь интерпретировать опции Vehicle и отображать заданный значок. Кроме того, оверлей должен уметь отображаться в произвольных пиксельных координатах в контексте карты.

- -

Наконец, что такое "карта" в терминах решаемой задачи? Это схематическое изображение фрагмента земной поверхности, согласно запрошенным координатам и масштабу. В чём заключается ответственность карты в нашей иерархии? Очевидно, в предоставлении данных о наблюдаемой пользователем области (координаты и масштаб) и информации об изменениях этих параметров. Кроме того, ответственностью карты в том или ином виде является пересчёт координат (очевидно, карта "знает", в какой проекции она нарисована) и предоставление возможности отрисовать поверх себя графические фигуры (оверлеи).

- -

Если внимательно посмотреть на каждый объект, то мы увидим, что, в итоге, каждый объект оказался в смысле своей ответственности составным: Vehicle одновременно "знает" и про объект реального мира, и про его виртуальное отображение на карте; карта "знает" свою область картографирования - фактически, область на реальной Земле, которую схематически отображает, - и при этом должна предоставлять некоторый контекст для отображения виртуальных графических фигур, и так далее.

- -

Ничего удивительного в этом, конечно же, нет — поскольку API в целом связывает разные контексты, в нём всегда будут объекты, объединяющие термины разных предметных областей. Наша задача - декомпозировать объекты так, чтобы, с одной стороны, разработчикам было удобно и понятно пользоваться нашей иерархией абстракций, а нам, с другой стороны, было удобно такую архитектуру поддерживать.

- -

Декомпозиция интерфейсов

- -

Если каждый объект представляет собой объединение разнородной ответственности в терминах разных предметных областей, каким образом мы можем добиться эффективного уменьшения связанности объектов между собой? Давайте подумаем, где мы можем "сэкономить" на связях.

- -

Возьмём, например, объект Map. Во взаимодействии с объектом source он выступает как чистый источник географических координат, вся прочая ответственность объекта карты source не касается.

- -

Напротив, оверлею географические координаты ни к чему: он существует в некотором графическом контексте, где оперируют пикселями.

- -

Раз смежным объектам знание о полной функциональности карты не нужно, именно здесь мы и можем убрать лишние связи: потребуем, чтобы карта при взаимодействии с источником данных выступала только как картографический контекст, а при взаимодействии с оверлеем - как чисто графический контекст. Для организации такой абстракции используются интерфейсы.

- -

Определим два интерфейса:

- - - -

Объект Map в этом случае реализует оба интерфейса, однако связанные сущности работают уже не с конкретным объектом Map, а с некоторой реализацией абстрактного интерфейса, ничего не зная о прочих свойствах этого объекта.

- -

NB. Во многих языках программирования нет поддержки интерфейсов, абстрактных классов и/или множественного наследования. Однако выделять интерфейсы нам это не мешает, поскольку мы всегда можем "договориться", что объект source имеет право пользоваться только вот этим набором свойств и методов. Конечно, контролировать соблюдение этой договоренности в достаточно развесистом API довольно сложно, но, поверьте автору, вполне возможно, тем более, что тестами и/или статическим анализом кода соблюдение договоренностей об интерфейсах можно проверить почти всегда.

- -

Разделение контекстов - не единственная причина, по которой выделение интерфейсов критически важно при проектировании API. Предъявление к входящим параметрам требования только удовлетворять интерфейсу существенно упрощает создание альтернативных реализаций ваших объектов, в том числе в целях тестирования. Теперь чтобы протестировать объект source достаточно написать mock на IGeoContext, а не весь класс map целиком. Аналогично, если мы захотим использовать наши оверлеи для показа их, скажем, в качестве какой-то инфографики или на абстрактном плане местности, нам не придётся переделывать для этого класс Map - достаточно будет альтернативной реализации IGraphicalContext.

- -

При выделении интерфейсов важно также понимать, что интерфейс, в отличие от его реализации, должен быть минимально достаточным и не должен включать в себя вспомогательные методы. Например, если класс map имеет как метод для получения всей области картографирования в виде четырехугольника getBBox, так и методы получения каждого из углов по отдельности - getLeftBottom, getRightTop, например, — то интерфейс IGeoContext должен содержать что-то одно. Нет никакого смысла загромождать интерфейс альтернативными реализациями одной и той же функциональности — это затрудняет чтение и усложняет написание собственных реализаций. Если только нет каких-то показаний с точки зрения производительности, следует отдать предпочтение максимально общему методу - в нашем случае getBBox.

- -

Интерфейсы как универсальный паттерн

- -

Как мы убедились в предыдущей главе, выделение интерфейсов крайне важно с точки зрения удобства написания кода. Однако, интерфейсы играют и другую важную роль в проектировании: они позволяют уложить в голове архитектуру API целиком.

- -

Любой сколько-нибудь крупный API рано или поздно обрастает разнообразной номенклатурой методов, как в силу того, что в одном объекте «сходятся» несколько предметных областей, так и в силу появления со временем разнообразной вспомогательной и дополнительной функциональности. Особенно сложной номенклатура объектов и их методов становится в случае появления альтернативных реализаций одного и того же интерфейса.

- -

Человеческие возможности небезграничны: невозможно держать в голове всю номенклатуру объектов. Это осложняет и проектирование API, и рефакторинг, и просто решение возникающих задач по реализации той или иной бизнес-логики.

- -

Держать же в голове схему взаимодействия интерфейсов гораздо проще - как в силу исключения из рассмотрения разнообразных вспомогательных и специфических методов, так и в силу того, что интерфейсы позволяют отделить существенное (в чем смысл конкретной сущности) от несущественного (деталей реализации).

- -

Поскольку задача выделения интерфейсов есть задача удобного манипулирования сущностями в голове разработчика, мы рекомендуем при проектировании интерфейсов руководствоваться, прежде всего, здравым смыслом: интерфейсы должны быть ровно настолько сложны, насколько это удобно для человеческого восприятия (а лучше даже чуть проще). В простейших случаях это просто означает, что интерфейс должен содержать семь плюс-минуса два метода. Более сложные интерфейсы должны декомпозироваться в несколько простых.

- -

Это правило существенно важно не только при проектировании api - не забывайте, что ваши пользователи неизбежно столкнутся с той же проблемой - понять примерную архитектуру вашего api, запомнить, что с чем связано в вашей системе. Правильно выделенные интерфейсы помогут и здесь, причём сразу в двух смыслах - как непосредственно работающему с вашим кодом программисту, так и документатору, которому будет гораздо проще описать структуру вашего api, опираясь на дерево интерфейсов.

- -

С другой стороны надо понимать, что бесплатно ничего не бывает, и выделение интерфейсов - самая «небесплатная» часть процесса разработки API, поскольку в чистом виде приносится в жертву удобство разработки ради построения «правильной» архитектуры: разумеется, код писать куда проще, когда имеешь доступ ко всем объектам API со всей их богатой номенклатурой методов, нежели когда из каждого объекта доступны только пара непосредственно примыкающих интерфейсов, притом с максимально общими методами.

- -

Помимо прочего, это означает, что интерфейсы необходимо выделять там, где это актуально решаемой задаче - прежде всего, в точках будущего роста и там, где возможны альтернативные реализации. Чем проще API, тем меньше нужда в интерфейсах, и наоборот: сложное API требует интерфейсов практически всюду просто для того, чтобы ограничить разрастание излишне сильной связанности и при этом не сойти с ума.

- -

В пределе в сложном api должна сложиться ситуация, при которой все объекты взаимодействуют друг с другом только как интерфейсы — нет ни одной публичной сигнатуры, принимающей конкретный объект, а не его интерфейс. Разумеется, достичь такого уровня абстракции практически невозможно - почти в любой системе есть глобальные объекты, разнообразные технические сущности (имплементации стандартных структур данных, например); наконец, невозможно «спрятать» за интерфейсы системные объекты.

- -

Информационные контексты

- -

При выделении интерфейсов и вообще при проектировании api бывает полезно взглянуть на иерархию абстракций с другой точки зрения, а именно: каким образом информация протекает через нашу иерархию.

- -

Вспомним, что одним из критериев отделения уровней абстракции является переход от структур данных одной предметной области к структурам данных другой. В рамках нашего примера через иерархию наших объектов происходит трансляция данных реального мира - географическое положение и реальные свойства транспортного средства - через добавление высокоуровневых опций (вид иконки этого транспортного средства) в графические примитивы конкретной платформы (svg-оверлей, заданный в пиксельных координатах сцены).

- -

Мы уже отмечали, что одним из недостатков нашей первой "наивной" реализации api была необходимость объекту source "знать" о том, как осуществляется преобразование данных (геокоординат в пиксели). Если это правило обобщить, оно будет выглядеть следующим образом:

- - - -

Из этих правил явно следует, что в нашей системе source имеет право оперировать только географическими координатами, а оверлей - только пиксельными. Оперировать и теми, и другими имеет право только объект, реализующий оба интерфейса IGeoContext и IGraphicalContext, то есть Map. Аналогично, source имеет право оперировать только свойствами реальных транспортных средств (идентификационный номер), оверлей - только свойствами их графического представления (иконка, её размер), и только Vehicle может связать идентификатор объекта со свойствами его графического представления.

- -

Достаточно внимательный читатель в этом месте может заметить, что правило информационной иерархии всё равно нарушается: объект высшего уровня Map оперирует данными низшего уровня абстракции - пикселями сцены, и будет совершенно прав. Конечно, в нашем умозрительном примере это совершенно излишне, но в реальном "большом" api карту от этого знания необходимо избавить.

- -

Для этого необходимо ввести некоторую промежуточную сущность, которая, с одной стороны, будет следить за изменением области картографирования и предоставлять методы трансляции геокоординат в пиксели сцены; с другой - предоставлять графическим примитивам холст для рисования - родительский svg-элемент в нашем случае.

- -

Назовем такую сущностью, скажем, IRenderingEngine; в нашей иерархии это. Интерфейс займёт промежуточное положение между картой и графическими объектами. Тогда ответственностью карты как IGraphicalContext будет предоставление доступа к своему rendering engine; соответственно, оверлей будет работать уже не с IGraphicalContext, а именно с engine; связывание же оверлея с его графическим контекстом должен произвести тот, кто оверлей инстанцирует - в нашей системе это может быть либо vehicle, либо сам графический контекст, либо какая-то третья сущность, которую мы выделим специально для этого.

- -

Дерево информационных контекстов (какой объект обладает какой информацией, и кто является транслятором из одного контекста в другой), по сути, представляет собой "срез" нашего дерева иерархии интерфейсов; выделение такого среза позволяет проще и удобнее удерживать в голове всю архитектуру проекта.

- -

Связывание объектов

- -

Существует множество техник связывания и управления объектами; ряд паттернов проектирования - порождающие, поведенческие, а также MV*-техники посвящены именно этому. Однако, прежде чем говорить о конкретных паттернах, нужно, как и всюду, ответить на вопрос "зачем" - зачем с точки зрения разработки API нам нужно регламентировать связывание объектов? Чего мы хотим добиться?

- -

В разработке программного обеспечения в целом снижение связанности объектов необходимо прежде всего для уменьшения сайд-эффектов, когда изменения в одной чести кода могут затронуть работоспособность другого части кода, а также для унификации разработки.

- -

Разумеется, к API эти суждения также применимы с поправкой на то, что изменения происходят не только при рефакторинге кода; API так же должно быть устойчиво:

- - -

Необходимость слабой связанности объектов API также вытекает из требования дискретных интерфейсов, поскольку поддержание разумно минимальной номенклатуры свойств и методов требует снижения количества связей между объектами. Чем слабее и малочисленнее связи между различными частями API, тем проще заменить одну технологию другой, если возникнет такая необходимость.

- -

Проблема связывания объектов разбивается на две части:

- - -

Установление связей между объектами

- -

В нашем примере нам нужно установить связи между источниками данных - map и source - и объектами, эти данные представляющими - vehicle и overlay - при посредничестве промежуточных сущностей - renderingEngine. При этом вариантов, как же нам связать эти объекты друг с другом просматривается множество: фактически, связь должна быть - напрямую или опосредованно - между любой парой объектов.

- -

Можем, например, сделать вот так:

-