Abusing the EventAggregator

Hi,

I’m creating an app where my different components needs to communicate a lot with each other, and also get notified when data from the backend have been refreshed etc.

I’ve used the EventAggregator a lot for that, and it works splendid. Then today when I wanted to dump out all messages to see the flow I found this suggestion. What caught my eye was the ending:

But please don’t go overboard on use of the Event Aggregator! It has its uses, but it is not a message queue, it is one-way only, and I’ve seen it abused far too many times as a communications mechanism between units.

I’m not using it as a message queue, but I am using it extensively “as a communications mechanism between units”. For instance:
Component A updates a configuration value. It publishes “configuration changed”, which is picked up by my API class. The API class does a POST request and when it receives a response it publishes “configuration returned from the backend”. Component A, B and C are all listening for that and updates their configurations accordingly.

The same goes for when a button happens to be in a top right component, I publish a message telling the lower left component that the button have been clicked.

These are all “fire and forget” and a way for me to not tie all components together, or using some shared variable being watched all over the place for changes. It makes sense to me, but now I’m worried there are possible draw backs I haven’t thought about.

So: Am I abusing the poor EventAggregator or is this the intended usage?

2 Likes

Maybe the Aurelia Store plugin would be better choice?
http://aurelia.io/docs/plugins/store#what-is-the-state
I guess depending on how you’ve implemented the EventAggregator - and how extensively you have used it - will determine if migrating to the Aurelia Store is feasible.

1 Like

That does seem like a better choice, thanks!

It’ll be a fairly painful transition, as I’ve integrated the EventAggregator in a lot of my code, but it looks like it’d be worth it in the long run.

I don’t need history at all, but it seems simple enough to turn off.

1 Like

If EA gave you well structured code, easy to maintain, easy to test, simple to understand, there is no reason to panic because of some other people’s opinions.

Sure there are users who abuse EA, the statement is true with any lib.

If it doesn’t create trouble for you, you are not abusing it. All you need is to trust yourself.

My personal experience is to not adopt a tool with more features (plus more complexity) unless you have some existing pain to kill. Otherwise you would bring in more pains. Just my 2c.

6 Likes

One more comfort: @EisenbergEffect once told me he mainly uses EA + DI, even in complex apps, because that combo is simple but powerful enough to get most jobs done.

Simplicity unlocks flexibility, complexity creates barriers.

6 Likes

A typical issue that may appear with too massive use of EA is as an example that CompA fires an event, B listens to and fires another one where due to either mistake or misdesign A listens to and fires back the first event. So you end up in an event loop. Another one is where you depend on order of events. Imagine a ValidationOk and SaveEvent. You’d expect the prior to come first but what if it happens the way around due to shifting code or delays in validation because it’s suddenly async?

The trouble here is not the case, that can happen with every approach, but it’s the painful debugging you’d have.

So as @huochunpeng said, if you don’t feel any of these issues and didn’t need to invent weird mechanisms to circumvent these, you’re most probably good. The use case itself of lots of inner-comp communication though is for itself a perfect candidate for a state management lib like the store plugin. I’d advise to create a minimalistic sample app with common cases of your main app and try how the store works with that to get a better feeling. Then, if things go south with EA, you’re prepped :wink:

3 Likes

Thanks everyone for the good input. After reading up a bit on the store documentation have a I reached the conclusion I’ll put on my brave face and continue with EA for the time being. There are plenty of things I don’t need in the store (at least for now, such as the history). Also having to fetch the initial config from the backend, so it’s not existing at the time of main running but asynchronously fetched, and the isolated communication between compA and compB, and compB and compC respectively makes it feels like it’d risk complicating things for me rather than make it easier.

@zewa666 the event loop is a good example of dangers of an event driven app, but don’t you have the same danger with the store? IE you can end up in a store loop just as easy as in an event loop?

That said, I think I’ll have use for the plugin later on for another part I haven’t gotten into yet, and no matter it seems like a really useful plugin that I’ll get use for in the future.

1 Like

I think from reading your replies you’re doing the right thing. Trying out new things in existing apps is always a challenge and if you have the opportunity to test it out on a smaller scale it’s certainly the better way.

Yes you can run into the same issue of looping, as mentioned with every approach. The difference though is that the dispatch is always deterministic, speak easier to debug. You exactly know where it got dispatched and using e.g Redux devtools the trackability is even easier.

