A few days ago there was a post on the SproutCore Google Group by an individual asking if his code could be reviewed by people in the community. Given the request, I checked out the code from github and began to go through it. In a nutshell, I was reviewing a small SproutCore application that allows you to simply log in and log out of the application. The application makes use of Ki, a statechart framework, in order to keep track of what state the application is currently in. In addition, there are two custom views. One view represents a toolbar displaying whether you are logged in or logged out, and another view that represents a login form. The login form view also contains logic that will fake the log in procedure.

When reviewing both the toolbar view and the login form view, I noticed that both of them have a mouseDown method that, based on the view’s current state, would make a direct call to methods on the application’s statechart, like so (various code removed for clarity):

Login.ToolbarView = SC.View.extend({

  ...

  mouseDown: function(evt) {
    var target = SC.CoreQuery(evt.target);
    var state = this.state;
    if (target.attr('id') === 'login') {
      if (state === 'loggedOut') {
        Login.statechart.beginLogin();
      } else if (state === 'loginIn') {
        // nothing to do here
      } else if (state === 'loggedIn') {
        Login.statechart.logout();
      }
    }
  }

});

Typically, any time I see a class that is directly coupled with an object instance (sometimes referred to as hard coupling), that gives me big, red warning signs. Why? Well, for a few primary reasons:

  1. It just makes unit testing a view that more difficult
  2. It makes your view less modular and harder to reuse
  3. The direct coupling increases the chance of hard to pin-down side effects

Whenever I construct a view, or any class for that matter, I strive heavily for loose coupling in order to prevent the above undesired outcomes — and, besides, it’s just a good object oriented practice whether you program in Java, Ruby, and, yes, even JavaScript. So given the toolbar view with its hard coupling, how do we go about decoupling the view from the application’s statechart? For that we turn our attention to delegation.

Delegation is a practice that allows an object to effectively delegate some task or tasks to another object but without having to directly know what that object is. During the execution of your application you can then construct your object where you provide your view (or what have you) an object that will be delegated to.

Alright. Now that we know about delegation, how do we actually apply it to the toolbar view within SproutCore? There are two primary ways we can go about applying delegation to the view. The first pattern is the target-action pair. The second pattern is to create a delegate mixin. Let’s explore how each works.

The Target-Action Pattern

The target-action pattern makes use of two concepts (surprise, surprise): 1) A target that is to receive an action; and 2) the action that will be invoked on the target. That seems simple enough, and it is as you’ll find out.

Because the toolbar view should not know nor care about how the application actually goes about logging and logging out, we want to delegate those actions to some target whenever the view is clicked based on the view’s current state. Let’s now go ahead and update the toolbar view to make use of this target and action (code simplified for clarity):

Login.ToolbarView = SC.View.extend(SC.DelegateSupport, {

  logoutTarget: null,
  logoutAction: null,

  loginTarget: null,
  loginAction: null,

  ...

  mouseDown: function(evt) {
    var target = SC.CoreQuery(evt.target), action;
    var state = this.state;

    if (target.attr('id') === 'login') {
      if (state === 'loggedOut') {
        target = this.get('loginTarget') || null;
        action = this.get('loginAction');
      } else if (state === 'loggedIn') {
        target = this.get('logoutTarget') || null;
        action = this.get('logoutAction');
      }

      this.invokeDelegateMethod(target, action);
    }
  }

});

Above you’ll notice a few changes. First, the toolbar view now has four new properties: logoutTarget, logoutAction, loginTarget, and loginAction. These properties are what get assigned to specify the targets and actions to handle the actual logging in and logging out. The second change is to the mouseDown method. mouseDown will get the target and action pair based on the view’s current state and then use the SC.DelegateSupport* mixin’s invokeDelegateMethod method in order to invoke the action on the target. Note that if the value of target is null then invokeDelegateMethod will default the target to the view itself.

Great. With the toolbar view updated, let’s look at how we would apply this in the sample application. In the sample app there is a main page that contains the toolbar view, so let’s update the main page to take advantage of the view’s target and action properties (some code removed for clarity):

Login.mainPage = SC.Page.design({

  ...

  mainPane: SC.MainPane.design({
   
    childViews: 'toolbar mainView'.w(),
    
    toolbar: Login.Toolbar.design({
      title: "Login States",
      logoutTarget: Login.statechart,
      logoutAction: 'logout',
      loginTarget: Login.statechart,
      loginAction: 'beginLogin'
    }),
    
    mainView: SC.View.design({ ... })

  }),
  
  loginPane: SC.Pane.design({ ... })

});

With the changes to the main page, we now have the toolbar view linked with the app’s statechart. The statechart is both the target for the login and logout actions, which, in this case, are the statechart’s beginLogin and logout methods, respectively. Pretty easy, right? Yep.

Although we’ve done the trick of decoupling the the view from the statechart, we could further improve how we make use of the target-action pairs since there are a few limitations with the changes we’ve made.

