Fun with Stamps. Episode 11. Interfering composition

Vasyl Boroviak
6 min readDec 18, 2016

Hello. I’m developer Vasyl Boroviak and welcome to the eleventh episode of Vasyl Boroviak presents Fun with Stamps.

Classic class-based OOP is rather limiting of what you can and cannot do with your classes. It has certain rules about how the inheritance works. They are set and unchangeable. TraitsJS and mixins are the same — more flexible though, but limited.

Stamps are free. No limits of what you can compose. But what if you really need to validate a result of your composition? For example, what if you want to make sure there is no method name collisions?

Stampit (starting v3.1) allows you to write reusable sort-of-a-hooks as a reusable stamps, where you can alter composition with your own logic. You can have multiple of them seamlessly. You can even override a composed stamp if necessary.

Introducing Composers

At the time of writing the composers feature is experimental and available only in stampit v3.1 and above.

Composers are just callback functions executed each time a composition happens. Also, the composers are executed at the moment they are composed with anything.

import stampit from 'stampit';const Tracked = stampit()
.composers(({composables, stamp}) => { // declaring a composer
console.log(`Composed a stamp "${stamp}" from the following:
"${composables.join()}"`);
});

The code above will log “Composed a stamp …” to the console.

Now you can compose it with anything else and enjoy the console output.

// The following will log to the console
const TrackedComponent = ReactStampComponent.compose(Tracked);
// The following will log to the console too
const MapComponent = TrackedComponent.compose(Map);

If a composer returns a stamp — it overrides the actual composed stamp.

import bunyanLogger from './my-bunyan-logger';
import stampit from 'stampit';
export default const HaveLogger = stampit()
.composers(({stamp}) => {
return stampit().compose(stamp).props({logger: bunyanLogger});
});

The code above does not mutate the result of the composition but overrides it by returning a new stamp. It adds the property logger to every object created. (The usefulness of the example is vague, please think twice before adding that to a production code. :) )

Examples

getStamp() method for every object

Simple enough you can add the .getStamp() method to every stamp.

import _stampit from 'stampit';const SelfAware = _stampit()
.composers(({stamp}) => {
const methods = stamp.compose.methods || {};
methods.getStamp = () => stamp; // mutation! Adding a method
stamp.compose.methods = methods;
});
// detach the `compose` and make sure it always adds our composer
const
stampit = SelfAware.compose.bind(SelfAware);

The code above implements an alternative “stampit” module. The composer will be added to every stamp created with that “stampit” and executed immediately at the time of any composition. Thus, the getStamp will be added to every stamp also immediately.

The implementation is memory and CPU friendly because the methods object becomes the __proto__ of all the objects created from a stamp.

Collecting a stamp’s origins

Let’s collect everything a stamp is composed of to the configuration.wasComposedOf array.

import stampit from 'stampit';const ComponentsMonitoring = stampit()
.composers(({stamp, composables}) => {
// get or create the configuration metadata
const conf = stamp.compose.configuration || {};
stamp.compose.configuration = conf;
// concatenate new composables with the previous list
conf.wasComposedOf = composables.concat(conf.wasComposedOf);
// de-duplicate the array
conf.wasComposedOf = _.uniq(conf.wasComposedOf);
});

You might have noticed that the composer above will mutate any stamp composed with the ComponentsMonitoring.

const Character = stampit()
.compose(ComponentsMonitoring)
.compose(Dog, Robot, Killer);

console.log(stamp.compose.configuration.wasComposedOf);

Let’s add a method getOrigins to every stamp. The method would return the wasComposedOf array.

import stampit from 'stampit';const ComponentsMonitoring = stampit()
.composers(({stamp, composables}) => {
// get or create the configuration metadata
const conf = stamp.compose.configuration || {};
stamp.compose.configuration = conf;
// concatenate new composables with the previous list
conf.wasComposedOf = composables.concat(conf.wasComposedOf);
// de-duplicate the array
conf.wasComposedOf = _.uniq(conf.wasComposedOf);

// get or create the methods metadata
const methods = stamp.compose.methods || {};
stamp.compose.methods = methods;
// add or overwrite the method which returns the composables
const
wasComposedOf = conf.wasComposedOf;
methods.getOrigins = () => wasComposedOf;
});
const stamp = stampit()
.compose(ComponentsMonitoring)
.compose(Dog, Robot, Killer);
console.log(stamp.compose.configuration.wasComposedOf);
console.log(stamp().getOrigins()); // will print the same as above

