The Mutating Decorator

Jul 19, 2007 09:53

Mutating Decorator

I'm still playing with the Decorator pattern in JavaScript. It's a fascinating pattern in JavaScript because of the ease of modifying the interface of objects. My previous examples have focused on generating new classes based on the interface of the Decorated class. A developer can then append new method variations onto it. That's fine, but the plumbing gets complex and behaves strangely in certain situations. But that got me thinking- is there another way to approach this problem in JavaScript?
Co-Ed Classes

A class prototype is nothing more than a collection of members- methods and variables- that are added to an instance at runtime. Wouldn't it be nice if I could just grab the members of one class and plop them right into another? Why build these complex wrappers that have to do wizardry to talk to the underlying class?

Why not make the class its own Decorator? Dynamically add and remove methods at runtime. It can be done, but is it practical?
Some Reasons Not To

Well, define "practical". There's a performance disadvantage- dynamic binding is expensive. It's not an issue in most cases- not unless you're doing thousands of these. It might be very expensive in situations where your Decoration has a very large number of methods (which it shouldn't anyway!).

It may also make your code less clear- especially if used carelessly. We don't usually mutate the interface to our classes. With my proposed solution, it's possible that some variable x has a method bar that just vanishes. In short, it's completely possible to shoot yourself in the foot with this. Patterns and libraries are not replacements for good programming skills.
HOWTO

Here's what I want to do. I have some instance variable x and some class p. I want to take all the members and implementation of p and embed them in x. I can replace any functionality already in x, but with one caveat. The process needs to be reversible. At some point, I need to be able to undecorate x. That requirement is what makes this an interesting exercise in programming.

I decided to implement decorate and undecorate wrapped in an object literal like so:

MutatingDecorator={
decorate:function(instance, proto){...},
undecorate:function(instance, proto){...}
};
Decorating

Simply decorating is trivial. Given an instance x and a prototype, flip through the instance methods of the prototype and overwrite what's in x. Like so:

var temp = new proto();
for (var key in temp)
{
instance[key] = temp[key];
}
That seems easy, but remember our second design goal- the process needs to be reversible. The simplest solution would be to re-invoke the original constructor of the instance and let it clean up- but that won't get rid of added methods, and it certainly won't work if we decorated an already decorated class. We need some way of tracking history- the old version of the methods need to be remembered.
The delta Object

When decorating, I can track the original version as an object. var info = new Object(); I can give info certain properties- original, to house the original version of the member, source, to know which prototype is responsible for causing this change. Those are the only ones required, although in my implementation I also track name and a primitive boolean- the former for reflection, the latter for potentially changing the behavior for primitive members.

Once I've got the delta built, I just need to track it. Something like:

instance[key] = temp[key]; //swap out the original for the decorated version
instance[key].delta = info; //store the delta-log
Memory

The advantage- and cost- of this is that it remembers the entire history for the method. Let's say I decorate x with prototype p. x.foo is replaced, but x.foo.delta contains the history- the original x.foo. Now, I decorate again, this time with prototype q. x.foo is q's implementation. x.foo.delta.original contains the p version, and x.foo.delta.original.delta.original contains the original x.foo.

This is important for the next step.
Undecorating

Since we're tracking history, this isn't super-difficult, but it does require a little more work than building the decorations. Isn't that how it always goes? It's always more work to take down the holiday decorations than put them up.

Once again, we need an instance and a prototype. We can create an instance of the prototype, and use that to inspect which methods are attached. Our formula is as follows:
  1. Grab a member of the instance corresponding to a method of the prototype
  2. If there's a delta object attached, grab it.
  3. If the delta.source property is equal to this prototype, we've found the revision we're looking for.
    • If there's an original, grab it and overwrite the current version.
    • The bonus? It automatically moves all the other revisions forward in the delta chain. So we've still got our history.
  4. If there isn't an original, that means this prototype added a method to the underlying instance. Just delete it, there's no history here.
  5. If there is history, but the current revision doesn't belong to this prototype, we need to do more work.
    • We can walk the delta chain. Look at this delta's delta.
    • Does that revision belong to this prototype?
      • If it does, grab that revision's original, and make it the next revisions original- basically, pull this node out of the linked list.
      • If not, keep walking the chain. If we hit the end of the chain without finding anything, just give up- it means we weren't used to decorate this class.
Notes

Originally, I tracked an array of "decoratedBy"s. When a decorator was applied, it logged a boolean in that array. The problem with that is that it doesn't allow for the same decoration to be applied twice. Instead of making a complex data structure that supports that, I just decided to use the delta chain, which already does that. The downside- if someone tries to remove a decoration that was never applied, the undecorate method will walk the whole delta chain to do absolutely nothing. This is somewhat ineffecient. This shouldn't be a common scenario.