EA is all great and fine as long as you know what you’re doing. I just got bitten too often in larger projects with multiple devs that things got so unpredictable. So you see I’m definitely biased :wink:

2 Likes

In my experience, the main issue with too much EA is it can be a little harder to track all the things that respond to event ‘SomeEventHere’. The components are so decoupled that it is usually not possible for a compiler or code editor to help you navigate across the communication boundary of EA. The only alternative I can think of is to put event subscribing and publishing into some kind of shared service class that is injected into each component/module. Then you you can track the coupling by who uses that service class. This is more self-documenting, but it is also more code and more complicated. In the case of EA, a simple “find” in the editor would probably work if you use very unique event names. In general, I think you are using it correctly. The question is, would someone coming in to work on the project in a year be able to follow it easily? Is that an issue you need to be concerned with? How easy would it be to break the communication boundary given the way you implemented it. These questions just help you think about the trade-offs you need to evaluate for yourself and your project.

3 Likes

I think this is actually what most users end up with.

I don’t think users can get any better than this. If you compare this approach to any other offering on the market: redux, rxjs… The one with EA + DI is the thinest you can get, while providing code isolation.

Aurelia has this interesting feature: it can directly observe mutation on a PLAIN JavaScript Object. This means you can simplify mutate the properties on your service class instance. All Aurelia components then can magically react to those mutation without additional glue code.

Aurelia is reactive by itself, so you can enjoy reactive programming without bring in additional reactive lib.

Aurelia’s approach suddenly made any object observable, not just your objects, but also 3rd party objects. This is very different from using other reactive lib where you have to wrap values to be observable. Once users realised this, it will open up the design choices. For example, in one of my production app, I implemented undo/redo stacks using simply plain js array with push and pop. I don’t need any help from 3rd-party lib, and my code is simple to reason about.

3 Likes

I think you made a really great point with reactivity out-of-the-box. This is what enables you to concentrate on your tasks. It also allows you to chose between mutations and immutable approaches. You can pick design patterns such as pubsub, cqrs or really a mix of both. The point is Aurelia won’t force you in any direction. There is nothing imminently better, you can do everything with either approach. All these patterns aren’t new and came a long way from classic backend development be it MVC, MVVM or whatsoever so it’s always up to the following decision I’d say:

If you know what you’re doing and able to design well abstracted architectures, understandable for your co-workers, and you haven’t had any serious issues then by god stay with what youre using. If you feel you need a more rigid approach, reducing the surface of manipulations, thus limit the scope where things can go south try out a state management lib. Be it the store plugin, Redux or really whatever else.

Oh and since perhaps the store plugin is too much, perhaps try just to leverage it’s basic fundamentals of a Behaviorsubject and scratch the rest. It’s a simple class at the end that can be extended/modified according to your needs. Don’t need the dispatch queue? Just expose a new method acting directly on the BehaviorSubject.

4 Likes

My reality is probably hurt too much by redux with its endless boilerplate and strange unit tests in 2015 :joy: That’s the only reason stopping me from trying out aurelia-store (still want to try it out some days).

2 Likes

My take on this is that if your components subscribe to the provider of the events, then it’s a perfectly valid model, primarily because it’s a simple event listener pattern. If however they have no idea which component is generating the event, then this is when we quickly hit the ‘abuse’ situation I discussed.
If you end up creating two instances of your source component, how will subscribers know which one generated an event? If information is in the event data, and you guarantee through an agreed rule that you will always have unique instances, that’s fine, but it is also not intuitive for other developers, and begs the question as to whether there is a better mechanism.
In your example “configuration changed” is a common reason to raise a global event, so absolutely anything that wants to know the config has changed is alerted, and can go and refresh itself in any way it likes. Your configuration component doesn’t care if the observers have heard it or not, as it’s not that component’s responsibility to care for the consumers.
The button click event however is more worrying. To prevent double-clicking, do you disable the button once clicked? Then do you re-enable it after something else has happened? If so, how do you re-enable it? You can’t have a Promise in the click awaiting completion, because you’ve used an EA, and that never returns anything. How about if the subscriber process raises its own EA event to alert the button to enable itself? Aaargh, EA hell! :slight_smile:
We’ve not discussed potential issues related to timings, where an async module load might not have completed loading yet, so events are going nowhere, as that can also also apply to other methods, but debugging this sort of thing in EA patterns can be a nightmare (which is why I also wrote a post on how to trace them).
Nothing is ever ‘wrong’ is code, but can often be ‘unwise’ :smiley:

