Изначально, задача показалась весьма линейной — посмотреть тут, потом там, взять что-то по приоритету и передать дальше. Но в процессе выяснилось, что некоторые метки могут появляться асинхронно, и, следовательно, их нужно уметь «ждать».
Усложнение задачи привело к желанию упростить код, участвующий в ее решении.
На примере решения такой задачи, данный пост пытается показать, как проектирование и over engineering может помочь вам в разработке гибких и легко изменяемых приложений.
Условия задачи
Метки, которые мы будем искать — это метки UTM.
Источники меток — это те места, в которых мы ищем метки. По условию, они имеют приоритет поиска. В нашей задаче источниками являются:
- параметры GET запроса;
- cookies;
- заголовки HTTP запроса типа document.referrer.
Алгоритм чтения меток зависит от приоритета источника и изначально выглядит так:
Решение
В разрабатываемом приложении используется паттерн внедрения зависимостей, поэтому компоненты приложения, с некоторыми оговорками, представлены как сервисы. В решении задачи будут участвовать следующие:
- репозиторий cookies;
- репозиторий данных HTTP запроса (GET параметры, document.location.pathname и тд);
- и, непосредственно, сам сервис получения меток.
Назовем их соответственно cookies, query и utm.
Если с функциональностью cookies и query все достаточно понятно, то как быть с utm? Стоит ли реализовать алгоритм получения меток непосредственно внутри utm или абстрагироваться, и вынести реализацию алгоритма за пределы сервиса?
Алгоритм можно сильно упростить, если:
- ввести понятие абстрактного источника данных с единым интерфейсом;
- разделить метки на обязательные и опциональные.
Соответственно источниками должны быть наши сервисы cookies и query.
Но как быть, если у cookies геттер значения печеньки называется getCookie, а у query геттер параметра — getQueryParameter?
Другими словами, нам понадобиться использовать паттерн адаптер.
В результате появятся следующие сервисы-обертки:
- cookies-utm-adapter — выполняет поиск и, если нужно, декодирование сохраненной метки в кукисах;
- query-utm-adapter — выполняет поиск в GET параметрах;
- query-utm-adapter-backside — выполняет поиск по косвенным признакам HTTP запроса.
Сервис utm будет иметь метод addSource, который принимает в себя объект с интерфейсом источника меток и приоритетом для этого источника. Так же конструктор сервиса принимает в себя объект javascript, расширяющий дефолтные настройки сервиса.
На данной диаграмме представлена связь сервиса utm с репозиторием cookies:
В конфиге сервисов все это выглядело бы так:
cookies: path: ‘/src/service/cookies/cookies.js’ query: path: ‘/src/service/query/query.js’ cookies-utm-adapter: path: ‘/src/service/utm/cookies-utm-adapter.js’ deps: calls: [[‘setCookieService’, [‘@cookies’]] query-utm-adapter: path: ‘/src/service/utm/query-utm-adapter.js’ deps: calls: [[‘setQueryService’, [‘@query’]] query-utm-adapter-backside: path: ‘src/service/utm/query-utm-adapter-backside.js’ deps: calls: [[‘setQueryService’, [‘@query’]] utm: path: ‘src/service/utm/utm.js’ deps: args: [ parameters: [ name: ‘utm_source’ required: true , name: ‘utm_content’ required: false , name: 'utm_term' required: false ] ] calls: [ [‘addSource’, [‘@cookies-utm-adapter’, 0]], [‘addSource’, [‘@query-utm-adapter’, 1]], [‘addSource’, [‘@query-utm-adapter-backside’, 2]] ]
* в данном примере конфиг представлен на coffeescript, символы @ означают ссылку на инстанс сервиса. Похожий формат конфигов используется в компоненте Symfony Dependency Injection.
Представим, что мы все это дело реализовали и закодили. Теперь все работает, но работает синхронно. Как же быть с «ожиданием» каких-то меток?
Async.js + jQuery.Deferred
Пару слов о реализации.
Выбранное структурное решение имеет два логических уровня:
- логика опроса источников внутри сервиса utm;
- логика адаптеров, которая может быть самой разной — от простого обращения к внедренному репозиторию, до самых изощренных способов получения и форматирования данных о метках.
Для реализации асинхронного решения нам нужно внести изменения, как минимум, на первом уровне.
На уровне сервиса utm мы поменяем реализацию цикла опроса источников:
- цикл сделаем асинхронным при помощи библиотеки async.js, реализующей основные методы для коллекций в асинхронном стиле;
- в ответ на вызов метода get у адаптера будем ожидать либо значение метки, либо обещание на ее значение (promise) — в случае, когда адаптеру нужно ее подождать или где-то запросить. Обработка результата оборачивается в метод $.when и, в случае успешного разрешения, вызывает callback функцию цикла от async.
На уровне адаптеров мы добавим возвращение promise для тех меток, которые стоит ждать. Например, кука __utmz, которая выставляется после инициализации библиотеки ga.js (analytics.js) и может позволить определить некоторые метки.
Заключение
Иногда, в начале проектирования решаемой задачи мы не всегда представляем себе ее сложность и все подводные камни. И вот в такие моменты хочется сделать максимально просто, но чрезмерное дробление кода наводит на мысли об over engineering и вообще немного пугает. В данном случае, «правильность» проектирования принесла свои плоды и значительно упростила дальнейшие модификации логики приложения.
Спасибо за внимание! Надеюсь, кому-нибудь поможет.
ссылка на оригинал статьи http://habrahabr.ru/post/201674/
Добавить комментарий