A better observer pattern in Ember

This is a cross posting. Original source.

Recently I was working on an app which was going swimmingly till I realized that I had painted myself into a code design corner. Luckily after a little thought the Object Oriented Programming pattern Observer came to the rescue.

First I’ll tell you how I got myself stuck into an Ember conundrum and then how I bailed myself out.

I had a page that showed a list in one section and then a second list in another section. It made sense to separate each of these sections into their own components. The data being presented were two separate things unrelated.

<section>
  <EntityList />
</section>
<section>
  <EventList />
</section>

Imagine these components being deeply nested or complex. Then imagine getting the new requirement that a button deep in the EntryList has a side effect on the serve requiring the EventList to refresh.

Thus the painted corner; how do we communicate to unrelated components that they need to refresh without the famed [[prop-drilling problem|Message Chains]]. The answer was to create a delegate using an [[observer pattern|Observer pattern - Wikipedia]].

:information_source: Tip: This is different then the observers the Ember community is very against. In this article observer pattern is referring to the OOP design pattern not computed property observers.

In ember this can be implemented as a service. The service would allow components to subscribe to the service and other components could tell the service to publish an event. But it is implemented using pure functions instead of events. That allows a level of analysis that the typical Evented pattern lacks.

What this looks like is the EventList in this example would subscribe to the service:

export default class EventList extends Component {
  @service myObserver;
  @restartableTask *loadData () { … }
  constructor() {
    super(...arguments);
    this.loadData.perform();
    this.myObserver.subscribe(
      this,
      () => this.loadData.perform()
    );
  }
  willDestroy() {
    this.myObserver.unsubscribe(this);
    super.willDestroy(...arguments);
  }
}

And another component can simply this.myObserver.notify() to have the callback(s) execute.

Implementation

import Service from '@ember/service';

const observers = new Set();
const subscribers = new WeakMap();

export default class MyObserver extends Service {
  subscribe(key, callback) {
    let callbacks = subscribers.get(key) ?? new Set();
    callbacks.add(callback);
    observers.add(callbacks);
    subscribers.set(key, callbacks);
  }

  unsubscribe(key) {
    let callbacks = subscribers.get(key);
    observers.delete(callbacks);
    subscribers.delete(key);
  }

  notify() {
    observers.forEach(ob => ob.forEach(cb => cb()));
  }
}

Demo

4 Likes

Thanks for writing this up!

I’d add that while this pattern is very useful and works quite well, it’s often worth going a step further and thinking about how you can actually just turn the trigger for all those callbacks into a state change. That way, instead of needing to manage setting up and tearing down the callbacks, the system can simply “automatically” re-derive state as needed. For example, you can imagine that instead of doing loadData.perform, you can implement the “resource” pattern (or actually build a Resource!), such that what the action on the button does is (for example) determine a new value to fetch, or indicate that refetching needs to happen, or similar, and set that as @tracked state in the root.

Then there’s no need for callback registration; consumers of the service which use that tracked state will get updated by the reactivity layer “automatically”. It means you have less bookkeeping to do, both in the setup/teardown side of things but also just in the “make sure everything gets called/doesn’t get called” appropriately. You hand that off to the runtime and it all Just Works™.

The biggest upside to the pattern you outlined is its flexibility: downstream consumers can do anything in response to those callbacks. That’s also its biggest downside, though: it is exactly how you end up with the “our list of messages in the main messages view is out of sync with the list of messages in the popover” problem. Taking the approach I outlined inverts that: your downstreams have to be (purely-functional-ish) derived state from whatever state you define in the root, which substantially decreases the flexibility of the system. However, it also means that you end up with a single source of truth, with all changes to it happening in a single place. No more sync bugs!

(I personally strongly prefer the “update a single piece of root state and let everything else just derive from it” approach, but I’m even more interested in folks clearly understanding the options available and the tradeoffs between them!)

2 Likes

Sure, could have an ever increasing counter. Much like how @tracked is implemented internally.

In either case (callbacks or derived state) the pattern stays the same. I think which solution to use depends on the consumers. For things that will be driven by the template use the resource/derived state implementation. For pure JavaScript situations use callbacks.

As an aside one could go a further step and merge the two. Make a helper that will perform the JavaScript part and the helper recomputes when the derived state changes.

Many options.

P.S. I see performing an ember-concurrency task as the same as setting an @tracked because it is essentially derived state used in a template. The specific reason this pattern was chosen in my contrived scenario was because in the case I needed it the component responsible for derived state was the EventList component. Something the EntityList would know nothing about but both would share a notion that there was a ephemeral relationship between them. A relationship loosely tied to observation. Thus both could just know about the observer service without knowing about each other. Also this pattern shows how you can have event based ideas without relying on the non-static analyzable Evented.

At the end of the day, there are only 2 primary patterns for binding to an async load:

  • Use a backing property (_someProp) and a getter (someProp) and have the getter fire an async task that replaces the backing property and fires the binding update - useful for when loading state is not required, you just want the value to update
  • Have the getter(someProp) return some variation of a proxy, explicit (as in the AsyncData class in the link mentioned above, where your template “knows” it’s a proxy, either by helper or directly), or implicit, as in Ember’s own ObjectProxy-based classes, where a shell version is returned but then is “hydrated” when the load task completes.

Ember itself has gone back and forth and everywhere in between on this (use Observers! don’t use Observers, use proxies! don’t use… etc.) partly because JS itself is evolving. IMHO, event-driven approaches are superfluous when Promises are around - they are a lock-tight version of this event-driven pattern. Events to me are for monitoring complex state machines with unpredictable triggers (like a Hu-mon :slight_smile: ) - for this, it’s “start => unresolved, unresolved => resolved, unresolved => rejected” every time. Take the built-in solution there and wrap the pattern in some kind of proxy (Async Data, ember-concurrency tasks, or roll your own… but around promises).

This has not been my experience. I also think the pattern I was describing is related but different enough to be something different then your async …err… side-effect based getters is likely asking for trouble.

I missed this reply, but a quick comment on “err…side-effect based getters” - all getters are side effecting asynchronously: i.e. by the user (eventually). The side-effecting getter “anti-pattern” is about getters causing changes in their own cycle (whatever that means in the particular context). This is one of those unfortunate cases where a “coined” rule held immutable cuts off options unnecessarily. Every lazy-loading property in existence follows the “err…side-effect based getters” pattern I described.

I think maybe your semantics are ridged in this case. I was referring to the difference between asking for something vs. telling it to do something. If I ask for a value it is a side effect if that also spins off a task in the background. In my experience this causes confusion. A great example is ember-data’s PromiseObject which many find confusing. With Data Down Action Up it is a design attempt to mitigate that confusion.

If the promise like design you have is enough for you then use it.I’m not saying it is wrong just that it come bundled with possible troubles and that is a trade off you have to manage.

I feel like the use case for my original post is orthogonal to your concerns about the lack of a proxy based getter. I’m curious if my sense of invested interest your expressing is all me or if you do feel greatly invested in this debate?