Quantcast
Channel: Testing – Falafel Software Blog
Viewing all articles
Browse latest Browse all 68

Kendo ObservableObject subclasses: prototype vs init

$
0
0

An Unscheduled Detour

I said that my next post was going to be about how to test Kendo DataSources using the techniques I described last time, but I noticed something about the last code sample that I don’t like, so instead this time I’m going to call that out and show how to fix it. I promise I will talk about testing DataSources next time!

So what’s wrong with the last bit of code that I linked to? Well, from a functional point of view, nothing. The code works as expected. However, if you take a look at the state of the object, you might notice something a little odd.AddingViewModel

Take a look… Do you see it yet? Let’s take a look at it again after the test runs.AddingViewModel2

Now do you see it? The original values are still there in the prototype of the object. Now again, functionally, the object works as expected because of how JavaScript identifier resolution works: attempt to resolve the identifier against the object instance, and if that fails, move up the prototype chain and try again. However, once I saw this I knew I had to fix it. For one thing, it’s just not right that data that is meant to belong to individual instances would exist in the prototype. For another, this could actually be the source of a subtle bug. Consider this example, based on the sample I linked in the last blog:

var AddingViewModel = kendo.data.ObservableObject.extend({
  value1: 1,
  value2: 2,
  value3: null,
  button_click: function(e) {
    this.compute();
  },
  compute: function() {
    this.set('value3', this.get('value1') + this.get('value2'));
  }
});

describe('AddingViewModel', function() {
  var testAddingViewModel;
  
  beforeEach(function() {
    testAddingViewModel = new AddingViewModel();
  });
  
  afterEach(function() {
    testAddingViewModel = null;  
  });
  
  it('Deleting a property should completely remove it', function() {
    testAddingViewModel.set('value1', 10);
    delete testAddingViewModel.value1;
    expect(testAddingViewModel.get('value1')).toBe(undefined); // FAIL! It will read from the prototype and return 1!
  });
});

So what is the alternative, you might ask? We want to set up data fields on each instance of the AddingViewModel, rather than the prototype. When extending a Kendo class, the answer is to perform the initialization in the init() function. This is the name that Kendo uses for the class initializer function. In order to support inherited behavior, you must explicitly call the parent type’s initializer. In this case, the parent class is kendo.data.ObservableObject. The init function is an instance method, not a static method, so you must access the class’ prototype in order to call it. In Kendo, the class prototype is aliased as fn. Finally, you want to make sure that you specify that the initializer should use “this” object as the context, so you use either .call() or .apply() to do so. I prefer to use .apply(this, arguments) because it’s generic; it works without requiring special knowledge of the parent class’ initializer parameters (though they must still be provided when creating an instance of the subclass) Let’s take a look:

var AddingViewModel = kendo.data.ObservableObject.extend({
  init: function() {
    kendo.data.ObservableObject.fn.init.apply(this, arguments);
    this.set('value1', 1);
    this.set('value2', 2);
    this.set('value3', null);
  },
  button_click: function(e) {
    this.compute();
  },
  compute: function() {
    this.set('value3', this.get('value1') + this.get('value2'));
  }
});

If you replace the definition of AddingViewModel in the previous snippet with this one, the test will pass, so that solves the problem. If you inspect an instance of AddingViewModel, you will find what you would expect: that the value fields are on the instance, while the functions are in the prototype.

There is also another way you could write the init() override if you prefer. ObservableObject.fn.init() automatically copies all properties of the first argument passed and wraps object and array properties in ObservableObject and ObservableArray instances. So rather than setting their defaults after the fact, you could do something like this:

var AddingViewModel = kendo.data.ObservableObject.extend({
  init: function() {
    kendo.data.ObservableObject.fn.init.call(this, {
      value1: 1,
      value2: 2,
      value3: null
    });
  },
  button_click: function(e) {
    this.compute();
  },
  compute: function() {
    this.set('value3', this.get('value1') + this.get('value2'));
  }
});

But both of these approaches ignore or overwrite a value object that the caller might pass in. So an even better approach would be to create defaults and then allow the caller to override:

var AddingViewModel = kendo.data.ObservableObject.extend({
  init: function(values) {
    var defaultValues = {
      value1: 1,
      value2: 2,
      value3: null
    };
    kendo.data.ObservableObject.fn.init.call(this, $.extend({}, defaultValues, values));
  },
  button_click: function(e) {
    this.compute();
  },
  compute: function() {
    this.set('value3', this.get('value1') + this.get('value2'));
  }
});

For all this plus an updated suite of unit tests, please check this JSBin link.

So to recap, I noticed that setting default data values in the custom ViewModel’s prototype leaves you open to subtle bugs. I recommend that you put functions into the prototype definition and initialize instance-specific defaults in the init() function instead. Next time I promise I will get back on track and talk about testing Kendo DataSources!

The post Kendo ObservableObject subclasses: prototype vs init appeared first on Falafel Software Blog.


Viewing all articles
Browse latest Browse all 68

Trending Articles