Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mediate and share data between different modules

I am just trying to get my head around event driven JS, so please bear with me. There are different kinds of modules within my app. Some just encapsulate data, others manage a part of the DOM. Some modules depend on others, sometimes one module depends on the state of multiple other modules, but I don't want them to communicate directly or pass one module to the other just for easy access. I tried to create the simplest scenario possible to illustrate my problem (the actual modules are much more complex of course):

I have a dataModule that just exposes some data:

var dataModule = { data: 3 };

There is a configModule that exposes modifiers for displaying that data:

var configModule = { factor: 2 };

Finally there is a displayModule that combines and renders the data from the two other modules:

var displayModule = {
  display: function(data, factor) {
    console.log(data * factor);
  }
};

I also have a simple implementation of pub-sub, so I could just mediate between the modules like this:

pubsub.subscribe("init", function() {
  displayModule.display(dataModule.data, configModule.factor);
});
pubsub.publish("init"); // output: 6

However this way I seem to end up with a mediator that has to know all of the module-instances explicitly - is there even a way to avoid that? Also I don't know how this would work if there are multiple instances of these modules. What is the best way to avoid global instance-variables? I guess my question is what would be the most flexible way to manage something like that? Am I on the right track, or is this completely wrong? Sorry for not being very precise with my question, I just need someone to push me in the right direction.

like image 427
Đinh Carabus Avatar asked Mar 23 '16 22:03

Đinh Carabus


1 Answers

You are on the right track, I'll try to give you that extra push you're talking about:


It you want loose coupling, pub-sub is a good way to go.

But, you don't really need that "mediator", each module should ideally be autonomous and encapsulate its own logic.

This is done in the following way: each module depends on the pubsub service, subscribe to all relevant events and act upon them. Each module also publishes events which might be relevant to others (code samples in a minute, bear with me).

I think the bit you might be missing here is that modules, which use events, will hardly never be just plain models. They will have some logic in them and can also hold a model (which they update when receiving events).

So instead of a dataModule you are more likely to have a dataLoaderModule which will publish the data model (e.g. {data: 3}), once he finishes loading.

Another great requirement you set is sharing data while avoiding global instance-variables - this is a very important concept and also a step in the right direction. What you miss in your solution for this is - Dependency Injection or at least a module system which allows defining dependencies.

You see, having an event driven application doesn't necessarily mean that every piece of the code should communicate using events. An application configuration model or a utility service is definitely something I would inject (when using DI, like in Angular), require (when using AMD/CommonJS) or import (when using ES6 modules).
(i.e. rather then communicating with a utility using events).

In your example it's unclear whether configModule is a static app configuration or some knob I can tweak from the UI. If it's a static app config - I would inject it.

Now, let's see some examples:


Assuming the following:

  • Instead of a dataModule we have a dataLoaderModule
  • configModule is a static configuration model.
  • We are using AMD modules (and not ES6 modules, which I prefer), since I see you stuck to using only ES5 features (I see no classes or consts).

We would have:

data-loader.js (aka dataLoaderModule)

define(['pubsub'], function (pubsub) {
    // ... load data using some logic...
    // and publish it
    pubsub.publish('data-loaded', {data: 3});
});

configuration.js (aka configModule)

define([], function () {
    return {factor: 2};
});

display.js (aka displayModule)

define(['configuration', 'pubsub'], function (configuration, pubsub) {
    var displayModule = {
        display: function (data, factor) {
            console.log(data * factor);
        }
    };

    pubsub.subscribe('data-loaded', function (data) {
        displayModule.display(data, configuration.factor);
    });
});

That's it.

You will notice that we have no global variables here (not even pubsub), instead we are requiring (or injecting) our dependencies.


Here you might be asking: "and what if I meant for my config to change from the UI?", so let's see that too:

In this case, I rather rename configModule to settingsDisplayModule (following your naming convention).

Also, in a more realistic app, UI modules will usually hold a model, so let's do that too.

And lets also call them "views" instead of "displayModules", and we will have:

data-loader.js (aka dataLoaderModule)

define(['pubsub'], function (pubsub) {
    // ... load data using some logic...
    // and publish it
    pubsub.publish('data-loaded', {data: 3});
});

settings-view.js (aka settingsDisplayModule, aka config)

define(['pubsub'], function (pubsub) {
    var settingsModel = {factor: 2};

    var settingsView = {
        display: function () {
            console.log(settingsModel);

            // and when settings (aka config) changes due to user interaction,
            // we publish the new settings ...
            pubsub.publish('setting-changed', settingsModel);
        }
    };
});

data-view.js (aka displayModule)

define(['pubsub'], function (pubsub) {
    var model = {
        data: null,
        factor: 0
    };

    var view = {
        display: function () {
            if (model.data && model.factor) {
                console.log(model.data * model.factor);
            } else {
               // whatever you do/show when you don't have data
            }
        }
    };

    pubsub.subscribe('data-loaded', function (data) {
        model.data = data;
        view.display();
    });

    pubsub.subscribe('setting-changed', function (settings) {
        model.factor = settings.factor;
        view.display();
    });
});

And that's it.

Hope it helps :)

If not - comment!

like image 193
Yoav Aharoni Avatar answered Oct 16 '22 17:10

Yoav Aharoni