Metaobject Protocols - JavaScript Spessore: A thick shot of objects, metaobjects, & protocols (2014)

JavaScript Spessore: A thick shot of objects, metaobjects, & protocols (2014)

Metaobject Protocols

33

In This Chapter

In this chapter, we’ll explore making metaobjects that encapsulate their own metaprogramming behaviour. In other words, we’ll program OO, using OO.

Genesis

In this section, we’re going to build a Metaobject Protocol. Or in less buzzword-compatible terms, we’re going to build classes that can be programmed using methods.

We can already make class-like things with functions, prototypes, and the new keyword, provided we directly manipulate our “classes.” This makes it difficult to make changes after the fact, because all of our code is tightly coupled to the structure of our “classes.”

We can decouple our code from the structure of our classes by doing all of our manipulation with methods instead of with direct manipulation of functions and prototypes. Let’s start with a simple example, a quadtree class.

Normally, we would start with something like this:

function QuadTree (nw, ne, se, sw) {

this.nw = nw;

this.ne = ne;

this.se = se;

this.sw = sw;

}

And we would write methods using code like this:

QuadTree.prototype.population = function () {

return this.nw.population() + this.ne.population() +

this.se.population() + this.sw.population();

}

var account = new QuadTree(...);

As discussed before, we are directly manipulating QuadTree’s internal representation. Let’s abstract method definition away. We’d like to write:

QuadTree.defineMethod( 'population', function () {

return this.nw.population() + this.ne.population() +

this.se.population() + this.sw.population();

});

We’ll see why in a moment. How do we make this work? Let’s start with:

QuadTree.defineMethod = function (name, body) {

this.prototype[name] = body;

return this;

};

That works, and now we can introduce new semantics at will. For example, what does this do?

QuadTree.defineMethod = function (name, body) {

Object.defineProperty( this.prototype, name, {

writable: false,

enumerable: false,

value: body

});

return this;

};

factory factories

We can now write:

QuadTree.defineMethod( 'population', function () {

return this.nw.population() + this.ne.population() +

this.se.population() + this.sw.population();

});

We accomplished this by writing:

QuadTree.defineMethod = function (name, body) {

this.prototype[name] = body;

return this;

};

Fine. But presuming we carry on to create other classes like Cell, how do we give them the same defineMethod method?

Hmmm, it’s almost like we want to have objects that delegate to a common prototype. We know how to do that:

var ClassMethods = {

defineMethod: function (name, body) {

this.prototype[name] = body;

return this;

}

};

var QuadTree = Object.create(ClassMethods);