2 Likes

I’d like to hear your opinion, if I am violating the event-aggregator, too:

Component A is asked to do something, when component B (dialog/modal) fires an event. Event-Aggregator seems fine. Now the new requirement is, that A should only do it’s thing, when a checkbox in component C is toggled.
I can’t help it, but the voice in the back of my head instantly whispered “Switchmap! Store!”, mocking me that I’m not allowed to use any of it. So I came up with the next best thing, that I could think of: Sending the checkbox state via event-aggregator from C, subscribe to it in A. In the callback, I check if there’s a subscription to the events sent by the modal, subscribe to them, and eventually call some async function. It works and I have no problem with testability, because we don’t test. You guys impress me all the time, so I’m reaching out for you: Is a nested event-aggreagator-subscription a good solution / a bad solution? If so, how I could improve on that?

Thanks in advance for your critique!

1 Like

Before you go for creative solutions, let’s take one step back and look at the picture.
The intention of EventAggregation or PubSub in general is to, as the name suggests, subscribe and aggregate a series of events and react to those incoming. An event can be identified by it’s name and may carry some event data. So broken down one could see an event as an action, indicating that something should happen. So far so good.

Now the second part of the story is reacting to those actions and performing a modification of your current applications condition. Let’s search for a funny term and call it state shall we? Based on the state you’d conditionally want to do either A or B, as in your example if the checkbox is turned on or off.
In order for CompA to react to B’s event we just subscribe. But in order to know what the state of C is we’d need that info. So how can we do this?

  • Send the state along with the event as payload (assuming B has ref to C)
  • Move C’s local state into a service class where both A, B and C have access to (causes tight coupling since now neither can work without the service)
  • B instead of notifying A directly, emits another event which C listens to and emits now the original one targeting A and enriching it with the local state info (sounds cumbersome? yes it is :slight_smile: )

I think what you described there goes a bit in the direction of the last option but instead working with ephemeral subscription states which generate nested subscriptions. Is it good or bad? Dunno, depends on your situation. If it works it can’t be totally wrong right? :slight_smile:
Is it easily testable? Hell no that’s quite a lot of workflow to keep in mind in order to be able to test the inner subscription. But if you don’t care about testing the largest part of your problems is unimportant anyways :wink:

What I will say though, is that such an example is a perfectly valid reason why state management exists in itself.

  • Actions are fed both the current state + the payload to work with (both access to data and instruction are available in one place)
  • Actions can be dispatched by whomever, without having to have access to other Components or services (no coupling between multiple components/services)
  • State that has cross-concerns is available for everyone while state only relevant to a component itself is kept locally (think of it like public/private visibility)
  • Determinism is given (your async implementation might compete with another executed just before, whereas in CQRS terms actions get processed within a priority queue so safeguard from unintentional race conditions)

Bottom line, yes you can do that with the approach you did. If it works why not? If you feel every of your colleagues got the intention and you don’t intend to test either in future all good. If it be me though, I’m not sure I’d get that intention 3 months down the road, even if I wrote that part by myself, nor i’d have a test which could act as documentation :wink:

EDIT:
I was a bit unfair with not saying what an alternative approach for EA might be good. In my opinion I’d go with the service class, every component has access to. That way you simplify your events back to B sends to A and inside the subscription you just check the property stored on the service originally from C. Yes it’s more coupling but I personally feel like its way easier to understand. You’re still not guarded from race conditions but at least now have common data shared amongst the components and thus should be able to nail down the root cause more quickly by looking where all the prop assignment happened and eliminating them one by one to figure out the race condition.

3 Likes

As @zewa666 says, there are several ways to approach this, but alarm bells ring for me as soon as I see the words “Component” and “Event Aggregator” in the same sentence. If you mean a system component fine, but if you mean a UI component then you should be looking at binding, not events, and most certainly not Event Aggregators. A component event is supposed to signal to the Controller, and that event lives in the scope of the controller only. What would you shout out to the entire application? “Hey guys, someone clicked a checkbox on a form somewhere in the system!!!” :slight_smile:
I use EA to broadcast loudly information to any listening modules. In your case it sounds like “A.disabled = !C.checked” type of rule, which is unrelated to events. I assume the components are on the same form, so you can bind them directly in the View with zero code if you like.
I’m not saying what you are doing is wrong, but from your description it doesn’t sound like an appropriate use of EAs.

