Composable Behavior with Method Wrapping

Máté Safranka
6 min readMar 18, 2019

--

One of the fundamental rules of clean code is DRY — Don’t Repeat Yourself. The entire concept of functions and classes owe their existence to this principle, as means of reusing behavior across different parts of code. As time went on, and programs grew in complexity, so too did developers’ need for more sophisticated solutions to write more streamlined and efficient code.

Photo by Benjamin Ashton on Unsplash

Regardless of your preferred framework (if any), reusable behavior is by no means a foreign concept in frontend development. In this example, we will be looking at a family of widgets that all need to subscribe to a certain event when they are added to the layout, and unsubscribe when they are removed. We are going to assume that these widget classes implement the following interface:

interface IWidget {
attach(): void;
destroy(): void;
}

Again, we’re not talking about any particular framework here, but most of them offer similar lifecycle hooks for their components.

As you may suspect, we will need to subscribe to the event in the attach() method, and unsubscribe in the destroy() method:

class SomeWidget implements IWidget {
attach() {
someEvent.subscribe(this);
}
destroy() {
someEvent.unsubscribe(this);
}
}

Including this code in each widget makes code maintenance tedious and error prone. Let’s look at the ways we can solve them.

Inheritance

The most instinctive solution for OOP-minded programmers is to declare an abstract superclass for these widgets, and define the behavior there:

class AbstractWidget implements IWidget {
attach() {
someEvent.subscribe(this);
}
destroy() {
someEvent.unsubscribe(this);
}
}
class SomeWidget extends AbstractWidget {}class OtherWidget extends AbstractWidget {
attach() {
super.attach();
this.doSomethingElseOnAttach();
}
}

The upside of inheritance is that we can still choose to amend or overwrite the default behavior, as seen in OtherWidget.

However, seasoned OOP developers will know this is not a very good use of inheritance. Inheritance is supposed to define an “is-a” relationship between the subclass and the superclass. Our AbstractWidget, on the other hand, only defines a small set of behavior, with no real effect on the widgets’ identity.

Locking ourselves into this inheritance subtree also makes it extremely difficult (if not impossible) to create multiple sets of reusable behavior that we can add to our classes.

Mixins

Mixins belong to a broader concept known as horizontal composition. As opposed to inheritance, in which behavior is passed down in a strict hierarchy, horizontal composition allows developers to “pick and choose” their classes’ behavior like a buffet. Many languages offer some way to implement mixins; in JavaScript, the most straightforward one is the Object.assign() method.

const EventHandlingMixin = {
attach() {
someEvent.subscribe(this);
},
destroy() {
someEvent.unsubscribe(this);
},
};
class SomeWidget {
constructor() {
Object.assign(this, EventHandlingMixin);
}
}

This allows the EventHandling behavior to be reused in any class we like, but at a steep price: we are no longer able to implement the attach() and destroy() methods at the class level, as they are completely replaced by the version in the mixin. Any behavior we would want to implement in them is lost. Also, if we apply multiple mixins that implement the same method, the one we apply last “wins”. Thus, mixins that overlap in their methods can end up being only partly implemented by the class.

const EventHandlingMixin = {
attach() { /* ... */ },
destroy() { /* ... */ },
};
const LoggingMixin = {
attach() { /* ... */ },
};
class SomeWidget {
constructor() {
Object.assign(
this,
EventHandlingMixin,
LoggingMixin,
);

}
}

In the above example, SomeWidget will have the destroy() method of the EventHandlingMixin, but the attach() method of the LoggingMixin, since that was applied later. The result is bound to result in a runtime exception at best (as SomeWidget attempts to free up resources in destroy() that it never got to claim in attach()), or a silent error at worst (as SomeWidget will simply not exhibit the behavior we expect it to).

This is JavaScript. We can do better.

Method Wrapping

Observe the following function:

function wrapMethod(object, methodName, newBehavior) {
const originalMethod = object[methodName];

object[methodName] = function() {
newBehavior.apply(this, arguments);
originalMethod.apply(this, arguments);
};
}

“Method wrapping” here refers to the injection of new behavior into an object’s method by creating a new function that calls the new behavior and the original method in order, and assigning this function as the object’s method.

Let’s look at a concrete example to untangle all of that:

class SomeWidget {
attach() {
console.log('SomeWidget.attach called.');
}
}
let widget = new SomeWidget();wrapMethod(widget, 'attach', function() {
console.log('Injected behavior called.');
});

If we now call widget.attach(), we can see two lines in the output:

Injected behavior called.
SomeWidget.attach called.

(Note that it is vital to use the full function syntax, and not arrow functions, because we need access to both this and the arguments variable)

This approach allows us to define the reusable behavior in one place, and then call wrapMethod() in the widgets’ constructors to apply it to them:

const EventHandlingMixin = {
attach() {
someEvent.subscribe(this);
},
destroy() {
someEvent.unsubscribe(this);
},
};
class SomeWidget {
constructor() {
wrapMethod(this, 'attach', EventHandlingMixin.attach);
wrapMethod(this, 'destroy', EventHandlingMixin.destroy);

}
}

This solution also allows us to apply multiple mixins that implement the same method:

const EventHandlingMixin = {
attach() { /* ... */ },
destroy() { /* ... */ },
};
const LoggingMixin = {
attach() { /* ... */ },
};
class SomeWidget {
constructor() {
wrapMethod(this, 'attach', LoggingMixin.attach);
wrapMethod(this, 'attach', EventHandlingMixin.attach);
wrapMethod(this, 'destroy', EventHandlingMixin.destroy);

}
attach() { /* ... */ }
}

Now, instances of SomeWidget will exhibit the behavior defined in both mixins, as well as whatever we choose to implement in the class itself. Although the approach does not (at least in this form) allow us to specify what order the behavior should be executed in, at least we’re not losing functionality.

Mixins with Method Wrapping

Once the basic concept is clear, we can take it one step further. We can create a function that applies a mixin to an object using wrapMethod instead of replacing the methods completely:

applyMixin(target, mixin) {
for (let name in mixin) {
if (typeof mixin[name] !== 'function') continue;
if (name in target) {
wrapMethod(target, name, mixin[name]);
}
else {
target[name] = mixin[name];
}
}
}

The applyMixin() function iterates over the (enumerable) methods of the second argument. If the target object does not contain a method with the same name, it just assigns the method to it; if it does, it calls wrapMethod() to compose their behavior.

With this, we can define the reusable behavior as a mixin, and then just apply it in our widgets’ constructor:

const EventHandlingMixin = {
attach() { /* ... */ },
destroy() { /* ... */ },
};
const LoggingMixin = {
attach() { /* ... */ },
};
class SomeWidget {
constructor() {
applyMixin(this, LoggingMixin);
applyMixin(this, EventHandlingMixin);

}
attach() { /* ... */ }
}

Not only does this reduce our boilerplate code even further, it also allows us to add new methods to the mixin (e.g. if we need to cover other lifecycle events) without having to modify the code of the widgets that use it.

If you’re feeling especially experimental, you could even take a look at the TC39 proposal for decorators (which is already implemented in Babel) and see if they could be used to make the process completely declarative. I myself have not ventured this deep yet, but if you do, I would love to read about it in the comments.

Mate Safranka is a frontend developer at Supercharge. If you find the ideas in this article interesting, you might also want to check out this one about encapsulating in-app navigation.

--

--

Máté Safranka

Frontend developer, learning confectioner, hobbyist game maker, amateur writer.