The ComponentsMonitoring is a stamp (aka composable behavior) which, as it sounds, can be composed into any other stamp. As the result, every object will know the stamps it was made of.

Method name collisions automatic detection

In the code below we will loop through all the given composables (stamps or descriptors) and see if any of them have identical method names. Will throw when find a duplicate method name.

import stampit from 'stampit';
import isStamp from 'stampit/isStamp';
const ThrowOnMethodCollision = stampit()
.composers(({stamp, composables}) => { // declaring a composer
const methodsArray = composables
// convert stamps to descriptors
.map(c => isStamp(c) ? c.compose : c)
// get methods metadata from the descriptors
.map(d => d.methods).filter(m => m);
// do the checks
const methodsStore = {};
methodsArray.forEach(methods => {
// `methods` is an object, each property should be checked
Object.keys(methods).forEach(methodName => {
if (methodsStore[methodName]) {
// oops, we see this method name second time!
throw new Error(`Method "${methodName}" conflict`);
} else {
// all good, we haven't seen that method yet
methodsStore[methodName] = true;
}
});
});
});
// All the examples below will throwstampit(ThrowOnMethodCollision)
.methods({ foo() {} })
.compose({ methods: { foo() {} } });
stampit(
ThrowOnMethodCollision,
{methods: { foo() {} } },
stampit.methods({ foo() {} })
);

Assign first stamp argument to every object (mimic stampit v2)

The stampit v1 and v2 merged (as in Object.assign) the first factory argument to the instantiated objects.

In Stampit v2 the objects below will have property foo.

const Stamp = stampit({ init({instance, stamp, args}) {
console.log(this); // printing the created object instance
}});
Stamp({foo: 'bar'}); // {foo: "bar"}

In Stampit v3 the objects will not have the foo property:

const Stamp = stampit({ init({instance, stamp, args}) {
console.log(this); // printing the created object instance
}});
Stamp({foo: 'bar'}); // {}

Let’s reimplement the feature.

const AssignFirstArgument = stampit({
init(options) {
Object.assign(this, options); // merging properties
}
});

But there is one flaw. The initializer above should be executed the first to make sure the options properties is available to all other initializers. In other word the following will throw:

const UrlValidator = stampit({
init() {
if (!validUrl.isUri(this.url)) // accessing the `this.url`
throw new Error(`Invalid URL ${this.url}`);
}
});
const Connection = compose(
UrlValidator, // This initializer will use `this.url`
AssignFirstArgument // This initializer will assign `this.url`
)
.methods({
connect() {
return Promise.reject(); // a dummy implementation. Ignore.
}
});
// This line will throw because the UrlValidator was composed
// before the AssignFirstArgument
const
connection = Connection({url: 'http://example.com'});

To make sure the above code does not throw we’ll use the Composers feature. Our composer function will ensure that the AssignFirstArgument initializer is always the first in the initializers list Stamp.compose.initializers.

function alwaysFirstInitializer(options) {
Object.assign(this, options);
}
const AssignFirstArgument = stampit({
init: alwaysFirstInitializer, // The initializer itself
composers({stamp}) { // The composer callback
let inits = stamp.compose.initializers;

// removing the initializer from the array
inits = inits.filter(i => i !== alwaysFirstInitializer);
// insert our initializer to the beginning of the list
inits.unshift(alwaysFirstInitializer);
// mutating the stamp - overwriting the initializers list
stamp.compose.initializers = inits;
}
});

Voila! The code above will not throw anymore!

Several composers together as one

Now, finally, the main point of this whole Composers feature and this article.

You can easily mix all the behaviors above into a single stamp.

// 1) will log via Tracked composable behavior
// 2) will check method name collisions via ThrowOnMethodCollision
const
Blended = stampit(
Tracked,
SelfAware,
ComponentsMonitoring,
ThrowOnMethodCollision,
AssignFirstArgument
);
// 3) will have `myFlag` assigned to true via AssignFirstArgument
const
obj = Blended({myFlag: true});
obj.myFlag;
// 4) will return the Blended stamp via SelfAware
obj.getStamp();
// 5) will return the list of all 5 stamps via ComponentsMonitoring
obj.getOrigins();

It’s impossible to do anything like that using classic OOP or even infected compose feature.

We plan to develop a bunch of common behaviors, like ThrowOnMethodCollision etc, and publish as an NPM module(s).

The code from this article available as a GitHub Gist.

Have fun with stamps!

--

--