mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-04-17 11:06:25 +02:00
517 lines
100 KiB
HTML
517 lines
100 KiB
HTML
<!doctype html>
|
|
<html><head>
|
|
<meta charset="utf-8"/>
|
|
<title>API</title>
|
|
<style>
|
|
body {
|
|
width: 60%;
|
|
max-width: 1000px;
|
|
margin: 10px auto 0 300px;
|
|
font-family: Georgia, serif;
|
|
font-size: 16px;
|
|
}
|
|
|
|
header {
|
|
text-align: center;
|
|
}
|
|
|
|
h1, h2, h3, h4 {
|
|
font-weight: bold;
|
|
margin: 0;
|
|
padding: 0.2em 0 0.2em 0;
|
|
}
|
|
</style>
|
|
</head><body>
|
|
<header>
|
|
<h1>Сергей Константинов</h1>
|
|
<h1>API</h1>
|
|
<img src="Why.jpg" style="height: 600px;"/>
|
|
</header>
|
|
<h2>Об авторе</h2>
|
|
<h2>Введение</h2>
|
|
|
|
<p>Если мы посмотрим на определение из энциклопедии, то узнаем, что программирование — это что-то вроде преобразования формально проставленной задачи в исполняемый код, как правило, путём имплементации какого-то алгоритма на определённом языке программирования. И действительно, курсы обучения соответствующим дисциплинам большей частью и состоят из инструкций, как перевести словесно поставленную задачу в код и запрограммировать тот или иной алгоритм.</p>
|
|
|
|
<p>Этот алгоритмический подход — пожалуй, худшее, что могло случиться с программистами. Определять программирование через имплементацию алгоритмов — это как определять работу композитора как написание специальных значков на нотном стане, математика — как написание формул, астронома — как смотрение в телескоп.</p>
|
|
|
|
<p>Чем плохи эти определения? Тем, что они описывают лишь формальную сторону процесса, оставляя за кадром главный вопрос: зачем? Зачем программисты пишут код, математики - формулы, композиторы - ноты? Математики увеличивают сумму человеческих знаний, композиторы высекают своей музыкой искры из людских сердец, а вот зачем программисты пишут свои алгоритмы?</p>
|
|
|
|
<p>Почему я придаю этому вопросу столь большое значение? Потому что методы решения задачи и использованные инструменты в первую очередь должны быть адекватны используемой задаче, а всё остальное вторично. Именно цель написания кода должна определять такие первичные вещи, как архитектура приложения, используемый язык разработки и выбор инструментария. Я прочитал немалое количество книг по профессиональной разработке программного обеспечения - и не нашел качественного освещения этого вопроса ни в одной из них. Все они рассказывают, как писать "хороший" код - поддерживаемый, расширяемый, безошибочный; но опускают при этом вопрос о том, насколько написание "хорошего" кода адекватно стоящим перед разработчиками целям. Между тем, мировая практика такова, что ставит под большое сомнение предположение, что хороший код означает качественный или хотя бы поддерживаемый и расширяемый продукт. Легко привести множество примеров удачных продуктов, написанных откровенно плохо и с нарушением всех мыслимых заповедей разработки.</p>
|
|
|
|
<p>Хочу ли я тем самым сказать, будто бы нужно писать плохой код? Нет, что вы - на протяжении всей этой книги я буду призывать вас писать хороший код. Я хочу сказать, что, если мы посмотрим на разработку программных продуктов через призму вопроса "зачем" (они разрабатываются), то поймём, что качество решения задач пользователей само по себе не проистекает из следования советам по написанию хорошего кода - это совершенно отдельное качество продукта, и думать о нём также необходимо отдельно.</p>
|
|
|
|
<h3>Об API и уровнях восприятия</h3>
|
|
|
|
<p>У любопытного читателя может возникнуть вопрос: хорошо, мы поняли интенцию относительно соответствия принципов разработки программного обеспечения её целям. При чем же здесь API? Позвольте объяснить.</p>
|
|
|
|
<p>Программирование как трансляция формальной задачи с человеческого языка на машинный — это первый (скорее даже, нулевой) уровень восприятия процесса разработки. Нет алгоритма, который был бы как остров, сам по себе; каждый алгоритм откуда-то берётся и для чего-то предназначен. Иными словами, существует некоторый контекст, в котором будет работать этот небольшой фрагмент программного кода. В первую очередь таким контекстом является программный продукт целиком, в составе которого имплементируется алгоритм.</p>
|
|
|
|
<p>Чем больше, масштабнее и сложнее программный продукт, тем менее важна собственно алгоритмизация конкретной подзадачи и тем более важна архитектура продукта в целом. Правильно ли произведена декомпозиция? Будет ли этот код понятен другому разработчику через несколько лет? Можно ли потом эффективно расширять и поддерживать этот код?</p>
|
|
|
|
<p>Это следующий уровень восприятия проекта: понимание <em>контекста</em> существования конкретного фрагмента кода. На этом уровне появляется понимание того, что интерфейс должен быть сначала спроектирован, и что с ним надо «переспать», т.е. всесторонне обдумать; что «переключение контекстов» — дорогая операция, и что браться за другую задачу в другом проекте, на самом деле, очень сложно, даже если требуется написать всего 10 строчек кода.</p>
|
|
|
|
<p>Но и этот уровень не последний. Помимо контекста внутри программного продукта существует также и ещё более широкий контекст — внешний, в котором живёт сам продукт. Связи внешнего мира с внутренним миром разработки широки и разнообразны: внешний мир определяет функциональность продукта, внешний мир диктует свои правила. Помимо прямой связи существует и обратная: кто-то во внешнем мире будет пользоваться вашим продуктом и, возможно, вашим программным кодом — т.е. вы сами привносите нечто во внешний контекст, и это «нечто» какое-то время будет там жить.</p>
|
|
|
|
<p>API - это ничто иное, как средство связи контекстов. Например, когда вы реализуете какой-то алгоритм — вы используете API языка программирования. Компилятор языка программирования, в свою очередь, использует API команд процессора. Это API, в случае CISC-процессоров, в свою очередь построено поверх ещё более низкоуровнего API микрокоманд; и так далее. Только представьте себе, какое огромное количество API разных уровней должно отработать — и отработать правильно! — для того, чтобы вы смогли, скажем, увидеть страничку в браузере: тут и весь сетевой стек с его 6-7 уровнями абстракции; и серверное ПО — веб-сервер, генерация страницы, база данных, кэш; и клиентское ПО — браузер — имплементирующее буквально десятки разных API.</p>
|
|
|
|
<p>В современном мире роль API трудно преувеличить, и она продолжает расти. Всё новые и новые устройства — от часов и розеток до кораблей и ветряных турбин — включаются в «игру контекстов» и обзаводятся своим API. При этом, к сожалению, качество этих API всё больше оставляет желать лучшего.</p>
|
|
|
|
<p>В отличие от прикладного кода, которым будут пользоваться только ваши коллеги, разработка API подразумевает, что вашими программными интерфейсами будут пользоваться другие разработчики. Именно поэтому в разработке API проблемы, описанные в предыдущем разделе, стоят особенно остро. Хорошее API должно одновременно быть и качественно реализовано, и качественно спроектировано с точки зрения предоставляемых интерфейсов.</p>
|
|
|
|
<p>Цель написания этой книги - посмотреть на разработку программного обеспечения под другим углом, дополнить известные принципы разработки программных продуктов принципами разработки API.</p>
|
|
|
|
<h3>Об относительности критериев качества</h3>
|
|
<p>Несмотря на всё написанное выше, я вовсе не призываю вас подходить к каждой задаче с дополнительными наборами требований. Вовсе даже наоборот: я скорее призываю эти требования сокращать.</p>
|
|
|
|
<p>У каждого инструмента, включая и API, есть своя область применимости, и это важно понимать. Допустим, вы пишете, скажем, скрипт импорта данных из Википедии для того, чтобы залить в базу данных вашего продукта какие-то тестовые данные, и вы собираетесь воспользоваться им один раз. В этом случае, очевидно, совершенно неважно, правильная ли у него архитектура и понятен ли его API. Нет, разумеется, вы можете спроектировать и реализовать этот скрипт «правильно», гибко и универсально, чтобы вы могли им гордиться. Но зачем?</p>
|
|
|
|
<p>Любая красота сложна. Правильная архитектура требует больших затрат времени и сил, часто — неоднократного полного переписывания базовых модулей. Правильное API требует времени и сил ещё больше.</p>
|
|
|
|
<p>Понимать, где ваши усилия полезнее и нужнее — пожалуй, не менее важно, чем их (усилий) приложение.</p>
|
|
|
|
<p>Эти вещи кажутся очевидными на простых примерах; со сложными же проектами, поверьте, то же самое. Достоинства и недостатки любой методологии, как правило, отлично известны, однако редко когда выбор конкретного фреймворка, паттерна или подхода опирается на анализ соответствия их достоинств и недостатков проекту.</p>
|
|
|
|
<p>Есть ещё один момент, который хочется подчеркнуть: инструменты должны соответствовать конкретным обстоятельствам — предметной области, планируемому жизненному циклу продукта и, что немаловажно, компетенциям команды. Во многих случаях неидеальный инструмент, с которым команда привыкла работать, предпочтительнее неидеального, который придётся осваивать по ходу.</p>
|
|
|
|
<h3>API как универсальный паттерн</h3>
|
|
|
|
<p>На фоне сказанного выше этот заголовок должен вызывать некоторые сомнения. Универсального паттерна не существует, разве нет?</p>
|
|
|
|
<p>Тем не менее, API таковым паттерном является. Чтобы пояснить этот парадокс, давайте для начала поймём, что такое API.</p>
|
|
|
|
<p>В первом разделе мы говорили о том, что API — это связь, интерфейс взаимодействия двух программируемых контекстов. Вспомним, что мы живём в мире, очень сильно завязанном на правильное взаимодействие целых иерархий таких контекстов. Что же тогда самое важное, что такое API по смыслу?</p>
|
|
|
|
<p>API — это обязательство. Обязательство поддерживать связь контекстов. Всё остальное вторично.</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>
|