开发者

Better way than mediator pattern for decoupling widgets in this situation?

开发者 https://www.devze.com 2023-01-26 16:53 出处:网络
I’m trying to figure out which pattern to follow in a certain situation. I have web app that consists of several main widgets that interact with each other somehow. The widgets follow the module patt

I’m trying to figure out which pattern to follow in a certain situation. I have web app that consists of several main widgets that interact with each other somehow. The widgets follow the module pattern.

To let code speak:

MyApp.Widgets.MainLeftBox = (function(){
    var self = {};

    self.doSomething = function(){
        var certainState = MyApp.Widgets.MainRightBox.getCertainState();
        if (certainState === 1){
            console.log(‘this action happens now’);
        }
        else {
             console.log(‘this action can’t happen because of a certain state in My.App.Widgets.MainRightBox’);
        }
    } 
    return self;
})();

As you can see, I have tight coupling here. As we all know, tight coupling is evil. (Except when you have found the one and only! ;-))

I know a lot of decoupling can be achieved by following a pub-sub / custom event pattern. But that’s better suited for situations were A starts something and B can react upon. But I have a situation where A starts something independently but needs to check a certain state from B to proceed.

As I’m striving for maintainability, I’m looking for a way out of this hell.

What first came to my mind is the mediator pattern.

But still, my code would look like this:

MyApp.Widgets.MainLeftBox = (function(mediator){
    var self = {};

    self.doSomething = function(){
        var certainState = mediator.getCertainState();
        if (certainState === 1){
            console.log(‘this action happens now’);
        }
        else {
             console.log(‘this action can’t happen because of a certain state in mediator’);
        }
    } 
    return self;
})(MyApp.Mediator);

This is a little better, because Widgets don't communicate directly but indirectly through the mediator.

However, I still feel that I'm doing it wrong and there must be a better way to achieve decoupling the widgets from each other.

EDIT

Let me sum things up so far!

In general, I do like the MVC approach of separating the views! However, think of this example more like complex modules. Those doesn't really have to be "boxes" in a visual sense. It's just easier to describe this way.

Another given fact should be, that A starts an action independently and needs to check for some state then. It can't subscribe to B's state change and provide the action or doesn't. It has to be like A starts it independently and then needs to check a certain state. Think of this as some complex operation that B needs be asked for.

So I came up with a mixture of custom events/callback/mediator approach and there are some things that I really like about it.

1.) A module doesn't know about any other module

2.) A module doesn't know about a mediator neither

3.) A module that depends on some external state does only know that it depends on some external state - not more

4.) A module really doesn't care who will provide this certain state

5.) A module can determine if that certain state has been provided or not

6.) The request pipeline is straight. In other words the module is the starter of this operation. it doesn't just subscribe to a state change event (Remember A starts the action and then needs a state from B (or开发者_C百科 somewhere)

I posted some example code here and also provide a jsFiddle: http://jsfiddle.net/YnFqm/

<html>
<head>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.3/jquery.min.js"></script>
</head>
<body>
<div id="widgetA"></div>
<div id="widgetB"></div>
<script type="text/javascript">

var MyApp = {};

(function (MyApp){

    MyApp.WidgetA = function WidgetA(){

        var self = {}, inner = {}, $self = $(self);

        //init stuff
        inner.$widget = $('#widgetA');
        inner.$button = $('<button>Click Me!</button>')
                            .appendTo(inner.$widget)
                            .click(function(){self.doAction();});


        self.doAction = function(){
            //Needs state from WidgetB to proceed

            /* Tight coupling
            if (MyApp.WidgetB.getState() == 'State A'){
                alert('all fine!');
            }
            else{
                alert("can't proceed because of State in Widget B");
            }
            */

            var state;
            $self.trigger('StateNeeded',function(s){state = s});
            if (state == 'State A'){
                alert('all fine!');
            }
            else{
                alert("can't proceed because of State in Widget B");
            }                   
        };

        return self;
    };

    MyApp.WidgetB = function WidgetB(){

        var self = {}, inner = {};

        //init stuff
        inner.$widget = $('#widgetB');
        inner.$button = $('<button>State A</button>')
                            .appendTo(inner.$widget)
                            .click(function(){
                                var buttonState = inner.$button.text();
                                if (buttonState == 'State A'){
                                    inner.$button.text('State B');
                                }
                                else{
                                    inner.$button.text('State A');
                                }
                            });


        self.getState= function(){
            return inner.$button.text();
        };

        return self;
    };

    MyApp.Mediator = (function(){
        var self = {}, widgetA, widgetB;

        widgetA = new MyApp.WidgetA();
        widgetB = new MyApp.WidgetB();

        $(widgetA).bind('StateNeeded', function(event, callback){
            //Mediator askes Widget for state
            var state = widgetB.getState();
            callback(state);
        });

        return self;
    })();

})(MyApp);

</script>
</body>
</html>


