9

We've got a Web App where we have a lot (>50) of little WebComponents that interact with each other.

To keep everything decoupled, we have as a rule that no component can directly reference another. Instead, components fire events which are then (in the "main" app) wired to call another component's methods.

As time went by more and more components where added and the "main" app file became littered with code chunks looking like this:

buttonsToolbar.addEventListener('request-toggle-contact-form-modal', () => {
  contactForm.toggle()
})

buttonsToolbar.addEventListener('request-toggle-bug-reporter-modal', () => {
  bugReporter.toggle()
})

// ... etc

To ameliorate this we grouped similar functionality together, in a Class, name it something relevant, pass the participating elements when instantiating and handle the "wiring" within the Class, like so:

class Contact {
  constructor(contactForm, bugReporter, buttonsToolbar) {
    this.contactForm = contactForm
    this.bugReporterForm = bugReporterForm
    this.buttonsToolbar = buttonsToolbar

    this.buttonsToolbar
      .addEventListener('request-toggle-contact-form-modal', () => {
        this.toggleContactForm()
      })

    this.buttonsToolbar
      .addEventListener('request-toggle-bug-reporter-modal', () => {
        this.toggleBugReporterForm()
      })
  }

  toggleContactForm() {
    this.contactForm.toggle()
  }

  toggleBugReporterForm() {
    this.bugReporterForm.toggle()
  }
}

and we instantiate like so:

<html>
  <contact-form></contact-form>
  <bug-reporter></bug-reporter>

  <script>
    const contact = new Contact(
      document.querySelector('contact-form'),
      document.querySelector('bug-form')
    )
  </script>
</html>

I'm really weary of introducing patterns of my own, especially ones that aren't really OOP-y since I'm using Classes as mere initialisation containers, for lack of a better word.

Is there a better/more well known defined pattern for handling this type of tasks that I'm missing?