You can apply the same decoration more than once. This lets you build a stack of decorations. x is decorated by p, q, r, q, r, for example. Your code isn't responsible for noting which decorations have already been applied, which does create another possible problem- if you aren't careful about tracking which decorations you've applied, a single instance could be decorated again and again for no reason. There are good business reasons to want to apply the same decoration twice (x->p (do stuff), p->q (do stuff), q->p (do stuff) q<-p (do stuff)). The code is simpler by allowing it, so I see no good reason to force an artificial restriction on developers. But be careful- this does allow a memory leak when you're decorating globals. Every call to decorate should have a matching undecorate someplace.

History is available to a decorated call. If I have x decorated by p, p.foo can invoke x.foo: this.foo.delta.original(); Conceiveably, a decorated method can walk anywhere they desire in the chain. It may be tempting to create a convenience method for accessing the undecorated implementation, but that breaks the pattern too much.

A decorator can create new methods that were not in the original. This means that we aren't truly using a Decorator pattern. The Decorator pattern specifies that the interface remains the same. Here, we can actually change the interface. Again, there are good business reasons to do such a thing, my code is simpler for not doing it. Developer beware.
Making it a True Decorator

What if you really want that restriction? Could I implement such a thing in this library? Certainly. But this is JavaScript. Interfaces are fluid and there is no polymorphism. Hence, no compelling reason to enforce that kind of restriction. In a strongly-typed polymorphic language, this would matter. But in such a language, I could never have impemented this code anyway! While I'm a big fan of applying strict OO concepts to design, JavaScript doesn't support many of them. We should write code to take advantage of the quirks in our platform.
The Code

/**
* @author t3knomanser
*/
MutatingDecoration=
{
/**
* Given a prototype and a class instance, this method will decorate the instance with the prototype methods.
* Undecorated versions are mainained in the "delta" object appended to the class member. As more decorations are
* added, a chain of deltas will form: instance.currentMethod.delta.original.delta.original...
* The delta object will have the form:
* delta:
* source: the prototype that caused this change
* name: the property that has been changed
* original: the initial version of the property.
* primitive: true or false, was this a primitive datatype? This isn't really used for anything, but all primitives are magically promoted to objects by this process (through the addition of the delta object)
* @param {Object} instance The object to be decorated.
* @param {Class} proto The prototype to decorate with. The contructor should accept no paramters.
*/
decorate:function(instance, proto)
{
//debug.out("Adding:" + proto.toString().substring(8, 12));
var temp = new proto(); //make an instance - this is required to cycle across instances
for (var key in temp) //look at each property
{
var info = new Object(); //log key information for reversal
info.source = proto;
info.name = key;
info.original = instance[key];
if (typeof(instance[key]) != "function" && typeof(instance[key]) != "object")
{
info.primitive = true;
}
instance[key] = temp[key]; //swap out the original for the decorated version
instance[key].delta = info; //store the delta-log
}
if (!instance.decoratedBy) instance.decoratedBy = new Array();
instance.decoratedBy[proto] = true;
},
/**
* Reverse the process of decoration. This function relies heavily on the delta object to get the information that it
* needs to reverse the process. It's fairly robust, but if the delta object mutates, it might break. Don't modify the
* delta object.
* @param {Object} instance The class instance to be undecorated.
* @param {Class} proto The prototype to be removed from this instance. The contructor should accept no paramters.
*/
undecorate:function(instance, proto)
{
//debug.out("Removing:" + proto.toString().substring(8, 12));
/**
* Iterate across the delta chain (delta.original.delta.original...) to find the _parent_ delta of the selected prototype.
* That is to say, if I apply proto "P" and then apply proto "Q", the delta chain will be: deltaq.original.deltap.original (the final original is the original method)
* If I then want to remove "P" (deltap), then I need to know what "P"'s parent is (deltaq). Given "P", this will return the
* delta object for "Q"
* @param {Object} info The delta object where we start the search.
* @param {Class} proto The prototype to find in the delta chain.
*/
function findParent(info, proto)
{
var done = false;
var curInfo = info;
while (!done)
{
if (!curInfo.original || !curInfo.original.delta) //there isn't any
return null;
if (curInfo.original.delta.source == proto) //found it!
{
return curInfo;
}
curInfo = curInfo.original.delta;
}
}
var temp = new proto(); //get an instance - once again, we need to loop across members.
for (var key in temp) //let's examine each of the properties again
{
if (instance[key] && instance[key].delta) //verify that it hasn't been mutated out and that there is a delta object
{
var info = instance[key].delta; //grab the change-log
if (info.source == proto) //if this proto is the source, we have the easy scenario
{
if (info.original) //if there's an original
{
instance[key] = info.original; //revert to it
} else { //If there was no original- that is, this method was ADDED to the class
delete instance[key]; //make it go away.
}
} else { //We have the hard case- some other proto has been applied since we were here.
var previous = findParent(info, proto); //find the proto that was applied directly after the one we want to remove. This one will be the "parent" of ours in the filter chain.
if (previous) //make sure we got a result
{
if (previous.original.delta.original) //make sure all the important properties actuall exist
{
previous.original = previous.original.delta.original; //copy our original over our parent's original- this deletes our delta record, and it's like we were never here
}
}
}
}
}
}
}

programming, javascript

Previous post Next post
Up