3 Likes

Thank you for the feedback, @zewa666, @jsobell!
I‘m not shure, if I just hijacked this thread and should move the discussion to another thread?

@jsobell
“Hey guys, someone clicked a checkbox on a form somewhere in the system!” - Yes, exactly that.
It feels like abuse and I can tell you, the DX could be better. But I‘m constrainted to not use a store and also – I know it‘s a strawman-argument, but anyways - there‘s quite a lot of articles and books, that propose the EA as one of three ways of passing information between (distant) components in aurelia. In my case 2 of 3 components are siblings on the same page, bindings would be fine, but the third one (the modal) is dynamically created via some toolbar event. To give some context: Via the modal, the user can transition the whole page to another state, like different UI / different validation-rules etc., while the other two components are responsible for listing items and logging what the user did - let‘s call this actions, shall we? :wink:
I think EA is more appropriate than bindings here, but maybe using a service as @zewa666 said, would be best – but it has it‘s caveats too.
Unfortunately, I did not understand, what you mean by „bind them directly in the View with zero code if you like“. This sounds very elegant, can you please give an example for this technique?

@zewa666
I did not understand how e.g. the store-plugin would help with „State that has cross-concerns is available for everyone while state only relevant to a component itself is kept locally“. Could you please elaborate on that?
Actually, we already started using services for managing state. Imho passing down lots and lots of properties via bindings pollutes the interfaces midway when you „drill down“ over long distances. So I actually like using services, when their concerns are clearly separated, but I experienced timing-issues („race-condition“?). For example when „MotherOfAllComponents“ stores some state in a service, that some child component tries to access in it‘s lifecycle, but it‘s not there yet. What do you mean by „eleminating prop assignement“…stateless services / pass references to the view-models to the „stateAggregatingService“?

P.S.: I do care about Testing, but I don‘t see Unit-Testing happening anytime soon. I have yellow-greenish light for introducing Cypress tough. And 3 months down the road I‘ll very likely be like „Nested EA-sub, what was I thinking?“. :slight_smile:

1 Like

Actually exactly the way you described. Without Store or services, everything is local state. As soon as you introduce either of both you’re moving shared state to another place. That doesn’t mean everything is shared state necessary.

I agree that binding drill downs can become quite cumbersome and bloat your markup as you have to pump in multiple attributes. Especially deeper nested components become quite mean that way. Thats why I’ve also mentioend the service approach since you can stop with all the property bindings and instead just DI inject the service and get everything from there.
The timing issue concern is though exactly what I described and you’ve given a nice example. The big difference with Services and the Store is that later exposes a subscribable observer, which turns your code from imperative (manually check before applying action) to reactive (apply action only when subscription gets triggered), just like with an EA. So as you can see the Store is kind of a mix of both approaches and on top of that with a deterministic queue (only one action after another) guarantees execution order, even if async actions are dispatched.

I didn’t want to mock you because of the lack of testing and certainly do understand that there are most of the times reasons out of control why things cant happen. In my experience the solution to this is just get started. Use a short moment of time you have to get the test-infra in place and just start adding a couple of tiny tests which are likely fail-safe. Over time with user stories they will grow (1 after 1) and people will get curious. After the first time a unit test saves your ass everyone will say “oh what a wonderful invention” :slight_smile:

2 Likes

As soon as you introduce either of both you’re moving shared state to another place. That doesn’t mean everything is shared state necessary.

Ah, I think i see. Thank you for clarifying.

I didn’t want to mock you because of the lack of testing and certainly do understand that there are most of the times reasons out of control why things cant happen. In my experience the solution to this is just get started. Use a short moment of time you have to get the test-infra in place and just start adding a couple of tiny tests which are likely fail-safe. Over time with user stories they will grow (1 after 1) and people will get curious. After the first time a unit test saves your ass everyone will say “oh what a wonderful invention”

No problem, I didn´t feel mocked. That´s a good proposal, I´ll start with the value-converters. :slight_smile:

We dropped the double subscription and settled for getting the checkbox-state from a service, that already aggregated some page-wide state. And as it turns out, we can take the Store-Plugin for a ride tomorrow. :+1:

2 Likes