For one, the values that can be assigned to the target properties must be objects, not, say, a property path string due to the way invokeDelegateMethod works. Second, if the a target property is null invokeDelegateMethod will only use the view itself as a default target. It would better (and cooler!) if we could make our solution more generic so that we could supply a property path to the target properties, and if the target happens to be null, to try and make use of the default responder on the pane the view belongs to or the application’s root responder. Thankfully it turns out that’s pretty easy to do in SproutCore.

Instead of using the SC.DelegateSupport‘s invokeDelegateMethod, we will use the root responder (SC.RootResponder) and call its sendAction method, like so:

mouseDown: function(evt) {
  var target = SC.CoreQuery(evt.target), action;
  var state = this.state;
  var rootResponder = this.getPath('pane.rootResponder');

  if (target.attr('id') === 'login') {
    if (state === 'loggedOut') {
      target = this.get('loginTarget') || null;
      action = this.get('loginAction');
    } else if (state === 'loggedIn') {
      target = this.get('logoutTarget') || null;
      action = this.get('logoutAction');
    }

    if (rootResponder) {
      rootResponder.sendAction(action, target, this,
        this.get('pane'));
    } 
  }
}

Without getting too deep into how the root responder’s sendAction works, the method essentially goes through a series of checks try to find a target that can handle the given action. To simplify how the logic actually works, the sendAction will first try the given target passed to the method. If the given target can not handle the action then the given pane will be checked, if one was provided. If the pane itself can’t handle the action then its default responder will be tried. If the pane options are a no go, sendAction will go on to check the current main pane, key pane, in addition to their default responders, and, finally, then check the root responder’s default responder. Like I noted, a bunch of checks are performed. The other nice thing about sendAction is that the supplied target can be a property path string that will get converted into an actual object. Cool. Let’s see how we can apply this back into our main page:

Login.mainPage = SC.Page.design({

  ...

  mainPane: SC.MainPane.design({

    defaultResponder: 'Login.statechart',   

    childViews: 'toolbar mainView'.w(),
    
    toolbar: Login.Toolbar.design({
      title: "Login States",
      logoutAction: 'logout',
      loginAction: 'beginLogin'
    }),
    
    mainView: SC.View.design({ ... })

  }),
  
  loginPane: SC.Pane.design({ ... })

});

The target properties on the toolbar view can been removed and we added the property defaultResponder on to the main pane that has been assigned a property path to the app’s statechart. Again, because the given target supplied to sendAction will now be null, the method will ultimately use the main pane’s default responder as the default target. Neat’o!

We got some good coverage on the target-action pattern, now let’s see how the delegate mixin pattern works.

The Delegate Mixin Pattern

The delegate mixin pattern differs from the target-action pattern in that we make use of a mixin that gets applied to some object that we want our view to delegate to. The idea seems simple enough. The first step then is that we need to define our mixin, which will be the following:

Login.ToolbarViewDelegate = {

  isToolbarViewDelegate: YES,

  toolbarViewLogout: function() { },

  toolbarViewLogin: function() { }

};

The first thing to note is the name of the mixin: ToolbarViewDelegate. This is just a convention that helps clarify that the mixin is used by the toolbar view in order to delegate specific tasks to an object.

The next thing to observe is the isToolbarViewDelegate property that has a default value of YES (i.e. a value of true). When you add a mixin to a class, the mixin’s type, so to speak, is not retained. Rather, SproutCore simply takes all the properties from the mixin and directly applies it to the given class. This means that without some property indicating that the class does indeed have the mixin applied, there is then no way to determine so. Therefore we use isToolbarViewDelegate in order to support such a check. This essentially allows for duck typing.

The final part of the delegate mixin are the methods themselves. We see that the mixin has two methods: toolbarViewLogout and toolbarViewLogin, which will both be called by the toolbar view. Notice the convention used for the naming of the methods. They both have “toolbarView” prefixed to the action. Why? The main reason is to avoid method clobbering when a object, say, mixes in multiple mixins. If we just had methods logout and login, there is a chance another mixin might have a method with the same generic name. Hence, clobbering.

Alright. With our delegate mixin in place, let’s update the toolbar view to make use of it (various code removed for clarity):

Login.ToolbarView = SC.View.extend(
  Login.ToolbarViewDelegate,
  SC.DelegateSupport,
{

  delegate: null,

  toolbarViewDelegate: function() {
    var del = this.get('delegate');
    return this.delegateFor('isToolbarViewDelegate', del);
  }.property('delegate').cacheable(),

  mouseDown: function(evt) {
    var target = SC.CoreQuery(evt.target);
    var state = this.state;
    var del = this.get('toolbarViewDelegate');
    if (target.attr('id') === 'login') {
      if (state === 'loggedOut') {
        del.toolbarViewLogin();
      } else if (state === 'loginIn') {
        // nothing to do here
      } else if (state === 'loggedIn') {
        del.toolbarViewLogout();
      }
    }
  }

}); 

