You've already forked The-API-Book
mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-06-30 22:43:38 +02:00
html → md
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
.vscode
|
469
API.ru.html
469
API.ru.html
@ -1,469 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html><head>
|
|
||||||
<meta charset="utf-8"/>
|
|
||||||
<title>API</title>
|
|
||||||
<link rel="stylesheet" href="style.css"/>
|
|
||||||
</head><body>
|
|
||||||
<header>
|
|
||||||
<h1>Сергей Константинов</h1>
|
|
||||||
<h1>API</h1>
|
|
||||||
</header>
|
|
||||||
<h2>Об авторе</h2>
|
|
||||||
<h2>Введение</h2>
|
|
||||||
|
|
||||||
<p>Прежде чем говорить о разработке API, необходимо для начала договориться о том, что же такое API. Энциклопедия скажет нам, что API — это программный интерфейс приложений. Это точное определение, но бессмысленное. Примерно как определение человека по Платону: «двуногое без перьев» — определение точное, но никоим образом не дающее нам представление о том, чем на самом деле человек примечателен. (Да и не очень-то и точное: Диоген Синопский как-то ощипал петуха и заявил, что это человек Платона; пришлось дополнить определение уточнением «с плоскими ногтями».)</p>
|
|
||||||
|
|
||||||
<p>Что же такое API по смыслу, а не по формальному определению?</p>
|
|
||||||
|
|
||||||
<p>Вероятно, вы сейчас читаете эту книгу посредством браузера. Чтобы браузер смог отобразить эту страничку, должны корректно отработать: разбор URL согласно спецификации; служба DNS; соединение по протоколу TLS; передача данных по протоколу HTTP; разбор HTML-документа; разбор CSS-документа; корректный рендеринг HTML+CSS.</p>
|
|
||||||
|
|
||||||
<p>Но это только верхушка айсберга. Для работы HTTP необходима корректная работа всего сетевого стека, который состоит из 4-5, а то и больше, протоколов разных уровней. Разбор HTML-документа производится согласно сотням различных спецификаций. Рендеринг документа обращается к нижележащему API операционной системы, а также напрямую к API видеокарты. И так далее, и тому подобное — вплоть до того, что наборы команд современных CISC-процессоров имплементируются поверх API микрокоманд.</p>
|
|
||||||
|
|
||||||
<p>Иными словами, десятки, если не сотни, различных API должны отработать корректно для выполнения простейших действий типа просмотра web-страницы; без надёжной работы каждого из них современные информационные технологии попросту не могли бы существовать.</p>
|
|
||||||
|
|
||||||
<p>API — <em>это обязательство</em>. Формальное обязательство связывать между собой различные программируемые контексты.</p>
|
|
||||||
|
|
||||||
<h3>Об относительности критериев качества</h3>
|
|
||||||
<p>Несмотря на всё написанное выше, я вовсе не призываю вас подходить к каждой задаче с дополнительными наборами требований. Вовсе даже наоборот: я скорее призываю эти требования сокращать.</p>
|
|
||||||
|
|
||||||
<p>У каждого инструмента, включая и API, есть своя область применимости, и это важно понимать. Допустим, вы пишете, скажем, скрипт импорта данных из Википедии для того, чтобы залить в базу данных вашего продукта какие-то тестовые данные, и вы собираетесь воспользоваться им один раз. В этом случае, очевидно, совершенно неважно, правильная ли у него архитектура и понятен ли его API. Нет, разумеется, вы можете спроектировать и реализовать этот скрипт «правильно», гибко и универсально, чтобы вы могли им гордиться. Но зачем?</p>
|
|
||||||
|
|
||||||
<p>Любая красота сложна. Правильная архитектура требует больших затрат времени и сил, часто — неоднократного полного переписывания базовых модулей. Правильное API требует времени и сил ещё больше.</p>
|
|
||||||
|
|
||||||
<p>Понимать, где ваши усилия полезнее и нужнее — пожалуй, не менее важно, чем их (усилий) приложение.</p>
|
|
||||||
|
|
||||||
<p>Эти вещи кажутся очевидными на простых примерах; со сложными же проектами, поверьте, то же самое. Достоинства и недостатки любой методологии, как правило, отлично известны, однако редко когда выбор конкретного фреймворка, паттерна или подхода опирается на анализ соответствия их достоинств и недостатков проекту.</p>
|
|
||||||
|
|
||||||
<p>Есть ещё один момент, который хочется подчеркнуть: инструменты должны соответствовать конкретным обстоятельствам — предметной области, планируемому жизненному циклу продукта и, что немаловажно, компетенциям команды. Во многих случаях неидеальный инструмент, с которым команда привыкла работать, предпочтительнее неидеального, который придётся осваивать по ходу.</p>
|
|
||||||
|
|
||||||
<h3>О структуре этой книги</h3>
|
|
||||||
|
|
||||||
<p>Книга, которую вы держите в руках, состоит из трех больших разделов.</p>
|
|
||||||
|
|
||||||
<p>В первом разделе мы поговорим о проектировании API на стадии разработки концепции - как грамотно выстроить архитектуру, от крупноблочного планирования до конечных интерфейсов.</p>
|
|
||||||
|
|
||||||
<p>Второй раздел будет посвящён жизненному циклу API - как интерфейсы эволюционируют со временем и как развивать продукт так, чтобы отвечать потребностям пользователей.</p>
|
|
||||||
|
|
||||||
<p>Наконец, третий раздел будет касаться больше не-разработческих сторон жизни API - поддержки, маркетинга, достижения контакта с аудиторией.</p>
|
|
||||||
|
|
||||||
<p>Первые два будут интересны скорее разработчикам, третий — и разработчикам, и менеджерам. При этом я настаиваю, что как раз третий раздел — самый важный для разработчика API. Ввиду того, что API - продукт для разработчиков, перекладывать ответственность за его развитие и поддержку на не-разработчиков неправильно: никто кроме вас самих не понимает так хорошо продуктовые свойства вашего API.</p>
|
|
||||||
|
|
||||||
<h2>Проектирование API</h2>
|
|
||||||
|
|
||||||
<p>Подход, который мы используем для проектирования, состоит из четырёх шагов:</p>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>определение области применения;</li>
|
|
||||||
<li>разделение уровней абстракции;</li>
|
|
||||||
<li>разграничение областей ответственности;</li>
|
|
||||||
<li>описание конечных интерфейсов.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>Этот алгоритм строит API сверху вниз, от общих требований и сценариев использования до конкретной номенклатуры сущностей; фактически, двигаясь этим путем, вы получите на выходе готовое API - за что мы и ценим этот подход.</p>
|
|
||||||
|
|
||||||
<p>Поскольку я являюсь убеждённым приверженцем практического подхода, здесь и далее я буду рассматривать проектирование API на конкретных примерах. В качестве такого примера я выбрал гипотетическое API некоей метеорологической службы. На всякий случай, предупрежу заранее, что пример является выдуманным от начала до конца; если бы мне предложили заниматься таким API в реальности - я бы прежде всего потратил время на детальное исследование предметной области, и наверняка пришёл бы совсем к другим решениям в итоге. Но для наших учебных целей такой умозрительный пример вполне допустим, поскольку я ставлю своей целью, прежде всего, объяснение того, как надо думать и не стремлюсь вложить в голову читателя какие-то конкретные решения и рецепты успеха.</p>
|
|
||||||
|
|
||||||
<h3>Определение области применения</h3>
|
|
||||||
|
|
||||||
<p>Первый вопрос, на который мы должны ответить, принимая решение о разработке API - "зачем?" Кто и как будет пользоваться нашей программной связью контекстов?</p>
|
|
||||||
|
|
||||||
<p>Рассмотрим следующий пример. Допустим, мы умеем предсказывать погоду и располагаем возможностью предоставлять данные о погоде в машиночитаемом виде. Стоит ли нам разрабатывать API? Давайте подумаем.</p>
|
|
||||||
|
|
||||||
<p>Во-первых, необходимо понять, какую такую функциональность мы можем предоставлять потребителям, что они воспользуются API, а не будут разрабатывать сами?</p>
|
|
||||||
|
|
||||||
<p>По возрастанию сложности самостоятельной реализации такого функционала это будут: получение информации о текущей погоде; получение информации о прогнозе; получение информации о климате какой-то местности; получение информации о "внутреннем устройстве" предметной области - зоны высокого и низкого давления, атмосферные фронты и их движение. </p>
|
|
||||||
|
|
||||||
<p>Зачем такого рода функциональность может понадобиться? Вот несколько очевидных ответов:</o>
|
|
||||||
|
|
||||||
<ul><li>конечным пользователям - чтобы понять, нужно ли сегодня брать зонтик и куда можно съездить отдохнуть в ноябре;</li>
|
|
||||||
<li>компаниям, бизнес которых зависит от погоды - например, авиаперевозчикам.</li></ul>
|
|
||||||
|
|
||||||
<p>Для удовлетворения первой группы потребностей потребители будут сами приходить на ресурсы, принадлежащие нашим потенциальным клиентам - популярные локальные веб-сайты (например, на городские порталы за краткосрочным прогнозом погоды или на туристические агрегаторы за информацией о климате) или разного рода приложения для "умных" устройств (телефонов, часов, планшетов, телевизоров и даже домов) или специализированные новостные ресурсы. Вторую группу потребностей будут закрывать компании, производящие программное обеспечение для диспетчерских служб - либо, возможно, проприетарные решения самих транспортных компаний.</p>
|
|
||||||
|
|
||||||
<p>Допустим мы исследовали и рынок, и собственные технические ресурсы и пришли к выводу, что (а) мы готовы эту функциональность предоставлять, (б) пользователям она необходима и (в) мы способны извлечь для себя какую-то выгоду из предоставления такого API (подробнее обо всем этом я расскажу в третьем разделе). Тем самым мы обозначили пункт "что" - какую функциональность мы физически можем предоставить и зачем.</p>
|
|
||||||
|
|
||||||
<p>Следующий этап - описание пользовательских сценариев. Ответив на вопросы "что" и "зачем", нужно теперь описать ответ на вопрос "как" - как потребители API будут им пользоваться.</p>
|
|
||||||
|
|
||||||
<p>Очевидное решение, которое приходит в голову сразу - это выполнить наше API в виде веб-сервиса: разработчик обращается по сформированному определённым образом url и получает погодную информацию. В общем-то, всю имеющуюся у нас информацию можно через такой сервис предоставлять; он будет универсален - такое обращение можно реализовать на любой платформе, и, таким образом, оно покроет все кейсы. Разве нет?</p>
|
|
||||||
|
|
||||||
<p>Теперь давайте подойдём к вопросу с другой стороны. А как было бы удобно пользоваться нашим API, скажем, веб-мастеру-администратору небольшого локального форума? Скорее всего, ему требуется решение, которое не требует навыков разработки, чтобы было достаточно мышкой нащёлкать параметры информатора о прогнозе погоды. Нет, мы можем, конечно презрительно отмахнуться от таких непрофессионалов; но, тем самым, мы лишим сами себя значительной доли рынка.</p>
|
|
||||||
|
|
||||||
<p>Аналогично, если мы посмотрим на потребности крупных диспетчерских компаний, то мы, вероятно, выясним, что у них уже есть какое-то автоматизированной рабочее место диспетчера, и идеальным для них решением был бы набор компонентов, которые можно интегрировать в их программный комплекс. Аналогично, у новостных ресурсов также есть какое-то готовое решение для управления контентом, и они предпочтут не заказывать дорогостоящую разработку модуля визуализации погоды, а готовое решение.</p>
|
|
||||||
|
|
||||||
<p>Иными словами, в начале разработки API перед нами всегда стоит вопрос: насколько приближено оно должно быть к конечному пользователю? Чем более высок уровень вашего API, чем проще оно позволяет реализовать пользовательские сценарии - тем более интересно оно будет разработчикам, но тем дороже оно обойдётся вам самим. Понятно, что невозможно написать модули и компоненты для всех существующих cms и фреймворков, и вам придётся где-то остановиться - исходя из ваших представлений о массовости тех или иных кейсов. Вполне возможно, что в нашей гипотетической ситуации мы в итоге можем прийти к решению делать только http API, так как выясним, что получим от виджетов меньше дохода, чем потратим на разработку, а у каждого из крупных клиентов есть свой отдел разработки и они не доверяют в business-critical задачах разработкам сторонних компаний.</p>
|
|
||||||
|
|
||||||
<p>Однако, чтобы нам было, о чем говорить в дальнейших разделах, предположим, что мы решили закрывать следующие сценарии:</p>
|
|
||||||
|
|
||||||
<ul><li>общее http API для клиентов, готовых вести разработку самостоятельно;</li>
|
|
||||||
<li>набор виджетов для мобильных и веб-приложений;</li>
|
|
||||||
<li>набор компонентов для встраивания в АРМы диспетчерских компаний.</li></ul>
|
|
||||||
|
|
||||||
<p>Перейдём теперь непосредственно к проектированию API.</p>
|
|
||||||
|
|
||||||
<h3>Уровни абстракции</h3>
|
|
||||||
|
|
||||||
<p>"Разделите свой код на уровни абстракции" - пожалуй, самый общий совет для разработчиков программного обеспечения. Что под этим обычно подразумевается?</p>
|
|
||||||
|
|
||||||
<p>Вспомним, что программный продукт - это средство связи контекстов, средство преобразования терминов и операций одной предметной области в другую. Чем дальше друг от друга эти области отстоят - тем большее число промежуточных контекстов можно ввести. Например, модель OSI, которую часто приводят как эталон разделения уровней абстракции, насчитывает семь промежуточных этапов по дороге от аппаратного обеспечения к протоколам уровня приложений.</p>
|
|
||||||
|
|
||||||
<p>Если говорить о разделении уровней абстракции именно с точки зрения API, то оно очень желательно по нескольким причинам:</p>
|
|
||||||
|
|
||||||
<ul><li>Разделение проекта на несколько независимых частей; архитектура каждой из них, таким образом, становится проще, и упрощается интеграция;</li>
|
|
||||||
<li>с помощью такого разделения гораздо легче добиваться кроссплатформенности путём отделения платформо-зависимой логики в отдельный уровень (или уровни) абстракции.</li></ul>
|
|
||||||
<p>И главное:</p>
|
|
||||||
<ul><li>Упрощается задача для ваших клиентов; правильно разделённые уровни абстракции означают, что разработчикам не придется разбираться со всей номенклатурой сущностей вашего API - им достаточно будет работать только с объектами высокого уровня, отвечающими непосредственно за решение их задач.</li></ul>
|
|
||||||
|
|
||||||
<p>Если мы вернёмся к нашему примеру с погодным API, то увидим, что один уровень абстракции выделился автоматически: http API. Мы можем построить прочие виды API поверх базового http. Можем ли мы выделить ещё какие-то уровни, скажем, внутри самого http API?</p>
|
|
||||||
|
|
||||||
<h4>Построение иерархии абстракций</h4>
|
|
||||||
|
|
||||||
<p>Как обычно, мы начнём с вопроса "зачем" - если у нас не получается строгая иерархия, зачем нужна нестрогая? В первую очередь для того, чтобы вашим разработчикам было удобнее пользоваться вашим API. Представим, что мы спроектировали вот такой интерфейс для нашего http API:</p>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>GET /weather/temperature/{city_id} - возвращает текущую температуру в городе с указанным идентификатором;</li>
|
|
||||||
<li>GET /weather/temperature/{city_id}/forecast - возвращает прогноз изменения температуры;</li>
|
|
||||||
<li>GET /weather/pressure/{city_id} - возвращает атмосферное давление.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>Покрывает ли такой интерфейс наши сценарии использования? С одной стороны кажется, что да - каждый потребитель сможет получить нужный ему набор информации.</p>
|
|
||||||
|
|
||||||
<p>Но, с другой стороны, будет ли удобно вебмастеру, который хочет просто установить на свой сайт "прогноз погоды", пользоваться таким API? Понятно, что ему не нужны по отдельности ни температура, ни давление - ему нужна ровно та информация, которую захочет увидеть пользователь его ресурса.</p>
|
|
||||||
|
|
||||||
<p>Оптимально было бы ввести ещё один метод:</p>
|
|
||||||
<ul><li>GET /weather/{city_id} - возвращает параметры погоды в указанном городе: температуру, давление, прогноз.</li></ul>
|
|
||||||
|
|
||||||
<p>Этот новый ресурс позволит пользователям API оперировать не в метеорологических терминах - а в терминах задачи, которую они решают. Тем самым мы спроектировали интерфейс высокого уровня, опирающийся на интерфейсы нижнего уровня абстракции.</p>
|
|
||||||
|
|
||||||
<p>Аналогично, если мы будем проектировать API для новостных ресурсов, то придём к необходимости выделения сущности "картина атмосферных фронтов"; при проектировании API для диспетчерских служб - к сущности "план опасных для полета зон", и так далее.</p>
|
|
||||||
|
|
||||||
<p>Разделение уровней абстракции должно происходить вдоль трёх направлений:</p>
|
|
||||||
<ul><li>от сценариев использования к их внутренней реализации: высокоуровневые сущности и номенклатура их методов должны напрямую отражать сценарии использования API; низкоуровневый - отражать декомпозицию сценариев на составные части;</li>
|
|
||||||
<li>от терминов предметной области пользователя к терминам предметной области исходных данных - в нашем случае от высокоуровневых понятий "прогноз погоды" и "движение атмосферных фронтов" к базовым "температура", "давление", "скорость ветра";</li>
|
|
||||||
<li>наконец, от структур данных, в которых удобно оперировать пользователю к структурам данных, максимально приближенных к "сырым" - в нашем случае от "атмосферных фронтов" и "магнитных бурь" - к сырым байтовый данным, описывающим поле атмосферного давления или магнитное поле Земли.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>Чем дальше находятся друг от друга программные контексты, которые соединяет наше API - тем более глубокая иерархия сущностей должна получиться у нас в итоге. Обращаю ваше внимание, что иерархия не должна быть самоцелью; цель - введение промежуточных ступеней погружения в тонкости предметной области. Избыточная сложность здесь не менее вредна, чем избыточная простота.</p>
|
|
||||||
|
|
||||||
<h3>Уровни абстракции и декомпозиция</h3>
|
|
||||||
|
|
||||||
<p></p>Приближённость к сценариям использования - не единственная польза правильно построенной иерархии абстракций. Другим критерием качества архитектуры API здесь является снижение связности и декомпозиция объектов API.</p>
|
|
||||||
|
|
||||||
<p>Рассмотрим следующий пример организации функциональности карты атмосферных фронтов.</p>
|
|
||||||
|
|
||||||
<p>Первый способ:</p>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>GET /map - возвращает множество геометрий атмосферных объектов, попадающих в заданную область;</li>
|
|
||||||
<li>GET /geometry/{geometry_id} - возвращает метаданные о геометрии, в том числе тип объекта, описываемого геометрией (область повышенного или пониженного давления, атмосферный фронт);</li>
|
|
||||||
<li>GET /area/{area_id}, GET /front/{front_id} - возвращает метаданные конечного объекта.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>Порядок работы, вроде бы, ясен: разработчик получает карту и отрисовывает её, опираясь, если это необходимо, на нужные метаданные.</p>
|
|
||||||
|
|
||||||
<p>Однако, очевидно, с точки зрения иерархии здесь допущена ошибка: высокоуровневые сущности (карта, атмосферные фронты и области повышенного и пониженного давления) связаны посредством технической сущности - визуальной геометрии объекта, которая по всем критериям должна находиться где-то в самом низу иерархии. Чем это может быть плохо для нас?</p>
|
|
||||||
|
|
||||||
<p>Предположим, завтра у нас появится клиент, которого не устраивает векторное описание геометрии, и которому нужна растровая картинка. Мы окажемся в сложном положении: либо мы создаем параллельное API именно для работы с растром, либо клиенту придётся забирать и растровую картинку, и векторную - вторую только для того, чтобы через нее получить идентификаторы погодных объектов. Если бы иерархия была выстроена правильно (карта погоды отдает идентификаторы объектов, а уже по идентификаторам можно получить геометрии) - достаточно было бы добавить режим "растровое изображение".</p>
|
|
||||||
|
|
||||||
<p>Аналогичные проблемы нас ждут, если в какой-то момент появятся объекты, содержащие более одной геометрии - либо, наоборот, несколько объектов научатся делить одну геометрию. В правильной архитектуре мы просто добавим новые типы ресурсов; в неправильной нам вообще не удастся решить эту ситуацию без слома обратной совместимости.</p>
|
|
||||||
|
|
||||||
<p>Оба этих примера эксплуатируют одну и ТЦ же мысль: в иерархии уровней абстракции верхние уровни являются, как правило, более устойчивыми к разного рода изменениям. В самом деле, если объекты верхнего уровня описывают сценарии использования - они гораздо менее вероятно изменятся с развитием технологии. Потребность "мне нужно узнать прогноз погоды на завтра" никак не меняется уже много тысяч лет.</p>
|
|
||||||
|
|
||||||
<p>Чем универсальнее ваше API в смысле широты тех контекстов, которые вы связываете, тем больше альтернативных реализаций одного и того функционала вам придётся реализовать (см. Пример с растровыми и векторными изображениями) и тем "шире" окажется ваша иерархия уровней абстракции: у вас появится множество объектов, принадлежащих одному уровню; в достаточно большом API неизбежно возникнут поддеревья иерархии - каждая реализация будет опираться на свой, полностью или частично, независимый слой логики.</p>
|
|
||||||
|
|
||||||
<h3>Изоляция уровней абстракции</h3>
|
|
||||||
|
|
||||||
<p>Для эффективной работы с такой сложной и разветвлённой иерархией необходимо добиваться изоляции её уровней, как вертикальной, так и горизонтальной. Попробуем разобраться на примере.</p>
|
|
||||||
|
|
||||||
<p>Допустим, мы разрабатываем набор компонент для диспетчерских служб. У нас есть задача отражать на карте погоды движение транспортных средств клиента, и мы спроектировали примерно такие интерфейсы:</p>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>Map - класс собственно карты;</li>
|
|
||||||
<li>Vehicle - класс-диспетчеризируемый объект;</li>
|
|
||||||
<li>Overlay - графическое представление объекта.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>Далее у нас возникает задача отображать перемещение транспортных средств по карте. Так эти данные поступают от клиента, нам необходимо предоставить внешний интерфейс для этого. Как это сделать? Мы примерно представляем, что у клиента будет реализован какой-то поток уведомлений о смене местоположения его транспортных объектов, и мы предоставим ему специальный объект source - сущность, чьей ответственностью является обновление информации об объектах.</p>
|
|
||||||
|
|
||||||
<p>Возникает вопрос, каким образом мы свяжем source — объект, хранящий знание о положениях объектов — и overlay — объект непосредственно это знание реализующий. Наивное решение выглядит примерно так:</p>
|
|
||||||
|
|
||||||
<code>source.addOverlay(overlay)</code>
|
|
||||||
|
|
||||||
<p>При вызове этого метода source сохранит ссылку на overlay; при поступлении свежих данных опросит все оверлеи, выяснит идентификатор их родительского vehicle, и, если для данного vehicle пришла обновленная позиция, установит её исходному оверлею.</p>
|
|
||||||
|
|
||||||
<p>Хотя на бумаге всё выглядит гладко, нарушение принципов построения уровней абстракции очевидно:</p>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>объект source, имплементирующий логику в терминах клиента, должен быть объектом высшего уровня в иерархии, наравне с Map;</li>
|
|
||||||
<li>соответственно, source не должен работать с объектами низшего уровня - оверлеями, - которые, к тому же, инстанцирует не он;</li>
|
|
||||||
<li>кроме того, знание о пиксельных координатах объекта является совершенно лишним для source - как объект высокого уровня он должен работать с информацией в терминах клиента - то есть с идентификаторами и географическими координатами.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>Чем же нам грозит подобный неправильный интерфейс? Давайте разберёмся.</p>
|
|
||||||
|
|
||||||
<p>Мы заставляем высокоуровневый объект преобразовывать геокоординаты в пиксельные. Очевидно, для этого source должен будет обратиться в map и выяснить множество информации, совершенно source ненужной - проекцию и масштаб карты, например. Таким образом, в нашей схеме source обладает знанием о всех прочих объектах нашего API - map, vehicle, overlay.</p>
|
|
||||||
|
|
||||||
<p>Чем плоха такая сильная связность? Перечислим основные неприятности, которые поджидают нас на этом пути.</p>
|
|
||||||
|
|
||||||
<p>Во-первых, мы задумали source как объект доступа к пользовательским данным. Так как у каждого пользователя обязательно будет своя специфика доступа к данным, то ситуации, когда пользователю нужно будет написать полностью или частично собственную реализацию source, будут возникать регулярно. Прикиньте теперь объём кода, который для этого потребуется: вместо того, чтобы написать адаптер для выдачи пар идентификатор-координаты, пользователю нужно будет реализовать взаимодействие с каждым компонентом API!</p>
|
|
||||||
|
|
||||||
<p>В той же ситуации окажемся и мы, если решим сами реализовать какие-то типовые варианты source. В лучшем случае нам придется заняться редактором для выделения общих компонент по взаимодействию с другими сущностями, в худшем - пользоваться методом copy-paste.</p>
|
|
||||||
|
|
||||||
<p>Хуже всего здесь то, операция преобразования координат - логически сложная вещь, вряд ли разработчик будет доставать учебник по сферической тригонометрии и, скорее всего, просто найдёт где-нибудь готовый кусок кода. Ошибки в этом коде, которые там неизбежно есть, будут приводить к трудно отлаживаемым артефактам, опять же в силу слабого знакомства разработчика с кодом.</p>
|
|
||||||
|
|
||||||
<p>Во-вторых, любые изменения в интерфейсах любого компонента приведут нас к необходимости переписать и source тоже. Если, скажем, мы реализуем для карты возможность вращаться - нам придётся переписать и source, ведь преобразование геокоординат в пиксели сцены является его работой.</p>
|
|
||||||
|
|
||||||
<p>В-третьих, любая ошибка в реализации любой из компонент грозит транзитом через source затронуть работу других, напрямую несвязанных компонентов системы. Например, если где-то в получении параметров карты есть ошибка, то при пересчёте координат оверлеев произойдёт исключение, и обновление координат vehicle не произойдёт или произойдёт частично - несмотря на то, что эти операции логически никак не связаны. Таким образом, отказоустойчивость системы и её плавная деградация при отказе одного из компонентов существенно снижается.</p>
|
|
||||||
|
|
||||||
<p>В-четвёртых, тестировать такой код будет существенно сложнее - и нам при его имплементации, и сторонним разработчикам при написании своих компонент, поскольку гораздо сложнее тестировать и сам source (много разнородной функциональности и множество зависимостей, которые придётся подменять "заглушками"-mock-ами), и связанные с ним объекты (поскольку написать mock на source тоже нетривиально).</p>
|
|
||||||
|
|
||||||
<p>Если суммировать вышесказанное, то можно выделить основные проблемы, которые несёт за собой неправильное выделение уровней абстракции:</p>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>чрезмерное усложнение реализации альтернативных вариантов чересчур сильно связанных сущностей;</li>
|
|
||||||
<li>распространение ошибок по связанным сущностям и общее снижение отказоустойчивости системы;</li>
|
|
||||||
<li>усложнение работы с системой для внешних разработчиков, необходимость обладания экспертизой в областях, вообще говоря, напрямую не связанных с решаемой задачей;</li>
|
|
||||||
<li>усложнение тестирования кода как самого API, так и написанного поверх него;</li>
|
|
||||||
<li>необходимость постоянных рефакторингов и копи-паста кода.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>В следующем разделе мы попробуем спроектировать конкретные интерфейсы так, чтобы избежать перечисленных проблем.</p>
|
|
||||||
|
|
||||||
<h2>Разграничение областей ответственности</h2>
|
|
||||||
|
|
||||||
<p>Исходя из описанного в предыдущей главе, мы понимаем, что иерархия абстракций в нашем гипотетическом проекте должна выглядеть примерно так:</p>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>Map, source - сущности верхнего уровня;</li>
|
|
||||||
<li>Vehicle - среднего;</li>
|
|
||||||
<li>Overlay - нижнего.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>Каким же образом нам нужно организовать связи между этими объектами так, чтобы, с одной стороны, позволить нашему API быть максимально гибким и одновременно избежать проблем сильной связанности объектов?</p>
|
|
||||||
|
|
||||||
<p>Для этого необходимо, в первую очередь, определить ответственность каждой сущности: в чём смысл её существования в рамках нашего API, какие действия объект должен уметь выполнять сам а какие - делегировать другим объектам. Фактически, нам нужно применить "зачем-принцип" к каждой отдельной сущности нашего API.</p>
|
|
||||||
|
|
||||||
<p>Из предыдущей главы мы выяснили, например, что ответственностью source является возврат пар идентификатор-координаты, и ничего более. Попробуем воспроизвести подобные рассуждения в отношении других объектов.</p>
|
|
||||||
|
|
||||||
<p>Для этого нам нужно пройти по нашему API и сформулировать в терминах предметной области, что представляет из себя каждый объект. Напомню, что из концепции уровней абстракции следует, что каждый уровень иерархии - это некоторая собственная промежуточная предметная область, ступенька, по которой мы переходим от описания задачи в терминах одного связываемого контекста ("карта погоды с движущимися по ней транспортными средствами") к описанию в терминах второго ("SVG-объект, заданный в вычисляемых извне пиксельных координатах").</p>
|
|
||||||
|
|
||||||
<p>В этом смысле проще всего описать объект Vehicle - это программная сущность, представляющая собой абстракцию над объектом реального мира. Её задачей, очевидно, является описание характеристик реального объекта, которые нужны для решения поставленных задач. В нашем случае это идентификатор (или иной способ опознания объекта) и его географическое положение. Если в нашем API появятся какие-то расширенные сведения об реальном объекте - скажем, название или скорость, - очевидно, они будут привязаны к тому же объекту Vehicle.</p>
|
|
||||||
|
|
||||||
<p>Кроме того, ещё одной задачей Vehicle является описание собственного представления в интерфейсе - скажем, параметров иконки, если наш Vehicle должен выглядеть для пользователя интерфейса как значок на карте.</p>
|
|
||||||
|
|
||||||
<p>С оверлеем всё тоже вполне ясно - это некоторый графический примитив, который должен уметь интерпретировать опции Vehicle и отображать заданный значок. Кроме того, оверлей должен уметь отображаться в произвольных пиксельных координатах в контексте карты.</p>
|
|
||||||
|
|
||||||
<p>Наконец, что такое "карта" в терминах решаемой задачи? Это схематическое изображение фрагмента земной поверхности, согласно запрошенным координатам и масштабу. В чём заключается ответственность карты в нашей иерархии? Очевидно, в предоставлении данных о наблюдаемой пользователем области (координаты и масштаб) и информации об изменениях этих параметров. Кроме того, ответственностью карты в том или ином виде является пересчёт координат (очевидно, карта "знает", в какой проекции она нарисована) и предоставление возможности отрисовать поверх себя графические фигуры (оверлеи).</p>
|
|
||||||
|
|
||||||
<p>Если внимательно посмотреть на каждый объект, то мы увидим, что, в итоге, каждый объект оказался в смысле своей ответственности составным: Vehicle одновременно "знает" и про объект реального мира, и про его виртуальное отображение на карте; карта "знает" свою область картографирования - фактически, область на реальной Земле, которую схематически отображает, - и при этом должна предоставлять некоторый контекст для отображения виртуальных графических фигур, и так далее.</p>
|
|
||||||
|
|
||||||
<p>Ничего удивительного в этом, конечно же, нет — поскольку API в целом связывает разные контексты, в нём всегда будут объекты, объединяющие термины разных предметных областей. Наша задача - декомпозировать объекты так, чтобы, с одной стороны, разработчикам было удобно и понятно пользоваться нашей иерархией абстракций, а нам, с другой стороны, было удобно такую архитектуру поддерживать.</p>
|
|
||||||
|
|
||||||
<h3>Декомпозиция интерфейсов</h3>
|
|
||||||
|
|
||||||
<p>Если каждый объект представляет собой объединение разнородной ответственности в терминах разных предметных областей, каким образом мы можем добиться эффективного уменьшения связанности объектов между собой? Давайте подумаем, где мы можем "сэкономить" на связях.</p>
|
|
||||||
|
|
||||||
<p>Возьмём, например, объект Map. Во взаимодействии с объектом source он выступает как чистый источник географических координат, вся прочая ответственность объекта карты source не касается.</p>
|
|
||||||
|
|
||||||
<p>Напротив, оверлею географические координаты ни к чему: он существует в некотором графическом контексте, где оперируют пикселями. </p>
|
|
||||||
|
|
||||||
<p>Раз смежным объектам знание о полной функциональности карты не нужно, именно здесь мы и можем убрать лишние связи: потребуем, чтобы карта при взаимодействии с источником данных выступала только как картографический контекст, а при взаимодействии с оверлеем - как чисто графический контекст. Для организации такой абстракции используются интерфейсы.</p>
|
|
||||||
|
|
||||||
<p>Определим два интерфейса:</p>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>IGeoContext - предоставляет методы работы с областью картографирования;</li>
|
|
||||||
<li>IGraphicalContext - предоставляет контекст рендеринга.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>Объект Map в этом случае реализует оба интерфейса, однако связанные сущности работают уже не с конкретным объектом Map, а с некоторой реализацией абстрактного интерфейса, ничего не зная о прочих свойствах этого объекта.</p>
|
|
||||||
|
|
||||||
<p>NB. Во многих языках программирования нет поддержки интерфейсов, абстрактных классов и/или множественного наследования. Однако выделять интерфейсы нам это не мешает, поскольку мы всегда можем "договориться", что объект source имеет право пользоваться только вот этим набором свойств и методов. Конечно, контролировать соблюдение этой договоренности в достаточно развесистом API довольно сложно, но, поверьте автору, вполне возможно, тем более, что тестами и/или статическим анализом кода соблюдение договоренностей об интерфейсах можно проверить почти всегда.</p>
|
|
||||||
|
|
||||||
<p>Разделение контекстов - не единственная причина, по которой выделение интерфейсов критически важно при проектировании API. Предъявление к входящим параметрам требования только удовлетворять интерфейсу существенно упрощает создание альтернативных реализаций ваших объектов, в том числе в целях тестирования. Теперь чтобы протестировать объект source достаточно написать mock на IGeoContext, а не весь класс map целиком. Аналогично, если мы захотим использовать наши оверлеи для показа их, скажем, в качестве какой-то инфографики или на абстрактном плане местности, нам не придётся переделывать для этого класс Map - достаточно будет альтернативной реализации IGraphicalContext.</p>
|
|
||||||
|
|
||||||
<p>При выделении интерфейсов важно также понимать, что интерфейс, в отличие от его реализации, должен быть минимально достаточным и не должен включать в себя вспомогательные методы. Например, если класс map имеет как метод для получения всей области картографирования в виде четырехугольника getBBox, так и методы получения каждого из углов по отдельности - getLeftBottom, getRightTop, например, — то интерфейс IGeoContext должен содержать что-то одно. Нет никакого смысла загромождать интерфейс альтернативными реализациями одной и той же функциональности — это затрудняет чтение и усложняет написание собственных реализаций. Если только нет каких-то показаний с точки зрения производительности, следует отдать предпочтение максимально общему методу - в нашем случае getBBox.</p>
|
|
||||||
|
|
||||||
<h3>Интерфейсы как универсальный паттерн</h3>
|
|
||||||
|
|
||||||
<p>Как мы убедились в предыдущей главе, выделение интерфейсов крайне важно с точки зрения удобства написания кода. Однако, интерфейсы играют и другую важную роль в проектировании: они позволяют уложить в голове архитектуру API целиком.</p>
|
|
||||||
|
|
||||||
<p>Любой сколько-нибудь крупный API рано или поздно обрастает разнообразной номенклатурой методов, как в силу того, что в одном объекте «сходятся» несколько предметных областей, так и в силу появления со временем разнообразной вспомогательной и дополнительной функциональности. Особенно сложной номенклатура объектов и их методов становится в случае появления альтернативных реализаций одного и того же интерфейса.</p>
|
|
||||||
|
|
||||||
<p>Человеческие возможности небезграничны: невозможно держать в голове всю номенклатуру объектов. Это осложняет и проектирование API, и рефакторинг, и просто решение возникающих задач по реализации той или иной бизнес-логики.</p>
|
|
||||||
|
|
||||||
<p>Держать же в голове схему взаимодействия интерфейсов гораздо проще - как в силу исключения из рассмотрения разнообразных вспомогательных и специфических методов, так и в силу того, что интерфейсы позволяют отделить существенное (в чем смысл конкретной сущности) от несущественного (деталей реализации).</p>
|
|
||||||
|
|
||||||
<p>Поскольку задача выделения интерфейсов есть задача удобного манипулирования сущностями в голове разработчика, мы рекомендуем при проектировании интерфейсов руководствоваться, прежде всего, здравым смыслом: интерфейсы должны быть ровно настолько сложны, насколько это удобно для человеческого восприятия (а лучше даже чуть проще). В простейших случаях это просто означает, что интерфейс должен содержать семь плюс-минуса два метода. Более сложные интерфейсы должны декомпозироваться в несколько простых.</p>
|
|
||||||
|
|
||||||
<p>Это правило существенно важно не только при проектировании api - не забывайте, что ваши пользователи неизбежно столкнутся с той же проблемой - понять примерную архитектуру вашего api, запомнить, что с чем связано в вашей системе. Правильно выделенные интерфейсы помогут и здесь, причём сразу в двух смыслах - как непосредственно работающему с вашим кодом программисту, так и документатору, которому будет гораздо проще описать структуру вашего api, опираясь на дерево интерфейсов.</p>
|
|
||||||
|
|
||||||
<p>С другой стороны надо понимать, что бесплатно ничего не бывает, и выделение интерфейсов - самая «небесплатная» часть процесса разработки API, поскольку в чистом виде приносится в жертву удобство разработки ради построения «правильной» архитектуры: разумеется, код писать куда проще, когда имеешь доступ ко всем объектам API со всей их богатой номенклатурой методов, нежели когда из каждого объекта доступны только пара непосредственно примыкающих интерфейсов, притом с максимально общими методами.</p>
|
|
||||||
|
|
||||||
<p>Помимо прочего, это означает, что интерфейсы необходимо выделять там, где это актуально решаемой задаче - прежде всего, в точках будущего роста и там, где возможны альтернативные реализации. Чем проще API, тем меньше нужда в интерфейсах, и наоборот: сложное API требует интерфейсов практически всюду просто для того, чтобы ограничить разрастание излишне сильной связанности и при этом не сойти с ума.</p>
|
|
||||||
|
|
||||||
<p>В пределе в сложном api должна сложиться ситуация, при которой все объекты взаимодействуют друг с другом только как интерфейсы — нет ни одной публичной сигнатуры, принимающей конкретный объект, а не его интерфейс. Разумеется, достичь такого уровня абстракции практически невозможно - почти в любой системе есть глобальные объекты, разнообразные технические сущности (имплементации стандартных структур данных, например); наконец, невозможно «спрятать» за интерфейсы системные объекты.</p>
|
|
||||||
|
|
||||||
<h3>Информационные контексты</h3>
|
|
||||||
|
|
||||||
<p>При выделении интерфейсов и вообще при проектировании api бывает полезно взглянуть на иерархию абстракций с другой точки зрения, а именно: каким образом информация протекает через нашу иерархию.</p>
|
|
||||||
|
|
||||||
<p>Вспомним, что одним из критериев отделения уровней абстракции является переход от структур данных одной предметной области к структурам данных другой. В рамках нашего примера через иерархию наших объектов происходит трансляция данных реального мира - географическое положение и реальные свойства транспортного средства - через добавление высокоуровневых опций (вид иконки этого транспортного средства) в графические примитивы конкретной платформы (svg-оверлей, заданный в пиксельных координатах сцены).</p>
|
|
||||||
|
|
||||||
<p>Мы уже отмечали, что одним из недостатков нашей первой "наивной" реализации api была необходимость объекту source "знать" о том, как осуществляется преобразование данных (геокоординат в пиксели). Если это правило обобщить, оно будет выглядеть следующим образом:</p>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>каждый объект в иерархии абстракций должен оперировать данными согласно своему уровню иерархии;</li>
|
|
||||||
<li>Преобразованием данных имеют право заниматься только те объекты, в чьи непосредственные обязанности это входит.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>Из этих правил явно следует, что в нашей системе source имеет право оперировать только географическими координатами, а оверлей - только пиксельными. Оперировать и теми, и другими имеет право только объект, реализующий оба интерфейса IGeoContext и IGraphicalContext, то есть Map. Аналогично, source имеет право оперировать только свойствами реальных транспортных средств (идентификационный номер), оверлей - только свойствами их графического представления (иконка, её размер), и только Vehicle может связать идентификатор объекта со свойствами его графического представления.</p>
|
|
||||||
|
|
||||||
<p>Достаточно внимательный читатель в этом месте может заметить, что правило информационной иерархии всё равно нарушается: объект высшего уровня Map оперирует данными низшего уровня абстракции - пикселями сцены, и будет совершенно прав. Конечно, в нашем умозрительном примере это совершенно излишне, но в реальном "большом" api карту от этого знания необходимо избавить.</p>
|
|
||||||
|
|
||||||
<p>Для этого необходимо ввести некоторую промежуточную сущность, которая, с одной стороны, будет следить за изменением области картографирования и предоставлять методы трансляции геокоординат в пиксели сцены; с другой - предоставлять графическим примитивам холст для рисования - родительский svg-элемент в нашем случае.</p>
|
|
||||||
|
|
||||||
<p>Назовем такую сущностью, скажем, IRenderingEngine; в нашей иерархии это. Интерфейс займёт промежуточное положение между картой и графическими объектами. Тогда ответственностью карты как IGraphicalContext будет предоставление доступа к своему rendering engine; соответственно, оверлей будет работать уже не с IGraphicalContext, а именно с engine; связывание же оверлея с его графическим контекстом должен произвести тот, кто оверлей инстанцирует - в нашей системе это может быть либо vehicle, либо сам графический контекст, либо какая-то третья сущность, которую мы выделим специально для этого.</p>
|
|
||||||
|
|
||||||
<p>Дерево информационных контекстов (какой объект обладает какой информацией, и кто является транслятором из одного контекста в другой), по сути, представляет собой "срез" нашего дерева иерархии интерфейсов; выделение такого среза позволяет проще и удобнее удерживать в голове всю архитектуру проекта.</p>
|
|
||||||
|
|
||||||
<h3>Связывание объектов</h3>
|
|
||||||
|
|
||||||
<p>Существует множество техник связывания и управления объектами; ряд паттернов проектирования - порождающие, поведенческие, а также MV*-техники посвящены именно этому. Однако, прежде чем говорить о конкретных паттернах, нужно, как и всюду, ответить на вопрос "зачем" - зачем с точки зрения разработки API нам нужно регламентировать связывание объектов? Чего мы хотим добиться?</p>
|
|
||||||
|
|
||||||
<p>В разработке программного обеспечения в целом снижение связанности объектов необходимо прежде всего для уменьшения сайд-эффектов, когда изменения в одной чести кода могут затронуть работоспособность другого части кода, а также для унификации разработки.</p>
|
|
||||||
|
|
||||||
<p>Разумеется, к API эти суждения также применимы с поправкой на то, что изменения происходят не только при рефакторинге кода; API так же должно быть устойчиво:</p>
|
|
||||||
<ul>
|
|
||||||
<li>к изменениям в реализации других компонентов api, в том числе модификации объектов API сторонними разработчика, если такая модификация разрешена;</li>
|
|
||||||
<li>К изменениям во внешней среде - появлению новой функциональности, обновлению стороннего программного и аппаратного обеспечения, адаптации к новым платформам.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>Необходимость слабой связанности объектов API также вытекает из требования дискретных интерфейсов, поскольку поддержание разумно минимальной номенклатуры свойств и методов требует снижения количества связей между объектами. Чем слабее и малочисленнее связи между различными частями API, тем проще заменить одну технологию другой, если возникнет такая необходимость.</p>
|
|
||||||
|
|
||||||
<p>Проблема связывания объектов разбивается на две части:</p>
|
|
||||||
<ul>
|
|
||||||
<li>установление связей между объектами;</li>
|
|
||||||
<li>передача сообщений/команд.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h4>Установление связей между объектами</h4>
|
|
||||||
|
|
||||||
<p>В нашем примере нам нужно установить связи между источниками данных - map и source - и объектами, эти данные представляющими - vehicle и overlay - при посредничестве промежуточных сущностей - renderingEngine. При этом вариантов, как же нам связать эти объекты друг с другом просматривается множество: фактически, связь должна быть - напрямую или опосредованно - между любой парой объектов.</p>
|
|
||||||
|
|
||||||
<p>Можем, например, сделать вот так:</p>
|
|
||||||
<ul>
|
|
||||||
<li>карта принимает при создании source как параметр конструктора;</li>
|
|
||||||
<li>карта инстанцирует vehicle, хранит список всех созданных объектов и обновляет им координаты;</li>
|
|
||||||
<li>разработчик сам создаёт renderingEngine и прикрепляет его к карте методом setRenderingEngine;</li>
|
|
||||||
<li>после прикрепления engine карта создаёт оверлеи на каждый объект vehicle и передаёт их в renderingEngine для отрисовки;</li>
|
|
||||||
<li>При смене области просмотра карта вызывает метод setViewport у source;</li>
|
|
||||||
<li>При изменении географических (вследствие обновления) или пиксельных (вследствие смены области просмотра) координат карта перебирает все созданные ей оверлеи и устанавливает им новые координаты.</li>
|
|
||||||
|
|
||||||
<p>В этой схеме нарочно допущены все мыслимые ошибки проектирования. Разберём их в порядке, описанном в предыдущих главах, от области применения к конечным интерфейсам.</p>
|
|
||||||
|
|
||||||
<p>Во-первых, мы грубо проигнорировали кейсы использования. В нашей схеме у карты только один несменяемый источник данных, хотя разработчику может понадобиться как несколько карт с одним источником, так и множество источников на одной карте. Обратите внимание, кейс "много карт на один источник" мы сами себе заблокировали, заставив карту вызывать setViewport источнику - теперь источник не може отличить, какая из нескольких карт изменила область просмотра. При этом, при наличии нескольких карт, один vehicle, вполне возможно, будет отображаться сразу на нескольких картах.</p>
|
|
||||||
|
|
||||||
<p>Во-вторых, мы переступили через уровень абстракции, заставив карту задавать пиксельные координаты оверлеям. Это приводит к тому, что мы, возможно, будем не в состоянии реализовать дополнительные движки рендеринга и даже оптимизировать старые - например, движок мог бы оптимизировать движение карты на небольшие смещения, отрисовывая графическое окно с запасом и перемещая только область показа, а не все объекты.</p>
|
|
||||||
|
|
||||||
<p>В-третьих, мы создали объект-"швейцарский нож" - карту, которая следит за всем и реализует все сценарии, что приводит к сложностям в рефакторинге, тестировании и поддержке этого объекта.</p>
|
|
||||||
|
|
||||||
<p>Наконец, в-четвёртых, вместо того, чтобы сделать выбор движка рендеринга автоматизированным, мы заставляем разработчика всегда самому выбирать технологию, что осложняет работу с API, вынуждая разбираться в дополнительных, не связанных с решаемой задачей, концепциях и приводит к невозможности сменить движок по умолчанию в будущем.</p>
|
|
||||||
|
|
||||||
<p>Следует заметить, что каждую из этих задач можно закостылять и решить без нарушения обратной совместимости. Например, можно сделать в объекте source метод порождения дочерних объектов, выполняющих тот же интерфейс, чтобы передавать их в конструкторы map - таким образом можно будет привязывать несколько карт к одному источнику. Проблему с выставлением пиксельных координат оверлеям можно решить, написав новую реализацию оверлея, которая не будет применять полученные координаты, а обратится в renderingEngine для пересчёта полученных от карты координат в актуальные. И так далее, и тому подобное - через несколько итераций таких "улучшений" мы получим типичное современное API, в котором каждая функция есть магический чёрный ящик, выполняющий что угодно, кроме того, что написано в её названии, а для полноценной работы с таким API нужно прочитать не только всю документацию, но и комментарии на форумах разработчиков относительно неочевидной работы тех или иных функций.</p>
|
|
||||||
|
|
||||||
<p>Попробуем теперь спроектировать API правильно. Начнём с отношений map и source.</p>
|
|
||||||
|
|
||||||
<p>Мы знаем, исходя из сценариев использования, что связь map и source имеет вид "многие ко многим", хотя кейс "одна карта - один источник" является самым частотным. Мы знаем, что разработчики будут реализовывать свои source, но вряд ли свои map. Мы также знаем, что эти реализации будут сильно различаться по потребностям - в каких-то системах потребуется оптимизация - так, чтобы source следил только за видимыми объектами - а в каких-то, напротив, объектов мало и заниматься оптимизацией преждевременно.</p>
|
|
||||||
|
|
||||||
<p>Отсюда мы можем сформулировать наши требования к связыванию:</p>
|
|
||||||
<ul>
|
|
||||||
<li>начальное привязывание одиночного source к карте должно быть максимально упрощено;</li>
|
|
||||||
<li>должны быть методы добавления и удаления связей в runtime;</li>
|
|
||||||
<li>стандартные реализации source и map должны максимально упростить реализацию сложного кейса "к одному source подключено много map, и он оптимизирует слежение, запрашивая только видимые объекты"</li>
|
|
||||||
<li>разработчик должен иметь возможность реализовать свою имплементацию source, работающего по иным принципам, не прибегая к необходимости костылить методы.</p>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>Поскольку и карта, и источник должны знать друг о друге (источник отслеживает изменение области просмотра карты, а карта отслеживает обновление состояния источника), нам придётся создать парные методы добавления и удаления связей и в карте, и в источнике, что приводит нас к интерфейсу вида:</p>
|
|
||||||
|
|
||||||
<code>
|
|
||||||
Map.addSource(source)
|
|
||||||
Map.removeSource(source)
|
|
||||||
Source.addMap(map)
|
|
||||||
Source.removeMap(map)
|
|
||||||
</code>
|
|
||||||
|
|
||||||
<p>Однако такой интерфейс чрезвычайно не очевиден: из него не понятно, что для успешного связывания нужно выполнить и addSource, и addMap.</p>
|
|
||||||
|
|
||||||
<p>Мы можем упростить этот момент, реализовав эти методы так, чтобы они сами выполняли вызов связанного метода. Однако, проблемы разработчика это не решит: всё ещё неясно, каким методом пользоваться правильно.</p>
|
|
||||||
|
|
||||||
<p>Популярное решение состоит в том, чтобы объявить один из методов точкой входа для разработчика, а второй объявить техническим и запретить его вызывать иначе как из другого. Расширенным вариантом этого решения является объявление обеих пар методов техническими и создание специального объекта, через который осуществляется связывание.</p>
|
|
||||||
|
|
||||||
<p>На самом деле, такое решение не внесёт больше понимания. Из номенклатуры методов неясно, что же конкретно они делают. Попросту скрыть их также нельзя: разработчику, пишущему свою имплементацию source или map, всё равно придётся эти методы реализовать и разобраться в механизме их работы.</p>
|
|
||||||
|
|
||||||
<p>Чтобы выйти из этого порочного круга, вспомним о правиле декомпозиции интерфейсов. Если source имеет единственную задачу быть источником данных, то карта - композиция нескольких интерфейсов, и во взаимоотношениях с source должна выступать не как объект map, а как некий абстрактный провайдер сведений о наблюдаемой области.</p>
|
|
||||||
|
|
||||||
<p>Следовательно, метода addMap у source быть не может - для него добавление карты означает появление дополнительной отслеживаем ой области. Метод должен выглядеть примерно следующим образом:</p>
|
|
||||||
|
|
||||||
<code>
|
|
||||||
Source.addObserver(IGeographicalContext, IObserver)
|
|
||||||
</code>
|
|
||||||
|
|
||||||
<p>При выполнении этого метода source начинает передавать observer-у сведения о том, что происходит в указанной области. Тогда мы можем реализовать метод addSource так, чтобы он создавал observer и привязывал карту-контекст к источнику.</p>
|
|
||||||
|
|
||||||
<p>В этом решении ситуация выглядит понятной и логичной и в решении стандартных кейсов, так и при написании собственных имплементаций source.</p>
|
|
||||||
|
|
||||||
<p>Перейдём теперь к вопросу создания и связывания vehicle с картой и источником.</p>
|
|
||||||
|
|
||||||
|
|
||||||
<!--
|
|
||||||
<h2>О проектировании API</h2>
|
|
||||||
<p><em>Зачем</em> такому сервису API?</p>
|
|
||||||
- решать проблемы
|
|
||||||
- не делать конкурентов самому себе (если ты не амазон)
|
|
||||||
- множество разнородных задач:
|
|
||||||
- сделать хорошо конечному пользователю
|
|
||||||
- сделать хорошо разработчику
|
|
||||||
<h3>О неважности кода</h3>
|
|
||||||
- в правильном апи любой код заменяем и он неважен
|
|
||||||
<h3>Code style</h3>
|
|
||||||
— "так здесь принято" vs здравый смысл
|
|
||||||
<h3>Именование</h3>
|
|
||||||
<p>Если говорить и чисто программной составляющей API, то, пожалуй, именно именование важнее всего. Всё остальное, в конечном счёте, можно подправить или переписать, но вот номенклатура сущностей — это лицо вашего API, это фасад, с которым будет работать ваш пользователь.</p>
|
|
||||||
<p>Первое и важнейшее правило именования весьма просто: имя сущности должно понятно и однозначно описывать смысл этой сущности. Имя метода должно показывать, какие действия произойдут при вызове этого метода; имя поля — какой объект хранится в этом поле; имя класса — что из себя будет представлять экземпляр этого класса; и так далее.</p>
|
|
||||||
— делать именно то, что нужно
|
|
||||||
— сайдэффекты
|
|
||||||
- build и update
|
|
||||||
<h3>Тестирование</h3>
|
|
||||||
<p>Тесты обязаны быть в любом API; это, в общем, вытекает из определения API.</p>
|
|
||||||
<p>Вместе с тем, </p>
|
|
||||||
— покрытие в зависимости от проекта
|
|
||||||
<h3>Workflow</h3>
|
|
||||||
— эджайл vs здравый смысл
|
|
||||||
<h3>Версионирование</h3>
|
|
||||||
- зачем?
|
|
||||||
- semver
|
|
||||||
<h3>Уровни абстракции</h3>
|
|
||||||
- как?
|
|
||||||
- подход "от железа"
|
|
||||||
<h3>Интерфейсы</h3>
|
|
||||||
- зачем?
|
|
||||||
- 5 плюс минус 2
|
|
||||||
- объект в разных разрезах
|
|
||||||
<h3>Связность</h3>
|
|
||||||
<h4>Code reuse</h4>
|
|
||||||
- хороший копипаст
|
|
||||||
- плохой копипаст
|
|
||||||
<h4>Модульность</h4>
|
|
||||||
<h4>События</h4>
|
|
||||||
<h4>Паттерны</h4>
|
|
||||||
<h2>О видении продукта</h2>
|
|
||||||
<h3>Стабильность</h3>
|
|
||||||
<h3>Точки расширения</h3>
|
|
||||||
<h3>Внешние зависимости</h3>
|
|
||||||
<h3>Управление процессом</h3>
|
|
||||||
<p>Дело в том, что разработчик API, по определению, обладает относительно полным знанием о том, как API устроено, какие в нём есть крутые фишки и какие сложные задачи оно способно решать.</p>
|
|
||||||
<p>В реальности же 90%, если не больше, обращающихся за поддержкой пользователей будут решать совершенно тривиальные задачи и у них в принципе нет никакой мотивации разбираться с внутренним устройством API. Пользователь хочет решить свою задачу с наименьшим напряжением сил, и было бы странным его за это упрекать.</p>
|
|
||||||
<p>Люди не читают документацию. Не ищут ответов на свои вопросы на StackOverflow. Не являются гуру разработки. Даже напротив, абсолютное большинство ваших клиентов — начинающие и просто малоквалифицированные специалисты. Даже если вашим API пользуются ребята из соседнего отдела, квалификация которых не уступает вашей, — вряд ли даже у них есть желание глубоко разбираться в вашей архитектуре.</p>
|
|
||||||
<p>Поэтому любой поток обращений, как правило, делится на две неравные категории: вопросы от начинающих (часто глупые или тривиальные) и очень редкие запросы от профессионалов. Запросов первого типа абсолютное большинство, и, при том, найти проблему в коде пользователя частенько бывает весьма нетривиальной задачей.</p>
|
|
||||||
-->
|
|
||||||
</body></html>
|
|
378
API.ru.md
Normal file
378
API.ru.md
Normal file
@ -0,0 +1,378 @@
|
|||||||
|
# Сергей Константинов
|
||||||
|
# API
|
||||||
|
|
||||||
|
[](http://creativecommons.org/licenses/by-nc/4.0/)
|
||||||
|
Это произведение доступно по [лицензии Creative Commons «Attribution-NonCommercial» («Атрибуция — Некоммерческое использование») 4.0 Всемирная](http://creativecommons.org/licenses/by-nc/4.0/).
|
||||||
|
|
||||||
|
## О структуре этой книги
|
||||||
|
|
||||||
|
Книга, которую вы держите в руках, состоит из трех больших разделов.
|
||||||
|
|
||||||
|
В первом разделе мы поговорим о проектировании API на стадии разработки концепции - как грамотно выстроить архитектуру, от крупноблочного планирования до конечных интерфейсов.
|
||||||
|
|
||||||
|
Второй раздел будет посвящён жизненному циклу API - как интерфейсы эволюционируют со временем и как развивать продукт так, чтобы отвечать потребностям пользователей.
|
||||||
|
|
||||||
|
Наконец, третий раздел будет касаться больше не-разработческих сторон жизни API - поддержки, маркетинга, работы с комьюнити.
|
||||||
|
|
||||||
|
Первые два будут интересны скорее разработчикам, третий — и разработчикам, и менеджерам. При этом я настаиваю, что как раз третий раздел — самый важный для разработчика API. Ввиду того, что API - продукт для разработчиков, перекладывать ответственность за его развитие и поддержку на не-разработчиков неправильно: никто кроме вас самих не понимает так хорошо продуктовые свойства вашего 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 в виде веб-сервиса: разработчик обращается по сформированному определённым образом url и получает погодную информацию. В общем-то, всю имеющуюся у нас информацию можно через такой сервис предоставлять; он будет универсален - такое обращение можно реализовать на любой платформе, и, таким образом, оно покроет все кейсы. Разве нет?
|
||||||
|
|
||||||
|
Теперь давайте подойдём к вопросу с другой стороны. А как было бы удобно пользоваться нашим API, скажем, веб-мастеру-администратору небольшого локального форума? Скорее всего, ему требуется решение, которое не требует навыков разработки, чтобы было достаточно мышкой нащёлкать параметры информатора о прогнозе погоды. Нет, мы можем, конечно презрительно отмахнуться от таких непрофессионалов; но, тем самым, мы лишим сами себя значительной доли рынка.
|
||||||
|
|
||||||
|
Аналогично, если мы посмотрим на потребности крупных диспетчерских компаний, то мы, вероятно, выясним, что у них уже есть какое-то автоматизированной рабочее место диспетчера, и идеальным для них решением был бы набор компонентов, которые можно интегрировать в их программный комплекс. Аналогично, у новостных ресурсов также есть какое-то готовое решение для управления контентом, и они предпочтут не заказывать дорогостоящую разработку модуля визуализации погоды, а готовое решение.
|
||||||
|
|
||||||
|
Иными словами, в начале разработки API перед нами всегда стоит вопрос: насколько приближено оно должно быть к конечному пользователю? Чем более высок уровень вашего API, чем проще оно позволяет реализовать пользовательские сценарии - тем более интересно оно будет разработчикам, но тем дороже оно обойдётся вам самим. Понятно, что невозможно написать модули и компоненты для всех существующих cms и фреймворков, и вам придётся где-то остановиться - исходя из ваших представлений о массовости тех или иных кейсов. Вполне возможно, что в нашей гипотетической ситуации мы в итоге можем прийти к решению делать только http API, так как выясним, что получим от виджетов меньше дохода, чем потратим на разработку, а у каждого из крупных клиентов есть свой отдел разработки и они не доверяют в business-critical задачах разработкам сторонних компаний.
|
||||||
|
|
||||||
|
Однако, чтобы нам было, о чем говорить в дальнейших разделах, предположим, что мы решили закрывать следующие сценарии:
|
||||||
|
|
||||||
|
* общее http API для клиентов, готовых вести разработку самостоятельно;
|
||||||
|
* набор виджетов для мобильных и веб-приложений;
|
||||||
|
* набор компонентов для встраивания в АРМы диспетчерских компаний.
|
||||||
|
|
||||||
|
Перейдём теперь непосредственно к проектированию API.
|
||||||
|
|
||||||
|
### Уровни абстракции
|
||||||
|
|
||||||
|
"Разделите свой код на уровни абстракции" - пожалуй, самый общий совет для разработчиков программного обеспечения. Что под этим обычно подразумевается?
|
||||||
|
|
||||||
|
Вспомним, что программный продукт - это средство связи контекстов, средство преобразования терминов и операций одной предметной области в другую. Чем дальше друг от друга эти области отстоят - тем большее число промежуточных контекстов можно ввести. Например, модель OSI, которую часто приводят как эталон разделения уровней абстракции, насчитывает семь промежуточных этапов по дороге от аппаратного обеспечения к протоколам уровня приложений.
|
||||||
|
|
||||||
|
Если говорить о разделении уровней абстракции именно с точки зрения API, то оно очень желательно по нескольким причинам:
|
||||||
|
|
||||||
|
* Разделение проекта на несколько независимых частей; архитектура каждой из них, таким образом, становится проще, и упрощается интеграция;
|
||||||
|
* с помощью такого разделения гораздо легче добиваться кроссплатформенности путём отделения платформо-зависимой логики в отдельный уровень (или уровни) абстракции.
|
||||||
|
|
||||||
|
И главное:
|
||||||
|
|
||||||
|
* Упрощается задача для ваших клиентов; правильно разделённые уровни абстракции означают, что разработчикам не придется разбираться со всей номенклатурой сущностей вашего API - им достаточно будет работать только с объектами высокого уровня, отвечающими непосредственно за решение их задач.
|
||||||
|
|
||||||
|
Если мы вернёмся к нашему примеру с погодным API, то увидим, что один уровень абстракции выделился автоматически: http API. Мы можем построить прочие виды API поверх базового http. Можем ли мы выделить ещё какие-то уровни, скажем, внутри самого http API?
|
||||||
|
|
||||||
|
#### Построение иерархии абстракций
|
||||||
|
|
||||||
|
Как обычно, мы начнём с вопроса "зачем" - если у нас не получается строгая иерархия, зачем нужна нестрогая? В первую очередь для того, чтобы вашим разработчикам было удобнее пользоваться вашим API. Представим, что мы спроектировали вот такой интерфейс для нашего http API:
|
||||||
|
|
||||||
|
* GET /weather/temperature/{city_id} - возвращает текущую температуру в городе с указанным идентификатором;
|
||||||
|
* GET /weather/temperature/{city_id}/forecast - возвращает прогноз изменения температуры;
|
||||||
|
* GET /weather/pressure/{city_id} - возвращает атмосферное давление.
|
||||||
|
|
||||||
|
Покрывает ли такой интерфейс наши сценарии использования? С одной стороны кажется, что да - каждый потребитель сможет получить нужный ему набор информации.
|
||||||
|
|
||||||
|
Но, с другой стороны, будет ли удобно вебмастеру, который хочет просто установить на свой сайт "прогноз погоды", пользоваться таким API? Понятно, что ему не нужны по отдельности ни температура, ни давление - ему нужна ровно та информация, которую захочет увидеть пользователь его ресурса.
|
||||||
|
|
||||||
|
Оптимально было бы ввести ещё один метод:
|
||||||
|
|
||||||
|
* GET /weather/{city_id} - возвращает параметры погоды в указанном городе: температуру, давление, прогноз.
|
||||||
|
|
||||||
|
Этот новый ресурс позволит пользователям API оперировать не в метеорологических терминах - а в терминах задачи, которую они решают. Тем самым мы спроектировали интерфейс высокого уровня, опирающийся на интерфейсы нижнего уровня абстракции.
|
||||||
|
|
||||||
|
Аналогично, если мы будем проектировать API для новостных ресурсов, то придём к необходимости выделения сущности "картина атмосферных фронтов"; при проектировании API для диспетчерских служб - к сущности "план опасных для полета зон", и так далее.
|
||||||
|
|
||||||
|
Разделение уровней абстракции должно происходить вдоль трёх направлений:
|
||||||
|
|
||||||
|
* от сценариев использования к их внутренней реализации: высокоуровневые сущности и номенклатура их методов должны напрямую отражать сценарии использования API; низкоуровневый - отражать декомпозицию сценариев на составные части;
|
||||||
|
* от терминов предметной области пользователя к терминам предметной области исходных данных - в нашем случае от высокоуровневых понятий "прогноз погоды" и "движение атмосферных фронтов" к базовым "температура", "давление", "скорость ветра";
|
||||||
|
* наконец, от структур данных, в которых удобно оперировать пользователю к структурам данных, максимально приближенных к "сырым" - в нашем случае от "атмосферных фронтов" и "магнитных бурь" - к сырым байтовый данным, описывающим поле атмосферного давления или магнитное поле Земли.
|
||||||
|
|
||||||
|
Чем дальше находятся друг от друга программные контексты, которые соединяет наше API - тем более глубокая иерархия сущностей должна получиться у нас в итоге. Обращаю ваше внимание, что иерархия не должна быть самоцелью; цель - введение промежуточных ступеней погружения в тонкости предметной области. Избыточная сложность здесь не менее вредна, чем избыточная простота.
|
||||||
|
|
||||||
|
### Уровни абстракции и декомпозиция
|
||||||
|
|
||||||
|
Приближённость к сценариям использования - не единственная польза правильно построенной иерархии абстракций. Другим критерием качества архитектуры API здесь является снижение связности и декомпозиция объектов API.
|
||||||
|
|
||||||
|
Рассмотрим следующий пример организации функциональности карты атмосферных фронтов.
|
||||||
|
|
||||||
|
Первый способ:
|
||||||
|
|
||||||
|
* GET /map - возвращает множество геометрий атмосферных объектов, попадающих в заданную область;
|
||||||
|
* GET /geometry/{geometry_id} - возвращает метаданные о геометрии, в том числе тип объекта, описываемого геометрией (область повышенного или пониженного давления, атмосферный фронт);
|
||||||
|
* GET /area/{area_id}, GET /front/{front_id} - возвращает метаданные конечного объекта.
|
||||||
|
|
||||||
|
Порядок работы, вроде бы, ясен: разработчик получает карту и отрисовывает её, опираясь, если это необходимо, на нужные метаданные.
|
||||||
|
|
||||||
|
Однако, очевидно, с точки зрения иерархии здесь допущена ошибка: высокоуровневые сущности (карта, атмосферные фронты и области повышенного и пониженного давления) связаны посредством технической сущности - визуальной геометрии объекта, которая по всем критериям должна находиться где-то в самом низу иерархии. Чем это может быть плохо для нас?
|
||||||
|
|
||||||
|
Предположим, завтра у нас появится клиент, которого не устраивает векторное описание геометрии, и которому нужна растровая картинка. Мы окажемся в сложном положении: либо мы создаем параллельное API именно для работы с растром, либо клиенту придётся забирать и растровую картинку, и векторную - вторую только для того, чтобы через нее получить идентификаторы погодных объектов. Если бы иерархия была выстроена правильно (карта погоды отдает идентификаторы объектов, а уже по идентификаторам можно получить геометрии) - достаточно было бы добавить режим "растровое изображение".
|
||||||
|
|
||||||
|
Аналогичные проблемы нас ждут, если в какой-то момент появятся объекты, содержащие более одной геометрии - либо, наоборот, несколько объектов научатся делить одну геометрию. В правильной архитектуре мы просто добавим новые типы ресурсов; в неправильной нам вообще не удастся решить эту ситуацию без слома обратной совместимости.
|
||||||
|
|
||||||
|
Оба этих примера эксплуатируют одну и ТЦ же мысль: в иерархии уровней абстракции верхние уровни являются, как правило, более устойчивыми к разного рода изменениям. В самом деле, если объекты верхнего уровня описывают сценарии использования - они гораздо менее вероятно изменятся с развитием технологии. Потребность "мне нужно узнать прогноз погоды на завтра" никак не меняется уже много тысяч лет.
|
||||||
|
|
||||||
|
Чем универсальнее ваше API в смысле широты тех контекстов, которые вы связываете, тем больше альтернативных реализаций одного и того функционала вам придётся реализовать (см. Пример с растровыми и векторными изображениями) и тем "шире" окажется ваша иерархия уровней абстракции: у вас появится множество объектов, принадлежащих одному уровню; в достаточно большом 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 быть максимально гибким и одновременно избежать проблем сильной связанности объектов?
|
||||||
|
|
||||||
|
Для этого необходимо, в первую очередь, определить ответственность каждой сущности: в чём смысл её существования в рамках нашего API, какие действия объект должен уметь выполнять сам а какие - делегировать другим объектам. Фактически, нам нужно применить "зачем-принцип" к каждой отдельной сущности нашего API.
|
||||||
|
|
||||||
|
Из предыдущей главы мы выяснили, например, что ответственностью source является возврат пар идентификатор-координаты, и ничего более. Попробуем воспроизвести подобные рассуждения в отношении других объектов.
|
||||||
|
|
||||||
|
Для этого нам нужно пройти по нашему API и сформулировать в терминах предметной области, что представляет из себя каждый объект. Напомню, что из концепции уровней абстракции следует, что каждый уровень иерархии - это некоторая собственная промежуточная предметная область, ступенька, по которой мы переходим от описания задачи в терминах одного связываемого контекста ("карта погоды с движущимися по ней транспортными средствами") к описанию в терминах второго ("SVG-объект, заданный в вычисляемых извне пиксельных координатах").
|
||||||
|
|
||||||
|
В этом смысле проще всего описать объект Vehicle - это программная сущность, представляющая собой абстракцию над объектом реального мира. Её задачей, очевидно, является описание характеристик реального объекта, которые нужны для решения поставленных задач. В нашем случае это идентификатор (или иной способ опознания объекта) и его географическое положение. Если в нашем API появятся какие-то расширенные сведения об реальном объекте - скажем, название или скорость, - очевидно, они будут привязаны к тому же объекту Vehicle.
|
||||||
|
|
||||||
|
Кроме того, ещё одной задачей Vehicle является описание собственного представления в интерфейсе - скажем, параметров иконки, если наш Vehicle должен выглядеть для пользователя интерфейса как значок на карте.
|
||||||
|
|
||||||
|
С оверлеем всё тоже вполне ясно - это некоторый графический примитив, который должен уметь интерпретировать опции Vehicle и отображать заданный значок. Кроме того, оверлей должен уметь отображаться в произвольных пиксельных координатах в контексте карты.
|
||||||
|
|
||||||
|
Наконец, что такое "карта" в терминах решаемой задачи? Это схематическое изображение фрагмента земной поверхности, согласно запрошенным координатам и масштабу. В чём заключается ответственность карты в нашей иерархии? Очевидно, в предоставлении данных о наблюдаемой пользователем области (координаты и масштаб) и информации об изменениях этих параметров. Кроме того, ответственностью карты в том или ином виде является пересчёт координат (очевидно, карта "знает", в какой проекции она нарисована) и предоставление возможности отрисовать поверх себя графические фигуры (оверлеи).
|
||||||
|
|
||||||
|
Если внимательно посмотреть на каждый объект, то мы увидим, что, в итоге, каждый объект оказался в смысле своей ответственности составным: Vehicle одновременно "знает" и про объект реального мира, и про его виртуальное отображение на карте; карта "знает" свою область картографирования - фактически, область на реальной Земле, которую схематически отображает, - и при этом должна предоставлять некоторый контекст для отображения виртуальных графических фигур, и так далее.
|
||||||
|
|
||||||
|
Ничего удивительного в этом, конечно же, нет — поскольку API в целом связывает разные контексты, в нём всегда будут объекты, объединяющие термины разных предметных областей. Наша задача - декомпозировать объекты так, чтобы, с одной стороны, разработчикам было удобно и понятно пользоваться нашей иерархией абстракций, а нам, с другой стороны, было удобно такую архитектуру поддерживать.
|
||||||
|
|
||||||
|
### Декомпозиция интерфейсов
|
||||||
|
|
||||||
|
Если каждый объект представляет собой объединение разнородной ответственности в терминах разных предметных областей, каким образом мы можем добиться эффективного уменьшения связанности объектов между собой? Давайте подумаем, где мы можем "сэкономить" на связях.
|
||||||
|
|
||||||
|
Возьмём, например, объект Map. Во взаимодействии с объектом source он выступает как чистый источник географических координат, вся прочая ответственность объекта карты source не касается.
|
||||||
|
|
||||||
|
Напротив, оверлею географические координаты ни к чему: он существует в некотором графическом контексте, где оперируют пикселями.
|
||||||
|
|
||||||
|
Раз смежным объектам знание о полной функциональности карты не нужно, именно здесь мы и можем убрать лишние связи: потребуем, чтобы карта при взаимодействии с источником данных выступала только как картографический контекст, а при взаимодействии с оверлеем - как чисто графический контекст. Для организации такой абстракции используются интерфейсы.
|
||||||
|
|
||||||
|
Определим два интерфейса:
|
||||||
|
|
||||||
|
* IGeoContext - предоставляет методы работы с областью картографирования;
|
||||||
|
* IGraphicalContext - предоставляет контекст рендеринга.
|
||||||
|
|
||||||
|
Объект 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 сторонними разработчика, если такая модификация разрешена;
|
||||||
|
* К изменениям во внешней среде - появлению новой функциональности, обновлению стороннего программного и аппаратного обеспечения, адаптации к новым платформам.
|
||||||
|
|
||||||
|
Необходимость слабой связанности объектов API также вытекает из требования дискретных интерфейсов, поскольку поддержание разумно минимальной номенклатуры свойств и методов требует снижения количества связей между объектами. Чем слабее и малочисленнее связи между различными частями API, тем проще заменить одну технологию другой, если возникнет такая необходимость.
|
||||||
|
|
||||||
|
Проблема связывания объектов разбивается на две части:
|
||||||
|
|
||||||
|
* установление связей между объектами;
|
||||||
|
* передача сообщений/команд.
|
||||||
|
|
||||||
|
#### Установление связей между объектами
|
||||||
|
|
||||||
|
В нашем примере нам нужно установить связи между источниками данных - map и source - и объектами, эти данные представляющими - vehicle и overlay - при посредничестве промежуточных сущностей - renderingEngine. При этом вариантов, как же нам связать эти объекты друг с другом просматривается множество: фактически, связь должна быть - напрямую или опосредованно - между любой парой объектов.
|
||||||
|
|
||||||
|
Можем, например, сделать вот так:
|
||||||
|
|
||||||
|
* карта принимает при создании source как параметр конструктора;
|
||||||
|
* карта инстанцирует vehicle, хранит список всех созданных объектов и обновляет им координаты;
|
||||||
|
* разработчик сам создаёт renderingEngine и прикрепляет его к карте методом setRenderingEngine;
|
||||||
|
* после прикрепления engine карта создаёт оверлеи на каждый объект vehicle и передаёт их в renderingEngine для отрисовки;
|
||||||
|
* При смене области просмотра карта вызывает метод setViewport у source;
|
||||||
|
* При изменении географических (вследствие обновления) или пиксельных (вследствие смены области просмотра) координат карта перебирает все созданные ей оверлеи и устанавливает им новые координаты.
|
||||||
|
|
||||||
|
В этой схеме нарочно допущены все мыслимые ошибки проектирования. Разберём их в порядке, описанном в предыдущих главах, от области применения к конечным интерфейсам.
|
||||||
|
|
||||||
|
Во-первых, мы грубо проигнорировали кейсы использования. В нашей схеме у карты только один несменяемый источник данных, хотя разработчику может понадобиться как несколько карт с одним источником, так и множество источников на одной карте. Обратите внимание, кейс "много карт на один источник" мы сами себе заблокировали, заставив карту вызывать setViewport источнику - теперь источник не може отличить, какая из нескольких карт изменила область просмотра. При этом, при наличии нескольких карт, один vehicle, вполне возможно, будет отображаться сразу на нескольких картах.
|
||||||
|
|
||||||
|
Во-вторых, мы переступили через уровень абстракции, заставив карту задавать пиксельные координаты оверлеям. Это приводит к тому, что мы, возможно, будем не в состоянии реализовать дополнительные движки рендеринга и даже оптимизировать старые - например, движок мог бы оптимизировать движение карты на небольшие смещения, отрисовывая графическое окно с запасом и перемещая только область показа, а не все объекты.
|
||||||
|
|
||||||
|
В-третьих, мы создали объект-"швейцарский нож" - карту, которая следит за всем и реализует все сценарии, что приводит к сложностям в рефакторинге, тестировании и поддержке этого объекта.
|
||||||
|
|
||||||
|
Наконец, в-четвёртых, вместо того, чтобы сделать выбор движка рендеринга автоматизированным, мы заставляем разработчика всегда самому выбирать технологию, что осложняет работу с API, вынуждая разбираться в дополнительных, не связанных с решаемой задачей, концепциях и приводит к невозможности сменить движок по умолчанию в будущем.
|
||||||
|
|
||||||
|
Следует заметить, что каждую из этих задач можно закостылять и решить без нарушения обратной совместимости. Например, можно сделать в объекте source метод порождения дочерних объектов, выполняющих тот же интерфейс, чтобы передавать их в конструкторы map - таким образом можно будет привязывать несколько карт к одному источнику. Проблему с выставлением пиксельных координат оверлеям можно решить, написав новую реализацию оверлея, которая не будет применять полученные координаты, а обратится в renderingEngine для пересчёта полученных от карты координат в актуальные. И так далее, и тому подобное - через несколько итераций таких "улучшений" мы получим типичное современное API, в котором каждая функция есть магический чёрный ящик, выполняющий что угодно, кроме того, что написано в её названии, а для полноценной работы с таким API нужно прочитать не только всю документацию, но и комментарии на форумах разработчиков относительно неочевидной работы тех или иных функций.
|
||||||
|
|
||||||
|
Попробуем теперь спроектировать API правильно. Начнём с отношений map и source.
|
||||||
|
|
||||||
|
Мы знаем, исходя из сценариев использования, что связь map и source имеет вид "многие ко многим", хотя кейс "одна карта - один источник" является самым частотным. Мы знаем, что разработчики будут реализовывать свои source, но вряд ли свои map. Мы также знаем, что эти реализации будут сильно различаться по потребностям - в каких-то системах потребуется оптимизация - так, чтобы source следил только за видимыми объектами - а в каких-то, напротив, объектов мало и заниматься оптимизацией преждевременно.
|
||||||
|
|
||||||
|
Отсюда мы можем сформулировать наши требования к связыванию:
|
||||||
|
|
||||||
|
* начальное привязывание одиночного source к карте должно быть максимально упрощено;
|
||||||
|
* должны быть методы добавления и удаления связей в runtime;
|
||||||
|
* стандартные реализации source и map должны максимально упростить реализацию сложного кейса "к одному source подключено много map, и он оптимизирует слежение, запрашивая только видимые объекты"
|
||||||
|
* разработчик должен иметь возможность реализовать свою имплементацию source, работающего по иным принципам, не прибегая к необходимости костылить методы.
|
||||||
|
|
||||||
|
Поскольку и карта, и источник должны знать друг о друге (источник отслеживает изменение области просмотра карты, а карта отслеживает обновление состояния источника), нам придётся создать парные методы добавления и удаления связей и в карте, и в источнике, что приводит нас к интерфейсу вида:
|
||||||
|
|
||||||
|
`Map.addSource(source) Map.removeSource(source) Source.addMap(map) Source.removeMap(map)`
|
||||||
|
|
||||||
|
Однако такой интерфейс чрезвычайно не очевиден: из него не понятно, что для успешного связывания нужно выполнить и addSource, и addMap.
|
||||||
|
|
||||||
|
Мы можем упростить этот момент, реализовав эти методы так, чтобы они сами выполняли вызов связанного метода. Однако, проблемы разработчика это не решит: всё ещё неясно, каким методом пользоваться правильно.
|
||||||
|
|
||||||
|
Популярное решение состоит в том, чтобы объявить один из методов точкой входа для разработчика, а второй объявить техническим и запретить его вызывать иначе как из другого. Расширенным вариантом этого решения является объявление обеих пар методов техническими и создание специального объекта, через который осуществляется связывание.
|
||||||
|
|
||||||
|
На самом деле, такое решение не внесёт больше понимания. Из номенклатуры методов неясно, что же конкретно они делают. Попросту скрыть их также нельзя: разработчику, пишущему свою имплементацию source или map, всё равно придётся эти методы реализовать и разобраться в механизме их работы.
|
||||||
|
|
||||||
|
Чтобы выйти из этого порочного круга, вспомним о правиле декомпозиции интерфейсов. Если source имеет единственную задачу быть источником данных, то карта - композиция нескольких интерфейсов, и во взаимоотношениях с source должна выступать не как объект map, а как некий абстрактный провайдер сведений о наблюдаемой области.
|
||||||
|
|
||||||
|
Следовательно, метода addMap у source быть не может - для него добавление карты означает появление дополнительной отслеживаем ой области. Метод должен выглядеть примерно следующим образом:
|
||||||
|
|
||||||
|
`Source.addObserver(IGeographicalContext, IObserver)`
|
||||||
|
|
||||||
|
При выполнении этого метода source начинает передавать observer-у сведения о том, что происходит в указанной области. Тогда мы можем реализовать метод addSource так, чтобы он создавал observer и привязывал карту-контекст к источнику.
|
||||||
|
|
||||||
|
В этом решении ситуация выглядит понятной и логичной и в решении стандартных кейсов, так и при написании собственных имплементаций source.
|
||||||
|
|
||||||
|
Перейдём теперь к вопросу создания и связывания vehicle с картой и источником.
|
Reference in New Issue
Block a user