Недавно, в процессе разработки клиентской части веб-приложения, возникла необходимость определять метки рекламной кампании, приведшей пользователя на сайт.
Изначально, задача показалась весьма линейной — посмотреть тут, потом там, взять что-то по приоритету и передать дальше. Но в процессе выяснилось, что некоторые метки могут появляться асинхронно, и, следовательно, их нужно уметь «ждать».
Усложнение задачи привело к желанию упростить код, участвующий в ее решении.
На примере решения такой задачи, данный пост пытается показать, как проектирование и 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 и вообще немного пугает. В данном случае, «правильность» проектирования принесла свои плоды и значительно упростила дальнейшие модификации логики приложения.
Спасибо за внимание! Надеюсь, кому-нибудь поможет.
Автор: gobwas