Event Aggregator pattern
The concept is very simple. The Aggregator gathers events from multiple sources into a single object to simplify relations between event publisher and subscriber. The pattern is very similar to the Observer, however, there is a difference worth mentioning. The Event Aggregator sits in the middle between objects, emitting events (publishers) and subscribers (objects wishing to receive events). The Observer is a pattern where the subject holds a list of observers and notifies them on state changes.
You may wonder why I write about these patterns. Let me give you a little bit of a background.
Micro application architecture
At Neoteric, we adopted a Micro Application approach for our frontend applications architecture. Each micro application is an angular application which can exist on its own independently. The Micro App does not depend on any other micro application. These apps are built from angular modules which we try to keep loosely coupled. All micro apps depend on the Application Core which defines the third party dependencies and provides a common set of widgets and services.
That architecture scales very well and promotes code reusability. We have already built a suite of apps. As of now, a NeoB2B suite consists of the following apps: tasks, docs, workflow, and contractors. Each of them is a separate application and we can combine them in any way we want.
The architecture that we developed is interesting on its own, but this is way beyond this blog post. I will probably come back to that later if you are interested.
This short introduction should be enough to understand the problem that we had to solve and the motivation to introduce an Event Aggregator to Application Core.
Notifications mechanisms in angular
Micro applications depend on the Core and can communicate with the Core. However, we need a way to enable this communication without creating too tight a relationship between the Core and the micro app. In angular, there are a few notifications mechanisms, including scope emits or broadcasts and registering watches on data models.
The scope.$broadcast sends events down through the scope tree. The $emit sends events up through the scope tree.
In most cases $broadcast makes less sense, as only the components below the event publisher in the scope tree will be notified. Using $broadcast on root scope is also not the best idea (performance-wise) as events will go down to all child scopes. In both cases, usually, this is not what you want.
On the other hand, parent-to-child communication can be easily achieved in a different way – for instance:
- by passing a parent’s reference to a child component
- by sharing a service
- implementing an Observer pattern
- watching a shared data model
The child-to-parent communication can be easily done by using a special & binding and passing a callback function or by injecting a parent’s controller instance to child directive with the help of the required attribute.
Do we need events?
In general, events should be avoided. It is more difficult to follow and understand the execution of an event-driven application than of regular synchronous function calls. If you use events, make sure you need them. In most cases, events can be avoided (see the section above). As in angular two-way data binding solves the majority of data synchronization problems, overusing events is a symptom of a problem with the data model.
So when should we use events?
Events work best when you need to pass the information to components that are not directly related or when you simply do not know in advance which components should receive this information. One good example of a global application event is changing the language. There are many components potentially interested in consuming this event and the language switch widget is not related to them at all.
Using a rootScope as an Event Aggregator
For this type of issues in angular apps, we can leverage the root scope which naturally plays an Event Aggregator role. The scope object has all the necessary methods and root scope is available globally. So far so good. However, there are a few problems with this approach, which we should at least be aware of:
- Potential memory leaks. The event listener bound to the rootScope will not automatically unbind itself when the directive scope is destroyed. Hence, you should explicitly unbind the listener.
- Event names collisions. For small apps, this is usually not a problem, for bigger apps, it might be a problem. You need to come up with a good naming convention to avoid names overlapping and easily identify the event source.
- Polluting root scope. Passing too many events through root scope degrades the performance and potentially has a side effect on the other micro apps. We would like to keep micro apps in isolation from each other and using root scope as a communication channel inside micro app breaks this rule.
Recently, I have come up with a simple idea inspired by the Backbone/Marionette to implement an Event Aggregator factory and create as many aggregators as needed. The implementation is pretty straightforward. I decided to decorate the scope object, precisely its two methods: $on and $broadcast. Why $broadcast? Because the scope object created by $rootScope.$new has a parent rootScope and I don’t want my events to interfere with rootScope.
Here is the implementation of basic Event Aggregator:
In practice, I would need a single global Event Aggregator which will be available for all micro apps and will facilitate the communication between Core and Micro apps (as well as app-to-app if needed). Additionally, one or more aggregators for micro apps. I can easily achieve that by creating a service which is an instance of Event Aggregator, take a look:
This approach has several benefits. I am not polluting roots scope anymore. Micro apps are well separated and, last but not least, I don’t have to remember to deregister an event handler bound to root Scope – it happens automatically when the scope of a subscriber is destroyed.