A bunch of changes have been made to the toolbar view. First, the view mixes in both the Login.ToolbarViewDelegate and the SC.DelegateSupport*. The view also has two new properties. A standard property called delegate and a computed property called toolbarViewDelegate. And finally, the mouseDown has been updated in order to get a delegate and call one of its methods based on the view’s current state.

As you may have guessed, the delegate property is what gets assigned the object representing what will be delegated to, which means that the object will have the Login.ToolbarViewDelegate applied to it. But wait a minute. The toolbar view also has the Login.ToolbarViewDelegate. Does that mean the view is a delegate to itself? Well, basically, yes. But why is this? Essentially, by applying the delegate mixin to the view itself, you can make the view the default delegate if no value has been assigned to the view’s delegate property.

The delegate check is performed by the view’s toolbarViewDelegate computed property. toolbarViewDelegate determines what will be the actual delegate with the use of the SC.DelegateSupport‘s delegateFor method. If any of the values supplied to delegateFor have the isToolbarViewDelegate property, then that value will be returned, otherwise the default will be the view itself if it has the isToolbarViewDelegate property, which, in toolbar view’s case, it does.

With the toolbar view now updated, the next question becomes what do we actually apply the delegate mixin to besides the view itself? Anything really, but a standard practice is to apply the mixin to a controller that contains the logic to then make calls to the statechart. So let’s go ahead and do that:

Login.toolbarController = SC.Object.create(
  Login.ToolbarViewDelegate,
{

  toolbarViewLogout: function() {
    Login.statechart.logout();
  },

  toolbarViewLogin: function() {
    Login.statechart.beginLogin();
  }

}); 

With the controller in place, now let’s go back to the main page and once again update the code:

mainPage = SC.Page.design({

  mainPane: SC.MainPane.design({

    defaultResponder: 'Login.statechart',

    ...

    toolbar: Login.Toolbar.design({
      layout: { ... },
      title: "Login States",
      delegate: Login.toolbarController
    }),

    ...
  }),

  ...

}); 

With the controller now assigned to the toolbar view, the view will go ahead and delegate to it any time the mouse down action is fired on the view. Awesome. As one note, you’ll notice that the main pane still has its defaultResponder property assigned to the app’s statechart. For the delegate mixin, the default responder will not be called as a default target like it was when the target-action delegate pattern was used.

Which Pattern to Use?

We’ve gone through a fair amount of detail about how the two delegate patterns work. The question now is which one do you use. If you size up the two patterns against the toolbar view, you’re know doubt leaning towards the target-action pattern since it seems less involved and you get more bang for the buck with respect to making use of default responders. And to be fair, for this case, I would tend towards the target-action pattern. So if that’s the case, then when would you actually want to use the delegate mixin pattern? Let’s look at each and understand when they are useful and when they are not.

With respect to the target-action delegate pattern, it’s more useful when you want to fire simple actions against a target based on some response, say, a view reacts to. The most basic example of this can be seen with the SC.ButtonView. The button view just fires an action against a target and doesn’t care about any finer grained details. Where the target-action pattern is less useful is for cases when you want to delegate fine grained details of a particular task or behavior, which is what the delegate mixin pattern is more suited for.

With the delegate mixin, you’re more focused on allowing a delegate to optionally modify how something works. If a delegate is not supplied to an object that wants to delegate, then that object will continue using default behavior, hence why it’s customary for an object delegator to be its own default delegate. The common example of the delegate mixin pattern is with the SC.CollectionView and the SC.CollectionViewDelegate.

Looking a bit further at the two patterns, another difference is the information passed to a delegate. For the target-action, you’re not really focused on supplying the action with additional info. I mean, you can, but that’s not its sole purpose. The delegate mixin, however, it more suited to supplying the delegate with additional argument in order for the delegate to make a more informed decision about what to do. Again, you can witness this if you look at SC.CollectionViewDelegate. In addition, the delegate mixin provides a cohesive set of methods and properties that are intended to work together in some fashion; using the target-action pattern, not as much since each action and target can been seen as independent — again, not that it has to be.

In should be noted that the dos and don’ts for each of the patterns are just good practices to follow. Once you become familiar and know how to use them, then feel free to do what you see fit as far and bending the way the patterns are supposed to be used.

In any case, I hope this helps you get a good idea of what delegation is, why it’s important to know, and how you can ultimately apply it to your own SproutCore application.

Happy coding!

-FC

Update (Feb 5, 2011): A bit of an oversight on my part with regard to the SC.DelegateSupport mixin. SC.View already mixes in SC.DelegateSupport, so you do not have to explicitly mix it into your view. Mixing in SC.DelegateSupport explicitly won’t do any harm, but it’s not necessary. Despite the oversight, the fact that SC.View does mix in SC.DelegateSupport gives you a pretty good impression of just how important the idea of delegation is in SproutCore :-).

Advertisements