nicholaswmin
  • 1,869
  • 2
  • 18
  • 36
  • 2
    This actually looks mildly awesome. – Robert Harvey Apr 24 '18 at 19:42
  • When I remember correctly, in [this former question of yours](https://softwareengineering.stackexchange.com/questions/369690/how-to-prevent-a-mediator-from-becoming-a-god-object) you already called it a mediator, which is also the pattern name from the GoF book, so this is definitely an OOP pattern. – Doc Brown Apr 24 '18 at 19:47
  • @RobertHarvey Well your word has a lot of weight to me; Would you do it any differently? I'm not sure I'm overthinking this. – nicholaswmin Apr 24 '18 at 19:53
  • @NicholasKyriakides: I think if you interpret this more as a mediator or more as a facade is mainly a question of perspective. And if I got this right, the `toolbar`, for example, does not interact directly with the `contactForm`, but by a method like `toggleContactForm`, which is part of a `Contact` object. This looks definitely like a mediator to me. – Doc Brown Apr 24 '18 at 20:15
  • @DocBrown That's right; The fact that I'm listening for events of components from *within* this mediator is what threw me off in calling it a Mediator in the first place. AFAIK Mediators don't really include the participants within them, instead providing some `publish/subscribe` methods to be called externally. – nicholaswmin Apr 24 '18 at 20:17
  • @NicholasKyriakides: the example in the original Gof book were written in C++ and Java (if I remember correctly) at a time when language support for anonymous methods & callbacks in those 2 language was much less mature than today. You will hardly find a pattern example in that book looks exactly like your Javascript code, expecially when the latter contains callbacks. – Doc Brown Apr 24 '18 at 20:25
  • @DocBrown I forgot to mention that the contraption I described above also allows calling the methods externally, i.e `contact.toggleContactForm()` which further complicates what/which pattern is this. – nicholaswmin Apr 24 '18 at 20:25
  • 2
    Don't overthink this. Your "wiring" class looks SOLID to me, if it works and you are pleased with the name, it should not matter how it is called. – Doc Brown Apr 24 '18 at 20:28
  • @DocBrown Got it. I'm not pleased with the name; What would you call it? `ContactWirer`/`Initializer`? I'm trying to group these classes together under a folder and name them with a certain abbreviation to make their intent clear. – nicholaswmin Apr 24 '18 at 20:29
  • 1
    @NicholasKyriakides: better ask one of your coworkers (who surely knows the system better than me) for a good name, not a stranger like me from the internet. – Doc Brown Apr 24 '18 at 21:23

4 Answers4

6

The code you have is pretty good. The thing that seems a bit off-putting is the initialization code is not part of the object itself. That is, you can instantiate an object, but if you forget to call its wiring class, it's useless.

Consider a Notification Center (aka Event Bus) defined something like this:

class NotificationCenter(){
    constructor(){
        this.dictionary = {}
    }
    register(message, callback){
        if not this.dictionary.contains(message){
            this.dictionary[message] = []
        }
        this.dictionary[message].append(callback)
    }
    notify(message, payload){
        if this.dictionary.contains(message){
            for each callback in this.dictionary[message]{
                callback(payload)
            }
        }
    }
}

This is a DIY multi-dispatch event handler. You would then be able to do your own wiring by simply requiring a NotificationCenter as a constructor argument. Sending messages into it and waiting for it to pass you payloads is the only contact you have with the system, so it's very SOLID.

class Toolbar{
    constructor(notificationCenter){
        this.NC = notificationCenter
        this.NC.register('request-toggle-contact-form-modal', (payload) => {
            this.toggleContactForm(payload)
          }
    }
    toolbarButtonClicked(e){
        this.NC.notify('toolbar-button-click-event', e)
    }
}

Note: I used in-place string literals for keys to be consistent with the style used in the question and for simplicity. This is not advisable due to risk of typos. Instead, consider using an enumeration or string constants.

In the above code, the Toolbar is responsible for letting the NotificationCenter know what type of events it's interested in, and publishing all of its external interactions via the notify method. Any other class interested in the toolbar-button-click-event would simply register for it in its constructor.

Interesting variations on this pattern include:

  • Using multiple NCs to handle different parts of the system
  • Having the Notify method spawn off a thread for each notification, rather than blocking serially
  • Using a priority list rather than a regular list inside the NC to guarantee a partial ordering on which components get notified first
  • Register returning an ID which can be used to Unregister later
  • Skip the message argument and just dispatch based on the message's class/type

Interesting features include:

  • Instrumenting the NC is as easy as registering loggers to print payloads
  • Testing one or more components interacting is simply a matter of instantiating them, adding listeners for the expected results, and sending in messages
  • Adding new components listening for old messages is trivial
  • Adding new components sending messages to old ones is trivial

Interesting gotchas and possible remedies include:

  • Events triggering other events can get confusing.
    • Include a sender ID in the event to pinpoint the source of an unexpected event.
  • Each component has no idea whether any given part of the system is up and running before it receives an event, so early messages may be dropped.
    • This may be handled by the code creating the components sending a 'system ready' message , which of course interested components would need to register for.
  • The event bus creates an implied interface between components, meaning there is no way for the compiler to be sure you've implemented everything you should.
    • The standard arguments between static and dynamic apply here.
  • This approach groups together components, not necessarily behavior. Tracing events through the system may require more work here than the OP's approach. For example, OP could have all of the saving-related listeners set up together and the deleting-related listeners set up together elsewhere.
    • This can be mitigated with good event naming and documentation such as a flow chart. (Yes, the documentation is famously out of step with the code). You could also add pre- and post- catchall handler lists that get all messages and print out who sent what in which order.
Joel Harmon
  • 1,063
  • 7
  • 10
  • This approach seems reminiscent of the Event Bus architecture. The only difference is that your registration is a topic string rather than a message type. The main weakness of using strings is that they are subject to mistyping them--meaning the notification or the listener could be mispelled and it would be hard to debug. – Berin Loritsch May 01 '18 at 12:33
  • As far as I can tell this is a classic example of a *Mediator*. A problem with this approach is that it couples a component with the Event Bus/Mediator. What if I want to move a component, e.g the `buttonsToolbar` to another project that does not use an Event Bus? – nicholaswmin May 01 '18 at 13:41
  • +1 The benefit of the mediator is it allows you to register against strings/enums and have the loose coupling within the class. If you move the wiring outside the object to your main/setup class then it knows about all the objects and could wire them up directly to events/functions without worrying about coupling. @NicholasKyriakides Pick one or the other rather than trying to use both – Ewan May 01 '18 at 16:02
  • With the classic event bus architecture the only coupling is to the message itself. The message is typically an immutable object. The object that sends messages only needs the publisher interface to send the messages. If you use the type of the message object then you only need to publish the message object. If you use a string, you have to supply both the topic string and the message payload (unless the string is the payload). Using strings means you just have to be meticulous about the values on both sides. – Berin Loritsch May 01 '18 at 17:20
  • @NicholasKyriakides What happens if you move your original code to a new solution? You have to bring along your setup class and change it for its new context. Same thing applies to this pattern. – Joel Harmon May 01 '18 at 22:32
  • @BerinLoritsch Dispatching on the type may or may not be desirable. For example, in a web context, it's trivial to pass around an OnClick event, using an external string to indicate who should be interested in it. The alternative is wrapping it before passing, then checking later, which isn't a given in JavaScript. Finally, you may wish to publish multiple different event types to the same set of listeners, and let them figure out whether they're interested in a given message (rather than setting up multiple streams of events with the same objects pub/sub-ing to all of them). – Joel Harmon May 01 '18 at 22:38
  • @JoelHarmon There are cases where components can be moved to other systems where their role doesn't require listening for their events. In those cases a Mediator is not be needed at all. Using it like you propose *requires* that the Mediator is always moved/reused with the components (in your example the `NC` class). If I understood correctly, you propose referencing the Mediator from within the elements. – nicholaswmin May 01 '18 at 22:40
  • @NicholasKyriakides On the one hand, I'd be surprised to see a case where something can be wired up to events like that, but is still substantially useful if you don't. On the other, I didn't intend to suggest this as a panacea. There are both up sides and down sides, and it's a judgement call as to whether it's a net win in your case. If you think it would improve this answer, I can add a section for the trade offs. – Joel Harmon May 01 '18 at 23:34
  • @NicholasKyriakides I have implemented the above pattern using Rx. The component is an observable, observing the input (the bus). The bus observes the components. The component filters the observable using its own criteria. Moving the component into a new project is a non-issue as the message filter criteria can be a lambda or altered to fit the new message type. I have added an answer with some more info. As you say, the component may not need to observe, but it will always be observed. – Sentinel May 03 '18 at 09:47
  • Isn't the main draw back to event bus that you're now managing state in two locations? It only looks DRY, but introduces complexity when you have to go back. – Mirv - Matt May 07 '18 at 11:33
  • @Mirv I'm sorry, I don't see a DRY violation here. Could you be more explicit about where you see one? Also, what introduced complexity are you referring to? – Joel Harmon May 07 '18 at 16:03
  • I think the most succinct way of saying it is that by trying to go DRY, event bus type patterns create another type of statemachine* unnecessarily. There isn't enough chars to go into it ...but there is some great writing by far better minds on the antipattern. – Mirv - Matt May 07 '18 at 16:23
  • @Mirv The only state the bus has is who is listening for what message. The handlers don't necessarily even have the state of which events they've registered to listen for. If you could provide one or two links to examples of the great writing on this, then I would be happy to improve my answer based on them. – Joel Harmon May 07 '18 at 21:22
  • I'm not debating it - just making you aware there are considerations you haven't thought of yet for situations. Check the answer by Greg for an example relates to the dom if you don't want to google the antipattern. – Mirv - Matt May 07 '18 at 22:38
  • @Mirv I've added a section for possible drawbacks to consider. Greg's answer uses the DOM as a tree-based event bus, which can be handy if you're working in an environment that has one but it's also subject to the pitfalls I've included. As far as the DRY and state machine, I have done some reading on it and couldn't find anything that seemed similar to me. Perhaps you could edit in something? – Joel Harmon May 08 '18 at 11:37
  • I'm on cell so might be next week at earliest, I like the edits - the biggest issue relates to nesting & the one you addressed about needing a map to remember/discover what code... to the point where I had to plug in a gem on someone's code because they weren't at their office & have the gem do the lookups. – Mirv - Matt May 08 '18 at 15:01
  • I should say, I still prefer the other answer as it's simpler & elegant - I do you like your break down of the reasons - which is why I just upvoted you. It vaguely reminds me of the martin fowler article ... https://martinfowler.com/eaaDev/EventSourcing.html ... the other thing to note is no listener subscription model should be top level architecture - as its handling of concerns is not great as apps scale. – Mirv - Matt May 08 '18 at 15:13
4

I used to introduce an "event bus" of some sort, and in later years I've started relying more and more on the Document Object Model itself to communicate events for UI code.

In a browser, the DOM is the one dependency that is always present - even during page load. The key is utilizing Custom Events in JavaScript, and relying on event bubbling to communicate those events.

Before people start shouting about "waiting for the document to be ready" before attaching subscribers, the document.documentElement property references the <html> element from the moment JavaScript begins execution, no matter where the script is imported or the order in which it appears in your markup.

This is where you can start listening for events.

It's very common to have a JavaScript component (or widget) live within a certain HTML tag on the page. The "root" element of the component is where you can trigger your bubbling events. Subscribers on the <html> element will receive these notifications just like any other user generated event.

Just some example boiler plate code:

(function (window, document, html) {
    html.addEventListener("custom-event-1", function (event) {
        // ...
    });
    html.addEventListener("custom-event-2", function (event) {
        // ...
    });

    function someOperation() {
        var customData = { ... };
        var event = new CustomEvent("custom-event-3", { detail : customData });

        event.dispatchEvent(componentRootElement);
    }
})(this, this.document, this.document.documentElement);

So the pattern becomes:

  1. Use Custom Events
  2. Subscribe to these events on the document.documentElement property (no need to wait for the document to be ready)
  3. Publish events on a root element for your component, or the document.documentElement.

This should work for both functional and object oriented code bases.

Greg Burghardt
  • 34,276
  • 8
  • 63
  • 114
1

For what it's worth, I am doing something as part of a backend project and have taken a similar approach:

  • My system does not involve widgets (web components) but abstract 'adapters,' concrete implementations of which handle different protocols.
  • A protocol is modeled as a set of possible 'conversations' . The protocol adapter triggers these conversations depending on an incoming event.
  • There is an Event Bus which is basically an Rx Subject.
  • The Event Bus subscribes to the output of all adapters, and all adapters subscribe to the output of the Event Bus.
  • The 'adapter' is modelled as the aggregate stream of all its 'conversations.' A conversation is a stream subscribed to the output of the event bus, generating messages to the event bus, driven by a State Machine.

How I handled your construction/wiring challenges:

  • A protocol (implemented by adapter) defines conversation initiating criteria as filters over the input stream it is subscribed to. In C# these are LINQ queries over streams. In ReactJS these would be .Where or .Filter operators.
  • A conversation decides what is a relevant message using its own filters.
  • In general, anything subscribed to the bus is a stream, and the bus is subscribed to those streams.

The analogy with your toolbar:

  • The toolbar class is a .Map of an input observable (the bus), which is an observable of toolbar events, to which the bus is subscribed
  • An observable of toolbars (if you multiple sub-toolbars) means that you may have multiple observables, so your toolbar is an observable of observables. These would be RxJs .Merge'd into a single output to the bus.

Issues you may face:

  • Ensuring that events are not cyclical and hang the process.
  • Concurrency (don't know if this is relevant for WebComponents): for asynchronous operations or operations that may be long running, your event handler may block the observable thread if not run as a background task. RxJS schedulers can address this (by default you can .ObserveOn a default scheduler for all bus subscriptions, for example)
  • More complex scenarios that cannot be modeled without some notion of a conversation (eg: handling an event by sending a message and waiting for a response, which is itself an event). In this case, a state machine is useful to dynamically specify which events you want to handle (conversation in my model). I do this my having the conversation stream .filter according to state (actually, the implementation is more functional - the conversation is a flat map of observables from an observable of state change events ).

So, in summary, you can look at your whole problem domain as observables, or 'functionally'/'declaratively' and consider your webcomponents as event streams, as observables, derived from the bus (an observable) , to which the bus (an observer) is also subscribed. Instantiation of observables (eg: a new toolbar) is declarative, in that the whole process can be seen as an observable of observables .map'd from the input stream.

Sentinel
  • 432
  • 3
  • 10
1

I use this same style with my video game development with Unity 3D. I create components like Health, Input, Stats, Sound, etc. and add them to an Game Object to build up what that game object is. Unity already has mechanics to add components to game objects. However, what I found was most everyone was querying for components or directly referencing components inside other components (even if they used interfaces it's still more coupled thank I preferred). I wanted components to be able to be created in isolation with zero dependencies of any other components. So I had the components fire events when data changed (specific to the component) and declared methods to basically change data. Then the game object I created a class for and glued up all the component events to other component methods.

The thing I like about this is that to see all the interactions of components for a game object I can just look at this 1 class. It sounds like your Contact class is a lot like my Game Object classes (I name game objects to the object they should be like MainPlayer, Orc, etc).

These classes are sort of manager classes. They themselves don't really have anything except instances of components and the code to hook them up. I'm not sure why you create methods in here that just call other component methods when you could just hook them up directly. The point of this class is really just to organize the event hooking up.

As a side note for my event registrations, I added a filter callback and args callback. When the event is triggered (I made my own custom event class) it'll call filter callback if one exists and if it returns true it'll then move onto the args callback. The point of the filter callback was to give flexibility. An event might trigger for various reasons but I only want to call my hooked up event if a check is true. An example might be an Input component has a OnKeyHit event. If I have a Movement component that has methods like MoveForward() MoveBackward(), etc I can hook up OnKeyHit += MoveForward but obviously I wouldn't want to move forward with any key hit. I'd only want to do it if the key was 'w' key. Since OnKeyHit is filling out args to pass along and one of those is the key that was hit, inside my filter callback I can check that and if 'w' return true, else return false.

For me the subscription for a specific game object manager class looks more like:

input.OnKeyHit.Subscribe(movement.MoveForward, (args) => { return args.Key == 'w' });

Because components can be developed in isolation, multiple programmers could have coded them. With the above example the input coder gave the argument object a variable named Key. However, the Movement component developer may not have used Key (if it needed to look at the args, in this case probably not but in others they use the argument values passed). In order to remove this communication requirement the args callback acts as a mapping for the arguments between components. So the person making this game object manager class is the one who needs to just know the arg variable names between the 2 clients when they go to wire them up and perform the mapping at that point. This method is called after the filter function.

input.OnKeyHit.Subscribe(movement.MoveForward, (args) => { return args.Key == 'w' }, (args) => { args.keyPressed = args.Key });

So in the above situation the Input person named a variable inside the args object 'Key' but the Movement named it 'keyPressed'. This helps further the isolation between components themselves as they are being developed and puts it on the implementer of the manager class to hook up correctly.

user441521
  • 454
  • 5
  • 14