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

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

This commit is contained in:
Sergey Konstantinov 2015-07-05 22:04:54 +03:00
parent aaf6bf6d91
commit db105fbd3c

View File

@ -375,6 +375,95 @@
<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>