Fun with Stamps. Episode 8. Tracking and overriding composition
Hello. I’m developer Vasyl Boroviak and welcome to the eighth episode of Vasyl Boroviak presents Fun with Stamps.
UPD: A simpler composition tracking is explained in
Ep 11. Interfering Composition.
Sometimes you need to compose stamps in a certain non-standard way. For example, you might want to make sure there is no method collision when you compose. Or, maybe you want to debug which stamps were used to create a particular stamp. Etc.
You can do that by wrapping and overriding the compose() function.
Overriding composition
Let’s practice. Let’s write some code which would ensure there is no static method collisions.
We will wrap the compose() standalone function:
import compose from 'stamp-specification'; // real NPM modulefunction avoidStaticCollisionsCompose(...args) { // the wrapper.
checkNoStaticCollisions(args); // see below return compose(...args);
}
And try using it:
const MyStamp = compose({staticProperties: {myMethod() {}}});
const myDescriptor = {staticProperties: {myMethod() {}}};avoidStaticCollisionsCompose(MyStamp, myDescriptor); // will throw
The last line will throw the exception - “Error: Collision detected: myMethod”.
The checkNoStaticCollisions() function can be implemented something like that (which is not really important for us right now, but anyways, here is the full source code):
import _ from 'lodash';function getDescriptor(obj) {
if (_.isFunction(obj) && _.isFunction(obj.compose)) {
return obj.compose; // "obj" is a stamp
} if (_.isObject(obj)) {
return obj; // "obj" is a POJO
} return {}; // "obj" is a rubbish we don't care about
}function checkNoStaticCollisions(args) {
const allValues = {}; args.map(getDescriptor).map(arg => arg.staticProperties)
.forEach(arg => {
_.assignWith(allValues, arg, (dstValue, srcValue, key) => {
if (!_.isUndefined(dstValue) && dstValue !== srcValue) {
throw new Error(`Collision detected: ${key}`);
}
}
});
}
But there is one problem with this avoidStaticCollisionsCompose() wrapper. The collisions won’t be checked when the .compose() method is used. Like so:
avoidStaticCollisionsCompose() // even if the wrapper is used,
.compose(MyStamp, myDescriptor); // it doesn't throw any more :(
But no worries!
From the 6-th episode of Fun with Stamp you learned about statics — properties on stamps. We can use it to override the .compose() static method! Here:
function avoidStaticCollisionsCompose(...args) {
checkNoStaticCollisions(args); args.push({staticProperties: {
compose: avoidStaticCollisionsCompose // overriding .compose()
}}); return compose.apply(this, args); // `this` must be passed too
}
As you can see, we are forcing all new stamps to have the different .compose() method. We are adding a new static property to each new stamp. The property references the avoidStaticCollisionsCompose() itself!
So, now you can use the avoidStaticCollisionsCompose() same way as the regular compose() function.
I call that trick the
INFECTED COMPOSE.
Fun fact. The stampit module is an infected compose itself. :) It forcefully adds many static properties to your stamps.
Tracking composition
Now you are going to see a way how to track (or debug) stamp composition.
One more infected compose below. It will stop debugger when a composition happens. And it will print everything being composed.
function debuggerCompose(...args) {
debugger;
console.log('Composing these:', args); args.push({staticProperties: {
compose: debuggerCompose
}}); return compose.apply(this, args);
}debuggerCompose(1, {}, 'whatever', SomeStamp); // will print these
Decomposing stamps
Another example. If you want your objects to contain the list of stamps it was composed of, then you can simply collect them in the deepConfiguration. Like that:
import compose from 'stamp-specification'; // real NPM module
import _ from 'lodash';function initializer(options, {stamp}) {
const sources = _.uniq(stamp.compose.deepConfiguration.sources);
this.getMySources = () => sources; // <- creating method
}function sourceCollectingCompose(...args) {
const additional = {
staticProperties: {compose: sourceCollectingCompose},
deepConfiguration: {
sources: _.clone(args) // <- collecting composables
},
initializers: [initializer]
}; return compose.apply(this, [additional].concat(args));
}
As you can see we are infecting each stamp with three more metadata.
- First one is the infection part — the static .compose() method.
- Second one is the collection of composable sources (descriptors and stamps) inside the deepConfiguration.sources array.
- Third one is the initializer, which adds new .getMySources() method to each object. The method returns the data collected in the deepConfiguration.sources.
Below is the usage of our new infected compose. I would like to remind that Stamps are immutable. Every compose call creates a new Stamp.
const Stamp1 = compose({properties: {foo: 42}});
const Stamp2 = compose({methods: {bar: ()=>{}}});
const Descr3 = {deepProperties: {baz: []}};const All = sourceCollectingCompose(Stamp1) // <- INFECTED STAMP
.compose(Stamp2) // <- INFECTED STAMP TOO
.compose(Descr3); // <- INFECTED STAMP TOOconst obj = All();
console.log(obj.getMySources()); // accessing Stamp1, Stamp2, Descr3
Will print:
[
{ [Function: Stamp] compose: { [Function] properties: [Object] } },
{ [Function: Stamp] compose: { [Function] methods: [Object] } },
{ deepProperties: { baz: [] } }
]
Amazing! Objects know what they consist of!
Un-infecting stamps
To cure stamps you need to override composition one more time and replace the .compose with the original compose function implementation.
import compose from 'stamp-specification'; // originalfunction pureCompose(...args) {
args.push({staticProperties: {
compose: compose // <- referencing the original compose
}}); return compose.apply(this, args);
}const CuredStamp = pureCompose(InfectedStamp); // un-infected stamp
Double infected stamps
But what if you don’t want to remove first infection? What if you want to add another infection on top of the existing?
Easy!
Here is the first infection.
import compose from 'stamp-specification'; // originalfunction firstInfectionCompose(...args) {
console.log('First infection works!'); args.push({staticProperties: {compose: firstInfectionCompose}}); return compose.apply(this, args);
}const InfectedStamp = firstInfectionCompose();
InfectedStamp.compose();
The code above will print:
First infection works!
First infection works!
Now, we need to wrap the first infected compose with another wrapper.
import compose from 'stamp-specification'; // original// detaching the first infected compose implementation from a stamp
const firstCompose = InfectedStamp.compose;function secondInfectionCompose(...args) {
// calling the detached first infected compose
const TemporaryStamp = firstCompose.apply(this, args); // Now calling the second infection logic
console.log('Second infection works!'); return compose(TemporaryStamp, {staticProperties: {
compose: secondInfectionCompose
}});
}const DoubleInfectedStamp = secondInfectionCompose();
DoubleInfectedStamp.compose();
Will print:
Second infection works!
First infection works!
Second infection works!
First infection works!
The infected compose is a very powerful feature. But, please, remember.
With great power comes great responsibility
UPD: A simpler composition tracking is explained in
Ep 11. Interfering Composition.
The rest of the episodes:
- Episode 1. Stamp basics
- Episode 2. Dependency injection in FP
- Episode 3. Comparing with the ES2015 classes
- Episode 4. Implementing stamps yourself in 30 LOC
- Episode 5. Composition design pattern
- Episode 6. Statics — properties on stamps
- Episode 7. Early and late dependency injection
- Episode 8. Tracking and overriding composition (this article)
- Episode 9. Detaching compose()
- Episode 10. My stamp mental model
- Episode 11. Interfering composition
- Episode 12. New @stamp home
- Episode 13. Method collision control
- Episode 14. New @stamp/it as a replacement of Stampit
- Episode 15. The @stamp/ modules ecosystem
- Episode 16. TypeScript mix-in classes vs Stamps
- Episode 17. Easy 100% unit test coverage in JS
- Episode 18. Dependency injection paradise
- Episode 19. Java/C# abstract methods in JavaScript
- Episode 20. Stampit v4
- Episode 21. Private data in JavaScript. 4 ways using stamps
- Episode 22. JavaScript instanceof as composable stamp
- Episode 23. New stampit.js.org with all the docs
- Episode 24. New “name” feature