You should checkout a great article about large scale JS apps presented by Addy Osmani Patterns For Large-Scale JavaScript Application Architecture and here is a code sample Essential js design patterns


You can still go with the mediator, but implement your business logic inside it. So, instead of mediator.getCertainState(), have a method mediator.canTakeAction() which knows about the widget(s) to query, and determine if the action is allowed.

This will still end up with a mediator which knows the widgets to query, of course. But since we've offloaded the business logic inside the mediator, I think it is OK for it to know of such things. It may even be the entity that creates these widgets. Alternatively, you can use some sort of registration mechanism where you tell your mediator which widget is used for what role when you create them.


EDIT: Providing an example in the spirit of the given code samples.

MyApp.DeleteCommand=(function(itemsListBox, readOnlyCheckBox) {
  var self = {};

  self.canExecute = function() {
    return (not readOnlyCheckBox.checked()) && (itemsListBox.itemCount() > 0);
  }

  return self;
})(MyApp.Widgets.ItemsList, MyApp.Widgets.ReadOnly);

You can take this two steps further:

  1. Register to state changed events of the source widgets, and update a local cache of the canExecute every time a state change occurs on one of the source widgets.
  2. Also take a reference to a third control (say, to the delete button), and enable or disable the button according to the state.


Assuming I'm understanding the nature of a "box" as a box that's visible on your page, then the box should render a view that represents a state of your application or some piece of it -- the underlying state itself should be maintained by an object that's separate from the view that represents that state in the UI.

So, for example, a box view might render a view of a Person, and the box would be black when the person was sleeping and white when the person was awake. If another box on your was responsible for showing what the Person was eating, then you might want that box to only function when the person was awake. (Good examples are hard and I just woke up. Sorry.)

The point here is that you don't want views interrogating each other -- you want them to care about the state of the underlying object (in this case, a Person). If two views care about the same Person, you can just pass the Person as an argument to both views.

Chances are good that your needs are a tad more complicated :) However, if you can think about the problem in terms of independent views of "stateful objects", rather than two views that need to care directly about each other, I think you'll be better off.


Why can't you use pub-sub model in the following way

  1. LeftBox issues a getStateFromRightBox event.

  2. RightBox has getStateFromRightBox subscriber, which, issues sendStateToLeftBoxAndExecute event with the stateData

  3. LeftBox has a sendStateToLeftBoxAndExecute subscriber which extracts stateData and executes the action conditionally.


A Few Potential Options

I would still recommend using a Mediator -- however, if you're more of an Inheritance fan, you may want to play around with the Template Method, State or Strategy, and Decorator Patterns -- since JavaScript does not have interfaces, these might be useful.

The latter approach might allow you to categorize your procedures into more manageable strategies, however, I'll go on to cover the Mediator since it makes the most sense [to me] in this situation.

You can implement it as EDM (Event-Driven Mediation) or as a classic Mediator:

var iEventHub = function iEventHub() {
  this.on;
  this.fire;
  return this;
};

var iMediator = function iMediator() {
  this.widgetChanged;
  return this;
};

The only thing I can really advise is to break down your procedures to give Mediator a chance to have a say during the process. The mediation could look more like this:

var Mediator = function Mediator() {
  var widgetA = new WidgetA(this)
    , widgetB = new WidgetB(this);

  function widgetChanged(widget) {
    identifyWidget(widget);  // magical widget-identifier

    if (widgetA.hasStarted) widgetB.isReady();
    if (widgetB.isReady) widgetA.proceed("You're proceeding!");

  }

  return this;
};

var WidgetA = function WidgetA(director) {

  function start() {
    director.widgetChanged(this);
  }

  function proceed(message) {
    alert(message);
  }

  this.start = start;
  this.proceed = proceed;

  return this;
};

var WidgetB = function WidgetB(director) {

  function start() {
    this.iDidMyThing = true;
    director.widgetChanged(this);
  }

  function isReady() {
    return iDidMyThing;
  }

  this.iDidMyThing = false;
  this.start = start;
  this.isReady = isReady;

  return this;
};

Basically, WidgetA has to get permission from Mediator to proceed, as Mediator will have the high-level view on state.

With the Classic Mediator, you'll likely still need to call director.widgetChanged(this). However, the beauty of using EDM is that you don't necessarily couple to Mediator, itself, but all modules implement an iEventHub interface or couple to a common hub. Alternatively, you can modify the classic Mediator to aid in Module Authorization by refactoring the widgetChanged method:

// Mediator
function widgetChanged(ACTION, state) {
    var action = actionMap[ACTION || 'NO_ACTION_SPECIFIED'];
    action && action.call && action.call(this, state);
}

// WidgetX
const changes = this.toJSON();
director.widgetChanged('SOMETHING_SPECIFIC_HAPPENED', changes);

I think you're very close -- I hope this helps.

0

精彩评论

暂无评论...
验证码 换一张
取 消