Fun with Stamps. Episode 5. Composition design pattern
Hello. I’m developer Vasyl Boroviak and welcome to the fifth episode of Vasyl Boroviak presents Fun with Stamps.
I would like to show how to do the well known Composition Design Pattern using stamps. This Episode will convert the example code from that article (by Damien Lebreuilly) to stamps.
Composition design pattern
In the article Damien is saying that when you compose you might wanna hide some parts of the original implementations:
const CompositeDog = function (poopDelay, eatDelay, pooper, eater) {
const pooperImpl = pooper(poopDelay);
const eaterImpl = eater(eatDelay);
return {
pooperImpl,
eaterImpl,
poop: () => pooperImpl.poop(),
eat: food => eaterImpl.eat(food)
};
});const dog = CompositeDog(2, 1, pooper, eater);
dog.eat(food);
dog.poop();console.log(dog);{ eat: [Function],
poop: [Function],
eaterImpl: {},
pooperImpl: {} }
Let’s see how you can achieve the same using stamps. Additionally, the code will become more reusable and flexible.
Straight forward converting
The CompositeDog is going to be the stamp of a single initializer. It’s API will be identical to the one above.
import {init} from 'stampit';const CompositeDog = init(function (_, {args}) { // using args here
const [poopDelay, eatDelay, pooper, eater] = args;
const pooperImpl = pooper(poopDelay);
const eaterImpl = eater(eatDelay);
return {
pooperImpl,
eaterImpl,
poop: () => pooperImpl.poop(),
eat: food => eaterImpl.eat(food)
};
});const dog = CompositeDog(2, 1, pooper, eater);
dog.eat(food);
dog.poop();
Hold on saying your “booo”. Keep calm and continue. :)
Making the dependencies optional
The CompositeDog API insist that we supply both pooper and eater dependencies. That might be inconvenient sometimes. In fact, I find it annoying to supply all the dependencies each time I need a dog.
First, we are going to make the CompositeDog to accept a single options object instead of the 4 arguments:
const CompositeDog= init(function ({poopDelay, eatDelay, pooper, eater}) {
const pooperImpl = pooper(poopDelay);
const eaterImpl = eater(eatDelay);
return {
pooperImpl,
eaterImpl,
poop: () => pooperImpl.poop(),
eat: food => eaterImpl.eat(food)
};
});const dog= CompositeDog({poopDelay: 2, eatDelay: 1, pooper, eater});
dog.eat(food);
dog.poop();
Now, let’s not overwrite the factory-created object (see the return statement above). Let’s attach the poop and eat methods straight to it:
const CompositeDog= init(function ({poopDelay, eatDelay, pooper, eater}) {
this.pooperImpl = pooper(poopDelay); // attaching implementation
this.eaterImpl = eater(eatDelay); // attaching implementation
this.poop = () => this.pooperImpl.poop(); // attaching method
this.eat = food => this.eaterImpl.eat(food); // attaching method
});const dog= CompositeDog({poopDelay: 2, eatDelay: 1, pooper, eater});
dog.eat(food);
dog.poop();
Now, let’s make the pooper and eater dependencies optional, so that the default implementations are used if someone do not want to supply them:
import {pooper, eater} from './somewhere';const CompositeDog= init(function ({poopDelay, eatDelay, pooper, eater}) {
this.pooperImpl = (pooper || this.pooper)(poopDelay); // note ||
this.poop = () => this.pooperImpl.poop();
this.eaterImpl = (eater || this.eater)(eatDelay); // note ||
this.eat = food => this.eaterImpl.eat(food);
})
.props({pooper, eater}); // default dependenciesconst dog = CompositeDog({poopDelay: 2, eatDelay: 1}); // shorter!
dog.eat(food);
dog.poop();
We have just introduced two new properties to the dog object, — dog.pooper and dog.eater.
console.log(dog);{ eat: [Function],
poop: [Function],
eater: [Function], // <- new public property :(
pooper: [Function], // <- new public property :(
eaterImpl: {},
pooperImpl: {} }
We’ll get rid of them later. Continue reading. :)
Separating pooper and eater concerns
First, let’s remove pooper and eater from the initializer arguments list. The new CompositeDog implementation would look like this:
import {pooper, eater} from './somewhere';const CompositeDog = init(function ({poopDelay, eatDelay}) {
this.pooperImpl = this.pooper(poopDelay); // no more || operator
this.eaterImpl = this.eater(eatDelay); // no more || operator
this.poop = () => this.pooperImpl.poop();
this.eat = food => this.eaterImpl.eat(food);
})
.props({pooper, eater}); // since we have these defaultsconst dog = CompositeDog({poopDelay: 2, eatDelay: 1});
dog.eat(food);
dog.poop();
The only way to supply pooper and eater dependencies would be the compose function, like so:
CompositeDog = CompositeDog.props({pooper, eater});
That is useful in unit testing. In production you continue to use CompositeDog the same way as usual.
Now, let’s separate the two behaviors down to two stamps. Separation of concerns FTW.
import {pooper, eater} from './somewhere';const Pooper = init(function ({poopDelay}) {
this.pooperImpl = this.pooper(poopDelay);
this.poop = () => this.pooperImpl.poop();
})
.props({pooper});const Eater = init(function ({eatDelay}) {
this.eaterImpl = this.eater(eatDelay);
this.eat = food => this.eaterImpl.eat(food);
})
.props({eater});const CompositeDog = stampit(Pooper, Eater); // composing concerns!const dog = CompositeDog({poopDelay: 2, eatDelay: 1});
dog.eat(food);
dog.poop();
Yay! Two compatible stamps. Each can be used separately.
Making the implementations and the dependencies private
Now, let’s make those 4 properties (eater, eaterImpl, pooper, pooperImpl) private. I’ll convert Eater stamp for now. The Pooper stamp is going to be the same.
Hiding eaterImpl is easy:
const Eater = init(function ({eatDelay}) {
const eaterImpl = this.eater(eatDelay); // `eaterImpl` is closured
this.eat = food => eaterImpl.eat(food); // no `this` anymore
})
.props({eater});
Hiding eater dependency:
const Eater = init(function ({eatDelay}) {
const eaterImpl = this.eater(eatDelay);
delete this.eater; // boom! No more public property `eater`
this.eat = food => eaterImpl.eat(food);
})
.props({eater});
If deleting properties is too brutal for you then we can utilize stamp configuration instead of properties:
const Eater = init(function ({eatDelay}, {stamp}) {
const {eater} = stamp.compose.configuration; // <- !!!
const eaterImpl = eater(eatDelay); // <- !!!
this.eat = food => eaterImpl.eat(food);
})
.conf({eater}); // using configuration
Done.
The Pooper implementation would be very similar:
const Pooper = init(function ({poopDelay}, {stamp}) {
const {pooper} = stamp.compose.configuration;
const pooperImpl = pooper(poopDelay);
this.poop = food => pooperImpl.poop();
})
.conf({pooper});
The final CompositeDog would be this:
const CompositeDog = stampit(Pooper, Eater);const dog = CompositeDog({poopDelay: 2, eatDelay: 1});
dog.eat(food);
dog.poop();console.log(dog);Object { eat: [Function], poop: [Function] }
There are only two necessary properties (functions) the dog object has.
To override the default dependencies you would need to supply configuration instead of properties:
CompositeDog = CompositeDog.conf(
{pooper, eater} // overriding the default dependencies
);
The final full listing
import {pooper, eater} from './somewhere';
import stampit, {init} from 'stampit';const Eater = init(function ({eatDelay}, {stamp}) {
const {eater} = stamp.compose.configuration;
const eaterImpl = eater(eatDelay);
this.eat = food => eaterImpl.eat(food);
})
.conf({eater});const Pooper = init(function ({poopDelay}, {stamp}) {
const {pooper} = stamp.compose.configuration;
const pooperImpl = pooper(poopDelay);
this.poop = food => pooperImpl.poop();
})
.conf({pooper});const CompositeDog = stampit(Pooper, Eater);const dog = CompositeDog({poopDelay: 2, eatDelay: 1});
dog.eat(food);
dog.poop();console.log(dog);Object { eat: [Function], poop: [Function] }
Unit testing:
const MockedCompositeDog = CompositeDog.conf(
{pooper: mockedPooper, eater: mockedEater}
);const dog = MockedCompositeDog({poopDelay: 2, eatDelay: 1});
dog.eat(food);
dog.poop();
Dramatic pause…
The results:
- No unnecessary dependencies or implementations are sticking out.
- Pooping and eating concerns are separate now.
- The Pooper and Eater factories are reusable (composable).
- The Pooper and Eater can be used separately as a standalone factory.
- The dependencies are optional.
Pit falls.
- The new code depends on the <2kb of “stampit” module.
- The stamps paradigm is too new.
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 (this article)
- Episode 6. Statics — properties on stamps
- Episode 7. Early and late dependency injection
- Episode 8. Tracking and overriding composition
- 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