Alas, this doesn’t work for functions we use with new, because JavaScript functions are a special kind of object that we can’t decorate with our own prototypes. We could fool around with mixing behaviour in, but let’s abstract new away and use a .create` method to create instances.

.create

This is what QuadTree would look like with its own .create method. We’ve moved initialization into an instance method instead of being part of the factory method:

var QuadTree = {

create: function () {

var acct = Object.create(this.prototype);

if (acct.constructor) {

acct.constructor.apply(acct, arguments);

}

return acct;

},

defineMethod: function (name, body) {

this.prototype[name] = body;

return this;

},

prototype: {

constructor: function (nw, ne, se, sw) {

this.nw = nw;

this.ne = ne;

this.se = se;

this.sw = sw;

}

}

};

Let’s extract a few things. Step one:

function Class () {

return {

create: function () {

var instance = Object.create(this.prototype);

Object.defineProperty(instance, 'constructor', {

value: this

});

if (instance.constructor) {

instance.constructor.apply(instance, arguments);

}

return instance;

},

defineMethod: function (name, body) {

this.prototype[name] = body;

return this;

},

prototype: {}

};

}

var QuadTree = Class();

QuadTree.defineMethod(

'initialize', function (nw, ne, se, sw) {

this.nw = nw;

this.ne = ne;

this.se = se;

this.sw = sw;

}

).defineMethod(

'population', function () {

return this.nw.population() + this.ne.population() +

this.se.population() + this.sw.population();

}

);

Step two:

var BasicObjectClass = {

prototype: {}

}

function Class (superclass) {

return {

create: function () {

var instance = Object.create(this.prototype);

Object.defineProperty(instance, 'constructor', {

value: this

});

if (instance.constructor) {

instance.constructor.apply(instance, arguments);

}

return instance;

},

defineMethod: function (name, body) {

this.prototype[name] = body;

return this;

},

prototype: Object.create(superclass.prototype)

};

}

var QuadTree = Class(BasicObjectClass);

Step three:

var BasicObjectClass = {

prototype: {}

}

var MetaMetaObjectPrototype = {

create: function () {

var instance = Object.create(this.prototype);

Object.defineProperty(instance, 'constructor', {

value: this

});

if (instance.constructor) {

instance.constructor.apply(instance, arguments);

}

return instance;

},

defineMethod: function (name, body) {

this.prototype[name] = body;

return this;

}

}

function Class (superclass) {

return Object.create(MetaMetaObjectPrototype, {

prototype: {

value: Object.create(superclass.prototype)

}

})

}

var QuadTree = Class(BasicObjectClass);

// QuadTree.defineMethod...

Step four, now we change the shape:

var Class = {

create: function (superclass) {

return Object.create(MetaMetaObjectPrototype, {

prototype: {

value: Object.create(superclass.prototype)

}

});

}

}

var QuadTree = Class.create(BasicObjectClass);

// QuadTree.defineMethod...

Step five:

var MetaObjectPrototype = Object.create(MetaMetaObjectPrototype, {

constructor: {

value: function (superclass) {

this.prototype = Object.create(superclass.prototype)

}

}

});

var Class = {

create: function (superclass) {

var klass = Object.create(this.prototype);

Object.defineProperty(klass, 'constructor', {

value: this

});

if (klass.constructor) {

klass.constructor.apply(klass, arguments);

}

return klass;

},

prototype: MetaObjectPrototype

};

var QuadTree = Class.create(BasicObjectClass);

// QuadTree.defineMethod...

ouroboros

Now we are in an interesting place. Let’s consolidate and rename things:

var MetaObjectPrototype = {

create: function () {

var instance = Object.create(this.prototype);

Object.defineProperty(instance, 'constructor', {

value: this

});

if (instance.constructor) {

instance.constructor.apply(instance, arguments);

}

return instance;

},

defineMethod: function (name, body) {

this.prototype[name] = body;

return this;

},

constructor: function (superclass) {

if (superclass != null && superclass.prototype != null) {

this.prototype = Object.create(superclass.prototype);

}

else this.prototype = Object.create(null);

}

};

var MetaClass = {

create: function () {

var klass = Object.create(this.prototype);

Object.defineProperty(klass, 'constructor', {

value: this

});

if (klass.constructor) {

klass.constructor.apply(klass, arguments);

}

return klass;

},

prototype: MetaObjectPrototype

};

And now we do something interesting, we use MetaClass to make Class:

var Class = MetaClass.create(MetaClass);

Class.constructor === MetaClass

//=> true

And we use Class to make QuadTree:

var BasicObjectClass = Class.create(null);

var QuadTree = Class.create(BasicObjectClass);

QuadTree.constructor === Class

//=> true

QuadTree.defineMethod(

'initialize', function (nw, ne, se, sw) {

this.nw = nw;

this.ne = ne;

this.se = se;

this.sw = sw;

}

).defineMethod(

'population', function () {

return this.nw.population() + this.ne.population() +

this.se.population() + this.sw.population();

}

);

As well as Cell:

var Cell = Class.create(BasicObjectClass);

Cell.defineMethod( 'initialize', function (population) {

this._population = population;

}).defineMethod( 'population', function () {

return this._population;

});

What have we achieved? Well, QuadTree and Cell are both classes, and they’re also instances of class Class. How does this help us? We now have the ability to modify every aspect of the creation of classes and instances by modifying methods and classes. For example, if all classes need a new method, we can call Class.defineMethod.

Let’s try creating a new method on all classes:

Class.defineMethod( 'instanceMethods', function () {

var prototype = this.prototype;

return Object.getOwnPropertyNames(prototype).filter( function (propertyName) {

return typeof(prototype[propertyName]) === 'function';

})

});

Cell.instanceMethods()

//=> [ 'initialize', 'population' ]

It works!

The Class Class

In The “I” Word, we discussed how the word “inheritance” is used, loosely, to refer to either or both of membership in a formal class (“is-a”), and delegation of implementation and expectations (“was-a”). Given our code for initializing metaobjects:

constructor: function (superclass) {

if (superclass != null && superclass.prototype != null) {

this.prototype = Object.create(superclass.prototype);

}

else this.prototype = Object.create(null);

}

This sets up the prototype chain for instances. For example, a special subclass of QuadTree for trees of trees that supports interpolating n, e, s, w, and c:

var TreeOfTrees = Class.create(QuadTree);

TreeOfTrees.defineMethod('n', function () {

return this.ne.constructor.create(this.nw.ne, this.ne.nw, this.ne.sw, this.nw.s\

e);

}).defineMethod('e', function () {

return this.ne.constructor.create(this.ne.sw, this.ne.se, this.se.ne, this.se.n\

w);

})

// ...

Instances of TreeOfTrees inherit the methods defined in TreeOfTrees as well as those in QuadTree. Although this technique can be overused, it is a useful option for sharing behaviour.

Obviously, implementation inheritance allows two classes to share a common superclass, and thus share behaviour between them. What about classes themselves? As described, they are all instances of Class, but that is not a hard and fast requirement.

fluent interfaces

Consider fluent interfaces. In software engineering, a fluent interface (as first coined by Eric Evans and Martin Fowler) is an implementation of an object oriented API that aims to provide for more readable code.

Object and instance methods can be bifurcated into two classes: Those that query something, and those that update something. Most design philosophies arrange things such that update methods return the value being updated. However, the fluent style presumes that most of the time when you perform an update, you are more interested in doing other things with the receiver then the values being passed as argument(s), so the rule is to return the receiver unless the method is a query.

So, given:

var MutableQuadTree = Class.create(BasicObjectClass);

We might write:

MutableQuadTree

.defineMethod( 'setNW', function (value) {

this.nw = value;

return this;

})

.defineMethod( 'setNE', function (value) {

this.ne = value;

return this;

})

.defineMethod( 'setSE', function (value) {

this.se = value;

return this;

})

.defineMethod( 'setSW', function (value) {

this.sw = value;

return this;

});

This would be a fluent interface, allowing us to write things like:

var tree = MutableQuadTree

.create()

.setNW(Cell.create(1))

.setNE(Cell.create(0))

.setSE(Cell.create(0))

.setSW(Cell.create(01);

However, what work it is! Another possibility would be to subclass Class, and override defineMethod, like so:

var allong = require('allong.es');

var unvariadic = allong.es.unvariadic;

var FluentClass = Class.create(Class);

FluentClass.defineMethod( 'defineMethod', function (name, body) {

this.prototype[name] = unvariadic(body.length, function () {

var returnValue = body.apply(this, arguments);

if (typeof(returnValue) === 'undefined') {

return this;

}

else return returnValue;

});

return this;

});

A normal method semantics are that if it doesn’t explicitly return a value, it returns undefined. A FluentClass method’s semantics are that if it doesn’t explicitly return a value, it returns the receiver. So now, we can write:

var MutableQuadTree = FluentClass.create(BasicObjectClass);

MutableQuadTree

.defineMethod( 'setNW', function (value) {

this.nw = value;

})

.defineMethod( 'setNE', function (value) {

this.ne = value;

})

.defineMethod( 'setSE', function (value) {

this.se = value;

})

.defineMethod( 'setSW', function (value) {

this.sw = value;

});

var tree = MutableQuadTree

.create()

.setNW(Cell.create(1))

.setNE(Cell.create(0))

.setSE(Cell.create(0))

.setSW(Cell.create(01);

And our trees are fluent by default: Any method that doesn’t explicitly return a value with the return keyword will return the receiver.

By creating classes that share their own implementation of defineMethod, we now have a way to create collections of objects that have their own special semantics. There are many ways to do this, of course, but the key idea here is that we’re using polymorphism: Fluent classes look just like regular classes, so there is no need to write any special code when defining a fluent method or creating an object with fluent semantics.

We’re using the exact same techniques for programming with metaclasses that we use for programming with domain objects.

Class Mixins

In The Class Class, we saw how we could use implementation inheritance to create FluentClass, a class where methods are fluent by default.

Implementation inheritance is not the only way to customize class behaviour, and it’s rarely the best choice. Long experience with hierarchies of domain objects has shown that excessive reliance on implementation inheritance leads to fragile software that ends up being excessively coupled.

Contemporary thought suggests that traits or mixins are a better choice when there isn’t a strong need to model an “is-a” relationship. Considering that JavaScript is a language that emphasizes ad hoc sets rather than formal classes, it makes sense to look at other ways to create classes with custom semantics.

singleton prototypes

The typical arrangement for “classes” is that objects of the same class share a common prototype associated with the class:

Standard class prototype delegation

Standard class prototype delegation

This is true whether we are using functions as our “classes” or objects that are themselves instances of Class. In Singleton Prototypes, we looked at how we can associate a singleton prototype with each object:

Singleton prototype delegation

Singleton prototype delegation

Each object delegates its behaviour to its own singleton prototype, and that prototype delegates in turn to the class prototype. As discussed earlier, the motivation for this arrangement is so that methods can be added to individual objects while preserving a separation of domain attributes from methods. For example, you can add a method to an object’s singleton prototype and still use .hasOwnProperty() to distinguish attributes from methods.

To create objects with singleton prototypes, we could use classes that have their own .create method:

var SingletonPrototypicalClass = Class.create(Class);

SingletonPrototypicalClass.defineMethod('create', function () {

var singletonPrototype = Object.create(this.prototype);

var instance = Object.create(singletonPrototype);

Object.defineProperty(instance, 'constructor', {

value: this

});

if (instance.constructor) {

instance.constructor.apply(instance, arguments);

}

return instance;

});

This “injects” the singleton prototype in between the newly created instance and the class’s prototype. So if we create a new class:

var SingletonPrototypeCell = SingletonPrototypicalClass.create();

var SingletonPrototypeQuadTree = SingletonPrototypicalClass.create();

We know that every instance of SingletonPrototypeCell and SingletonPrototypeQuadTree will have its own singleton prototype.

mixins

This seems very nice, but what do we do if we want a class that has both fluent interface semantics and singleton prototype semantics, like our MutableQuadTree example?

Do we decide that all classes should encompass both behaviours by adding the functionality to Class? That works, but it leads to a very heavyweight class system. This is the philosophy of languages like Ruby, where all classes have a lot of functionality. That is consistent with their approach to basic objects as well: The default Object class also has a lot of functionality that every object inherits by default.

Do we make SingletonPrototypicalClass a subclass of FluentClass? That makes it impossible to have a class with singleton prototype semantics that doesn’t also have fluent semantics, and there’s the little matter that fluency and prototype semantics really aren’t related in any kind of “is-a” semantic sense. Making FluentClass a subclass of SingletonPrototypicalClass has the same problems.

Forty years of experience modeling domain entities has led OO thinking to the place where representing independent semantics with inheritance is now considered an anti-pattern. Instead, we look for ways to select traits, behaviours, or semantics on an a’la carte basis using template inheritance, also called mixing in behaviour.

Mixing behaviour in is as simple as extending the class object:

var MutableQuadTree = Class.create();

extend(MutableQuadTree, {

defineMethod: function (name, body) {

this.prototype[name] = unvariadic(body.length, function () {

var returnValue = body.apply(this, arguments);

if (typeof(returnValue) === 'undefined') {

return this;

}

else return returnValue;

});

return this;

}

});

extend(MutableQuadTree, {

create: function () {

var singletonPrototype = Object.create(this.prototype);

var instance = Object.create(singletonPrototype);

Object.defineProperty(instance, 'constructor', {

value: this

});

if (instance.constructor) {

instance.constructor.apply(instance, arguments);

}

return instance;

}

});

We eschew monkeying around explicitly with internals, so let’s create some functions we can call:

function Fluentize (klass) {

extend(klass, {

defineMethod: function (name, body) {

this.prototype[name] = unvariadic(body.length, function () {

var returnValue = body.apply(this, arguments);

if (typeof(returnValue) === 'undefined') {

return this;

}

else return returnValue;

});

return this;

}

});

}

function Singletonize (klass) {

extend(klass, {

create: function () {

var singletonPrototype = Object.create(this.prototype);

var instance = Object.create(singletonPrototype);

Object.defineProperty(instance, 'constructor', {

value: this

});

if (instance.constructor) {

instance.constructor.apply(instance, arguments);

}

return instance;

}

});

}

Now we can write:

MutableQuadTree = Class.create();

Fluentize(MutableQuadTree);

Singletonize(MutableQuadTree);

This cleanly separates the notion of subclassing classes from the notion of some classes having traits or behaviours they share with other classes.

Note that although the mixin pattern is completely different from the subclassing Class pattern, both are made possible by using objects as classes rather than using functions, the new keyword, and directly manipulating prototypes to define methods.

Well, Actually…

Both inheritance and mixins have their place for both our domain classes and the metaobjects that define them. However, if not designed with care, object-oriented code bases evolve over time to become brittle and coupled whether they’re built with inheritance or mixins. Let’s take another look at class mixins and see why.

In Class Mixins, we saw how we could build two mixins, Fluentize and Singletonize. Implementing these two semantics as mixins allows us to create classes that have neither semantic, fluent semantics, singleton prototype semantics, or both. This is far less brittle than trying to do the same thing by cramming both sets of functionality into every class (the “heavyweight base” approach) or trying to set up an artificial inheritance hierarchy.

This sounds great for classes of classes and for our domains as well: Build classes for domain objects, and write mixins for various bits of shared functionality. Close the book, we’re done!

Alas, we’re not done. The flexibility of mixins is an illusion, and by examining the hidden problem, we’ll gain a deeper understanding of how to design object-oriented software.

self-binding-ization

In our next example, Fluentize overrides the defineMethod method, such that fluent classes use its implementation and not the implementation built into MetaClass. Likewise, Singletonize overrides the create method, such that singleton prototype classes use its implementation and not the implementation built into MetaClass.

These two mixins affect completely different methods, and furthermore the changes they make do not affect each other in any way. But this isn’t always the case, consider this counter:

var Counter = Class.create();

Counter

.defineMethod('initialize', function () { this._count = 0; })

.defineMethod('increment', function () { ++this._count; })

.defineMethod('count', function () { return this.count; });

var c = Counter.create();

And we have some function written in continuation-passing-style:

function log (message, callback) {

console.log(message);

return callback();

}

Alas, we can’t use our counter:

log("doesn't work", c.increment);

The trouble is that the expression c.increment returns the body of the method, but when it is invoked using callback(), the original context of c has been lost. The usual solution is to write:

log("works", c.increment.bind(c));

The .bind method binds the context permanently. Another solution is to write (or use a function to write):

c.increment = c.increment.bind(c);

Then you can write:

log("works without thinking about it", c.increment);

It seems like a lot of trouble to be writing this out everywhere, especially when the desired behaviour is nearly always that methods be bound. Let’s write another mixin:

function SelfBindingize (klass) {

klass.defineMethod = function (name, body) {

Object.defineProperty(this.prototype, name, {

get: function () {

return body.bind(this);

}

});

return this;

}

}

var SelfBindingCounter = Class.create();

SelfBindingize(SelfBindingCounter);

SelfBindingCounter.defineMethod('initialize', function () { this._count = 0; })

SelfBindingCounter.defineMethod('increment', function () { return ++this._count; \

})

SelfBindingCounter.defineMethod('count', function () { return this.count; });

var cc = SelfBindingCounter.create();

function log (message, callback) {

console.log(message);

return callback();

}

log("works without thinking about it", cc.increment);

Classes that mix SelfBindingize in are self-binding. We’ve encapsulated the internal representation and implementation, and hidden it behind a method that we mix into the class.

Composition with Red, Blue and Yellow--Piet Mondrian (1930)

Composition with Red, Blue and Yellow–Piet Mondrian (1930)

composition

As we might expect, SelfBindingize plays nicely with Singletonize, just as Fluentize did. But speaking of Fluentize, Singletonize does not play nicely with Fluentize. They both create new versions of defineMethod, and if you try to use both, only the last one executed will take effect.

The problem is that SelfBindingize and Fluentize don’t compose. We can make up lots more similar examples too. In fact, metaprogramming tools generally don’t compose when you write them individually. And this lesson is not restricted to “advanced” ideas like writing class mixins: Mixins in the problem domain have the same problem, they tend not to compose by default, and that makes using them a finicky and fragile process.

The naïve way to solve this problem is to write our mixins, and then when conflicts are discovered, rewrite them to play well together. This is not a bad strategy, in fact it might be considered the “optimistic” strategy. As long as we are aware of the possibility of conflicts and know to look for them when the code does not do what we expect, it is not unreasonable to try to avoid conflicts rather than investing in another layer of abstraction up front.

On the other hand, if we expect to be doing a lot of mixing in, it is a good investment to design around composition up front rather than deal with the uncertainty of not knowing when what appears to be a small change might lead to a lot of rewriting later on.

When dealing with semantics “in the large,” such as when writing class mixins, there generally aren’t a lot of “custom” semantics needed in one software application, and they generally come up early in the design and development process. Thus, many developers take the optimistic approach: They build classes and incorporate custom semantics as they go, resolving any conflicts where needed.

However, in the real of the problem domain, there are many more requirements for mixing functionality in, and requirements change more often. Thus, experienced programmers will sometimes build (or “borrow” from a library) infrastructure to make it easy to compose mixed-in functionality for domain objects.

For example, in the Ruby on Rails framework, there is extensive support for binding behaviour to controller methods and to model object lifecycles, and the library is carefully written so that you can bind more than one quantum of behaviour, and the bindings compose neatly without overwriting each others’ effects.

This support is implemented with metaprogramming of the semantics of controller and model base classes. If we accept the notion that we are more likely to need to compose model behaviour than metaobject behaviour, one way forward is to use our tools for metaobject programming to implement a protocol for composing model behaviour.

method decorators

When looking at writing classes with classes, we used the example of making methods fluent by default. Fluent APIs are a wide-ranging, cross-cutting concern the can be applied to many designs. But for every wide-ranging design technique, there are thousands of application-specific domain concerns that can be applied to method semantics.

Depending on the problem domain, programmers might be concerned about things like:

· Checking for permissions before executing a method

· Logging the invocation and/or result of a method

· Checking entities for validity before persisting them to storage.

Each domain concern can be written as a mixin of sorts. In javaScript Allongé, we looked at techniques for using combinators to apply special semantics to methods. For example, here is a simple combinator that “validates” the arguments to a method:

function validates (methodBody) {

return function () {

var i;

for (i = 0; i < arguments.length; ++i) {

if (typeof(arguments[i].validate) === 'function' && !arguments[i].validate(\

)) throw "validation failure";

}

return fn.apply(this, arguments);

}

}

Amongst other improvements, a more robust implementation would mimic the method body’s arity. But this is close enough for discussion: Applied to a function, it returns a function that first checks all of its arguments. If any of them implement a .validate method, that method is called and an error thrown if it doesn’t return true.

And here’s a combinator that implements “always fluent” method semantics (this differs slightly from the “fluent-by-default” that we saw earlier):

function fluent (methodBody) {

return function () {

methodBody.apply(this, arguments);

return this

}

}

These two combinators compose nicely if you write something like this:

function Account () {

// ...

}

Account.prototype.validate = function () {

// ...

};

function AccountList () {

this.accounts = [];

}

AccountList.prototype.add = fluent( validates( function (account) {

this.accounts.push(account);

}));

You can even explicitly compose the combinators:

var compose = require('allong').es.compose;

var fluentlyValidates = compose(fluent, validates);

Or apply them after assigning the method:

AccountList.prototype.add = function (account) {

this.accounts.push(account);

};

// ...

AccountList.prototype.add = fluent( validates( AccountList.prototype.add ) );

Everything seems wonderful with this approach! But this has limitations as well. The limitation is that combinators operate on functions. The hidden assumption is that when we apply the combinator, we already have a function we want to decorate. This won’t work, for example:

function AccountList () {

this.accounts = [];

}

AccountList.prototype.add = fluent( validates( AccountList.prototype.add ) );

// ...

AccountList.prototype.add = function (account) {

this.accounts.push(account);

};

You can’t decorate the method’s function before you assign it! Here’s another variation of the same problem:

function AccountList () {

this.accounts = [];

}

AccountList.prototype.add = fluent( validates( function (account) {

this.accounts.push(account);

}));

// ...

So far so good, but:

function IndexedAccountList () {

this.accounts = {};

}

IndexedAccountList.prototype = Object.create(AccountList.prototype);

IndexedAccountList.prototype.add = function (account) {

this.accounts[account.id] = account;

}

IndexedAccountList.prototype.at = function (id) {

return this.accounts[id];

}

Our IndexedAccountList.prototype.add overrides AccountList.prototype.add… And in the process, overrides the modifications made by fluent and validates. This makes perfect sense if you are using prototypical inheritance purely as a convenience for sharing some behaviour. But if you are actually trying to model a formal class, then perhaps you always want the .add method to validate its arguments and fluently return the receiver. Perhaps you only want to override how the method adds things internally without modifying the “contract” you are expressing with the outside world?

Regardless of your exact interpretation of the semantics of overriding a method that you’ve decorated, the larger issue remains that there is some brittleness involved in using functions to decorate methods in classes. They don’t always compose neatly and cleanly.