Recently, during the development of the client part of the web application, it became necessary to determine the labels of the advertising campaign that led the user to the site.
Initially, the task seemed very linear - to look here, then there, to take something in priority and pass on. But in the process it turned out that some labels may appear asynchronously, and, therefore, they need to be able to "wait."
Complicating the task led to the desire to simplify the code involved in its solution.
')
Using the example of solving such a problem, this post tries to show how design
and over engineering can help you in developing flexible and easily changeable applications.
Conditions of the problem
The tags that we are looking for are
UTM tags.
Sources of tags are those places where we are looking for tags. By condition, they have priority search. In our problem, the sources are:
- GET request parameters;
- cookies;
- HTTP request headers of type document.referrer.
The algorithm for reading tags depends on the priority of the source and initially looks like this:

Decision
In the developed application, the
dependency injection pattern is used, so the application components, with some reservations, are presented as services. The following tasks will be involved in solving the problem:
- cookies repository;
- HTTP request data repository (GET parameters, document.location.pathname, etc.);
- and, directly, the service of obtaining labels.
Let's call them
cookies ,
query and
utm , respectively.
If the functionality of cookies and query is clear enough, then what about utm? Is it worth it to implement the algorithm for obtaining labels directly inside utm or to abstract, and to bring the implementation of the algorithm out of the service?
The algorithm can be greatly simplified if:
- introduce the concept of an abstract data source with a single interface;
- divide labels into required and optional.

Accordingly, the sources should be our cookies and query services.
But what if the cookie getter cookie values are called getCookie, and the query getter parameter has getQueryParameter?
In other words, we need to use the
adapter pattern .
As a result, the following wrapper services will appear:
- cookies-utm-adapter - performs a search and, if necessary, decoding a stored label in cookies;
- query-utm-adapter - performs a search in the GET parameters;
- query-utm-adapter-backside - performs indirect search for an HTTP request.
The utm service will have an addSource method that accepts an object with a label source interface and priority for that source. Also, the service constructor takes in a javascript object, which expands the default settings of the service.
This diagram shows the connection of the utm service with the cookies repository:

In the services config it would all look like this:
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]] ]
* in this example, the config is presented on coffeescript, the symbols @ signify a link to the service instance. A similar format of configs is used in the Symfony Dependency Injection component.Imagine that we all implemented and coded the whole thing. Now everything works, but it works synchronously. How to deal with the "waiting" of some tags?
Async.js + jQuery.Deferred
A few words about the implementation.
The selected structural solution has two logical levels:
- the logic of polling sources inside the utm service;
- adapter logic, which can be very different - from a simple reference to the embedded repository, to the most sophisticated ways of obtaining and formatting data on tags.
To implement an asynchronous solution, we need to make changes, at least at the first level.
At the utm service level, we will change the implementation of the cycle of interviewing sources:
- let's make the cycle asynchronous using the async.js library, which implements the basic methods for collections in asynchronous style;
- in response to the get method call from the adapter, we will expect either the value of the label, or a promise for its value (promise) - in the case when the adapter needs to wait for it or request it somewhere. Processing the result is wrapped in the $ .when method and, if successfully resolved, calls the loopback function from async.
At the adapter level, we will add a promise return for those tags that are worth the wait. For example, the __utmz cookie, which is set after initializing the ga.js library (analytics.js) and may allow you to define some labels.
Conclusion
Sometimes, at the beginning of designing a problem to be solved, we do not always imagine its complexity and all the pitfalls. And at such moments you want to make it as simple as possible, but the excessive fragmentation of the code suggests over engineering and is generally a little scary. In this case, the "correctness" of the design has borne fruit and greatly simplified further modifications of the application logic.
Thanks for attention! I hope someone will help.