Ultra Flexible JavaScript Object Oriented Programming with Stamps
Updated 16th October 2016 with Stamps v3! Yey!
The Mastering the Arcane Art of JavaScript-mancy series are my humble attempt at bringing my love for JavaScript to all other C# developers that haven’t yet discovered how awesome this language and its whole ecosystem are. These articles are excerpts of the super duper awesome JavaScript-Mancy book a compendium of all things JavaScript for C# developers.
In the last two articles of the series you learned about two great alternatives to classical object oriented programming: mixins and traits. Both techniques embrace the dynamic nature of JavaScript. Both encourage creating small reusable components that you can either mix with your existing objects or compose together to create new objects from scratch.
Object and functional mixins are the simplest approach to object composition. Together with Object.assign
they make super easy to create small units of reusable behavior and augment your domain objects with them.
Traits continue the tradition of composability of mixins adding an extra layer of expressiveness and safety on top. They let you define required properties, resolve naming conflicts and they warn you whenever you’ve failed to compose your traits properly.
In this chapter you’ll learn a new technique to achieve class-free inheritance through object composition. This particular technique embraces not only the dynamic nature of JavaScript but also its many ways to achieve code reuse: prototypes, mixins and closures. Behold! Stampsstamps!
What are Stamps?
Stamps are composable factory functions. Just like regular factory functions they let you create new objects but, lo and behold, they also have the earth shattering ability to compose themselves with each other. Factory composition unlocks a world of possibilities and a whole new paradigm of class-free object oriented programming as you’ll soon see.
Imagine that you have a factory function to create swords that you can wield:
const Sword = () =>
({
description: 'common sword',
toString(){ return this.description;},
wield(){
console.log(`You wield ${this.description}`);
}
});
const sword = new Sword();
sword.wield();
// => You wield the common sword
And another one that creates deadly knives that you can throw:
const Knife = () =>
({
description: 'rusty knife',
toString(){ return this.description},
throw(target){
console.log(`You throw the ${this.description} ` +
`towards the ${target}`);
}
});
const knife = new Knife();
knife.throw('orc');
// => You throw the rusty knife towards the orc
Wouldn’t it be great to have a way to combine that wielding with the throwing so you can wield a knife? Or throw a sword? That’s exactly what stamps let you do. With stamps you can define factory functions that encapsulate pieces of behavior and later compose them with each other.
Before we start composing, let’s break down both Sword
and Knife
into the separate behaviors that define them. Each of these behaviors will be represented by a separate stamp that we’ll create with the aid of the stampit
function, the core API of the stampit library.
So, we create stamps for something that can be wielded:
// wielding something
const Wieldable = stampit({
methods: {
wield(){
console.log(`You wield ${this.description}`);
}
}
});
Something that can be thrown:
// throwing something
const Throwable = stampit({
methods: {
throw(target){
console.log(`You throw the ${this.description} ` +
`towards the ${target}`);
}
}
});
And something that can be described:
// or describing something
const Describable = stampit({
methods: {
toString(){
return this.description;
}
},
// default description
props: {
description: 'something'
},
// let's you initialize description
init({description=this.description}){
this.description = description;
}
});
In the examples above, we use the stampit
function to create three different stamps: Wieldable
, Throwable
and Describable
. The stampit
function takes a configuration object that represents how objects should be created and produces a factory that uses that configuration to create new objects.
In our example we use different properties of the configuration object to create our stamps:
methods
: allows us to define the methods that an object should have likewield
,throw
andtoString
props
: lets us set a default value for an objectdescription
init
: allows stamp consumers initialize objects with a givendescription
As a result, the Wieldable
stamp creates objects that have a wield
method, the Throwable
stamp creates objects with a throw
method and so on.
Once you have defined these stamps you can compose them together into yet another stamp that represents a weapon by using the compose
method:
const Weapon = stampit()
.compose(Describable, Wieldable, Throwable);
Now you can use this stamp Weapon
, that works just like a factory, to create (or stamp) new mighty weapons that you’ll be able to wield, throw and describe.
Let’s start with a mighty sword:
const anotherSword = Weapon({description: 'migthy sword'});
anotherSword.wield();
// => You wield the mighty sword
anotherSword.throw('ice wyvern');
// => You throw the mighty sword towards the ice wyvern
Notice how we pass an object with a description
property to the stamp? This is the description
that will be forwarded to the init
method of the Describable
stamp we defined earlier.
And what about a sacrificial knife?
const anotherKnife = Weapon({description: 'sacrificial knife'});
anotherKnife.wield();
// => You wield the sacrificial knife
anotherKnife.throw('heart of the witch');
// => You throw the sacrificial knife towards the heart of the witch
Yey! We did it! Using stamps we were able to create factories for stuff that can be wielded, thrown and described, and compose them together to create a sword and a knife that can be both wielded and thrown. These examples only scratch the surface of the capabilities of stamps. There’s much more in store for you. Let’s continue!
Stamps OOP Embraces JavaScript
A very interesting feature of stamps that differentiates them from other existing approaches to object composition is that they truly embrace JavaScript strengths and idioms. Stamps use:
- Prototypical inheritance so that you can take advantage of prototypes to share methods between objects.
- Mixins so that you can compose pieces of behavior with your stamps, use defaults, initialize or override properties.
- Closures so that you can achieve data privacy and only expose it through the interface of your choice.
Moreover, stamps wrap all of this goodness in a very straightforward declarative interface:
let stamp = stampit({
// methods inherited via prototypical inheritance
methods: {...},
// properties and methods mixed in via Object.assign (mixin)
props: {...},
// properties and methods mixed in through a recursive algorithm
// (deeply cloned mixin)
deepProps: {...}
// closure that can contain private members
init: function(arg0, context){...},
});
Stamps By Example
Let’s continue with the example of the swords and the knives - we need to arm our army after all if we want to defeat The Red Hand - and go through each of the different features provided by stamps.
In the upcoming sections, we will define the weapon stamp from scratch using new stamp options as the need arises and showcasing how stamps take advantage of different JavaScript features.
Prototypical Inheritance and Stamps
We will start by defining some common methods that could be shared across all weapons and therefore it makes sense to place them in a prototype. In order to do that we use the methods
property you saw in previous examples:
const AWeapon = stampit({
methods: {
toString(){
return this.description;
},
wield(target){
console.log(`You wield the ${this} ` +
`and attack the ${target}`);
},
throw(target){
console.log(`You throw the ${this} ` +
`at the ${target}`);
}
}
});
We now have a AWeapon
stamp with three methods toString
, wield
and throw
that we can use to instantiate new weapons like this sword:
const aSword = AWeapon({description: 'a sword'});
aSword.wield('giant rat');
// => You wield the sword and attack the giant rat
You can verify that all these methods that we include within the methods
property are part of the aSword
object prototype using Object.getPrototypeOf()
:
console.log(Object.getPrototypeOf(aSword));
// => [object Object] {
// throw:....
// toString:...
// wield:...}
The Object.getPrototypeOf
method returns the prototype of the aSword
object which, as we expected, includes all the methods we are looking for: throw
, wield
and toString
.
Mixins and Stamps
The props
and deepProps
properties let you define the properties or methods that will be part of each object created via stamps props. Both properties define an object mixin that will be composed with the object being created by the stamp:
- Properties within the
props
object are merged usingObject.assign
and thus copied over the new object as-is. - Properties within the
deepProps
object are deeply cloned and then merged usingObject.assign
which guarantees that no state is shared between objects created via the stamp. This is very important if you have properties with objects or arrays since you don’t want state changes in one object affecting other objects.
We can expand the previous weapon example using props
and deepProps
to add new functionality to our weapon. The abilities to:
- obtain a detailed description when under thorough examination (
examine
) - enchant the weapon with powerful spells and enchantments (
enchant
)
const AWeightedWeapon = stampit({
props: {
// 1. props part of examining ability
weight: '4 stones',
material: 'iron',
description: 'weapon'
},
deepProps: {
// 2. deep props part of the enchanting ability
enchantments: []
},
methods: {
/*
// collapsed to save space
toString(){...},
wield(target){...},
throw(target){...},
*/
examine(){
console.log(`You examine the ${this}.
It's made of ${this.material} and ${this.examineWeight()}.
${this.examineEnchantments()}.`)
},
examineWeight(){
const weight = Number.parseInt(this.weight);
if (weight > 5) return 'it feels heavy';
if (weight > 3) return 'it feels light';
return 'it feels surprisingly light';
},
examineEnchantments(){
if (this.enchantments.length === 0)
return 'It is not enchanted.';
return `It seems enchanted: ${this.enchantments}`;
},
enchant(enchantment){
console.log(`You enchant the ${this} with ${enchantment}`);
this.enchantments.push(enchantment)
}
},
init({description = this.description}){
this.description = description;
}
});
Now we can examine
our weapons that, from this moment forward, will have a weight
and be made of some material
:
const aWeightedSword = AWeightedWeapon({
description: 'sword of iron-ness'
});
aWeightedSword.examine();
// => You examine the sword of iron-ness.
// It's made of iron and it feels light.
// It is not enchanted.
And even enchant
them with powerful spells:
aWeightedSword.enchant('speed +1');
// => You enchant the sword of iron-ness with speed +1
aWeightedSword.examine();
// => You examine the sword of iron-ness.
// It's made of iron and it feels light.
// It seems enchanted: speed +1.
It is interesting to point out that the object passed as an argument to the stamp function when creating a new object will be passed forward to all defined initializers (init
methods). For instance, in the example above we called the AWeightedWeapon
stamp with the object {description: 'sword of iron-ness'}
. This object was passed to the stamp init
method and used to initialize the weapon description for the resulting aWeightedSword
object. Had there been more stamps with init
methods, this object would have been passed as an argument to each one of them.
In addition to using props
and deepProps
to define the shape of an object created via stamps, we can use them in combination with the same mixins we saw in previous chapters. That is, we can take advantage of previously defined mixins that represent a behavior and compose them with our newly created stamps.
For instance, we could have defined a reusable madeOfIron
mixin:
const madeOfIron = {
weight: '4 stones',
material: 'iron'
};
And passed it as part of the props
object:
const AnIronWeapon = stampit({
props: madeOfIron,
...
});
This object composition is even easier to achieve using the stamp fluent API that we’ll examine in detail in later sections:
const AnHeavyIronHolyWeapon =
// A weighted weapon
AWeightedWeapon
// compose with madeOfIron mixin
.props(madeOfIron)
// compose with veryHeavyWeapon mixin
.props(veryHeavyWeapon)
// compose with deeply cloned version of holyEnchantments mixin
.deepProps(holyEnchantments);
Data Privacy and Stamps
Let’s imagine that we don’t want to expose to everyone how we have implemented the enchanting weapons engine, so that, we can change and optimize it in the future. Is there a way to make that information private? Yes, indeed there is. Stamps support data privacy by using closures through the init
property.
Let’s take the previous weapon example and make our enchantment implementation private. We’ll do that by moving the enchantments
property from the public API (props
) into the init
function where it’ll be isolated from the outside world. Since the manner of accessing this private enchantments
property is via closures, we’ll need to move all methods that need to have a access to the property inside the init
function as well (examineEnchantments
and enchant
):
const APrivateWeapon = stampit({
methods: {
/*
like in previous examples
toString(){...},
wield(target){...},
throw(target){...},
*/
examine(){
console.log(`You examine the ${this}.
It's made of ${this.material} and ${this.examineWeight()}.
${this.examineEnchantments()}.`)
},
examineWeight(){
const weight = Number.parseInt(this.weight);
if (weight > 5) return 'it feels heavy';
if (weight > 3) return 'it feels light';
return 'it feels surprisingly light';
}
},
props: {
weight: '4 stones',
material: 'iron',
description: 'weapon'
},
init: function({description=this.description}){
// this private variable is the one being enclosed
const enchantments = [];
this.description = description;
// augment object being created
// with examineEnchantments and enchant
// methods
Object.assign(this, {
examineEnchantments(){
if (enchantments.length === 0) return 'It is not enchanted.';
return `It seems enchanted: ${enchantments}`;
},
enchant(enchantment){
console.log(`You enchant the ${this} with ${enchantment}`);
enchantments.push(enchantment)
}
});
}
});
The init
function will be called during the creation of an object with the object itself as context (this
). This will allow us to augment the object with the examineEnchantments
and enchant
methods that enclose the enchantments
property. As a result, when we create an object using this stamp, it will have a private variable enchantments
that can only be operated through these methods.
Having defined this new stamp we can verify how indeed the enchantments
property is now private:
const aPrivateWeapon = APrivateWeapon({
description: 'sword of privacy'
});
console.log(aPrivateWeapon.enchantments);
// => undefined;
aPrivateWeapon.examine();
// => You examine the sword of privacy.
//It's made of iron and it feels light.
//It is not enchanted.
aPrivateWeapon.enchant('privacy: wielder cannot be detected');
// => You enchant the sword of privacy with privacy:
// wielder cannot be detected
aPrivateWeapon.examine();
// => You examine the sword of privacy.
// It's made of iron and it feels light.
// It seems enchanted: privacy: wielder cannot be detected.
In addition to helping you with information hiding, the init
function adds an extra degree of flexibility by allowing you to provide additional arguments that affect object creation.
The init
function takes two arguments:
- The first argument passed to the stamp during object creation. This is generally an
options
object with properties that will be used when creating an object. - A context object with these three properties:
{
instance, // the instance being created
stamp, // the stamp
args // arguments passed to the stamp during object creation
}
So we can redefine our init
function to, for instance, limit the number of enchantments
allowed for a given weapon:
const ALimitedEnchantedWeapon = stampit({
methods: {
/*
// Same as in previous examples
toString(){...},
wield(target){...},
throw(target){...},
examine(){...},
examineWeight(){...}
*/
},
props: {
weight: '4 stones',
material: 'iron',
description: 'weapon'
},
init: function({ /* options object */
description = this.description,
maxNumberOfEnchantments = 10
}){
// this private variable is the one being enclosed
const enchantments = [];
this.description = description;
Object.assign(this, {
examineEnchantments(){
if (enchantments.length === 0) return 'It is not enchanted.';
return `It seems enchanted: ${enchantments}`;
},
enchant(enchantment){
if(enchantments.length === maxNumberOfEnchantments) {
console.log('Oh no! This weapon cannot ' +
'be enchanted any more!');
} else {
console.log(`You enchant the ${this} with ${enchantment}`);
enchantments.push(enchantment);
}
}
});
}
});
In this example we have updated the init
method to unwrap the arguments being passed to the stamp function. The method now expects the first argument to be an options
object that contains:
- a
description
- a
maxNumberOfEnchantments
variable that will determine how many enchantments a weapon can hold. If it hasn’t been defined it defaults to a value of10
So now, we can call the stamp passing a configuration of our choosing:
const onlyOneEnchanmentWeapon = ALimitedEnchantedWeapon({
description: 'sword of one enchanment',
maxNumberOfEnchantments: 1
});
As we mentioned earlier, this options
object will be passed in to the init
function as its first argument resulting in a weapon that can only hold a single enchantment:
onlyOneEnchanmentWeapon.examine();
// => You examine the sword of privacy.
//It's made of iron and it feels light.
//It is not enchanted.
onlyOneEnchanmentWeapon.enchant('luck +1');
// => You enchant the sword of one enchanment with luck +1
onlyOneEnchanmentWeapon.enchant(
'touch of gold: everything you touch becomes gold');
// => Oh no! This weapon cannot be enchanted any more!
As you could appreciate in this example, the init
function adds a lot of flexibility to your stamps as it allows you to configure them via additional parameters during creation such as maxNumberOfEnchantments
.
Stamp Composition
Stamps are great at composition. On one hand you compose prototypes, mixins and closures to produce a single stamp. On the other, you can compose stamps with each other just like you saw in the introduction to this chapter with the words, knives, the wielding and the throwing.
Let’s take a closer look at stamp composition. Following the weapons example from previous sections, imagine that all of the sudden we need a way to represent potions and armors.
What do we do?
Well, we can start by factoring the weapon stamp into smaller reusable behaviors also represented as stamps. We have the Throwable
, Wieldable
and Describable
behaviors we defined at the beginning of the chapter:
const Throwable = stampit({
methods: {
throw(target){
console.log(`You throw the ${this.description} ` +
`towards the ${target}`);
}
}
});
// wielding something
const Wieldable = stampit({
methods: {
wield(target){
console.log(`You wield the ${this.description} ` +
`and attack the ${target}`);
}
}
});
// or describing something
const Describable = stampit({
methods: {
toString(){
return this.description;
}
},
props: {
description: 'something'
},
init({description=this.description}){
this.description = description;
}
});
We can define new Weighted
and MadeOfMaterial
stamps to represent something that has weight and something which is made of some sort of material:
const Weighted = stampit({
methods: {
examineWeight(){
const weight = Number.parseInt(this.weight);
if (weight > 5) return 'it feels heavy';
if (weight > 3) return 'it feels light';
return 'it feels surprisingly light';
}
},
props: {
weight: '4 stones'
},
init({weight=this.weight}){
this.weight = weight;
}
});
const MadeOfMaterial = stampit({
methods: {
examineMaterial(){
return `It's made of ${this.material}`;
}
},
props: {
material: 'iron'
},
init({material=this.material}){
this.material = material;
}
});
And finally an Enchantable
stamp to represent something that can be enchanted:
const Enchantable = stampit({
init: function({maxNumberOfEnchantments=10}){
// this private variable is the one being enclosed
const enchantments = [];
Object.assign(this, {
examineEnchantments(){
if (enchantments.length === 0) return 'It is not enchanted.';
return `It seems enchanted: ${enchantments}`;
},
enchant(enchantment){
if(enchantments.length === maxNumberOfEnchantments) {
console.log('Oh no! This weapon cannot be enchanted ' +
'any more!');
} else {
console.log(`You enchant the ${this} with ${enchantment}`);
enchantments.push(enchantment);
}
}
});
}
});
Now that we have identified all these reusable behaviors we can start composing them together. We could wrap the most fundamental behaviors in an Item
stamp:
const Item = stampit()
.compose(Describable, Weighted, MadeOfMaterial);
And define the new AComposedWeapon
stamp in terms of it:
const AComposedWeapon = stampit({
methods: {
examine(){
console.log(`You examine the ${this}.
${this.examineMaterial()} and ${this.examineWeight()}.
${this.examineEnchantments()}.`)
},
}
}).compose(Item, Wieldable, Throwable, Enchantable);
This reads very nicely. A Weapon is an Item that you can Wield, Throw and Enchant.
If we define a weapon using this new stamp we can verify how everything works just like it did before the factoring:
// now we can use the new weapon as before
const swordOfTruth = AComposedWeapon({
description: 'The Sword of Truth'
});
swordOfTruth.examine();
// => You examine the The Sword of Truth.
// It's made of iron and it feels light.
// It is not enchanted.."
swordOfTruth.enchant("demon slaying +10");
// => You enchant the The Sword of Truth with demon slaying +10
swordOfTruth.examine();
// => You examine the The Sword of Truth.
// It's made of iron and it feels light.
// It seems enchanted: demon slaying +10.
Now we can combine these behaviors together with new ones to define the Potion
and Armor
stamps.
A potion would be something that can be drunk and which has some sort of effect on the drinker. For instance, if we create a new stamp to represent something that can be drunk:
const Drinkable = stampit({
methods: {
drink(){
console.log(`You drink the ${this}. ${this.effects}`);
}
},
props: {
effects: 'It has no visible effect'
},
init({effects=this.effects}){
this.effects = effects;
}
});
We can define a potions as follows: An Item that you can Throw and Drink.
const Potion = stampit().compose(Item, Throwable, Drinkable);
We can verify that the potion works as we want it to:
const healingPotion = Potion({
description: 'Potion of minor healing',
effects: 'You heal 50 hp (+50hp)!'
});
healingPotion.drink();
// => You drink the Potion of minor healing. You heal 50 hp (+50hp)!
On the other hand, an armor would be something that you could wear and which would offer some protection. Let’s define a Wearable
behavior:
const Wearable = stampit({
methods: {
wear(){
console.log(`You wear ${this} in your ` +
`${this.position} gaining +${this.protection} ` +
`armor protection.`);
}
},
props: { // these act as defaults
position: 'chest',
protection: 50
},
init({position=this.position, protection=this.protection}){
this.position = position;
this.protection = protection;
}
});
And now an Armor is an Item that you can Wear and Enchant:
const Armor = stampit().compose(Item, Wearable, Enchantable);
Let’s take this Armor
for a test run and create a powerful steel breastplate of fire:
const steelBreastPlateOfFire = Armor({
description: 'Steel Breastplate of Fire',
material: 'steel',
weight: '50 stones',
});
steelBreastPlateOfFire.enchant('Fire resistance +100');
// => You enchant the Steel Breastplate of Fire with
// Fire resistance +100
steelBreastPlateOfFire.wear();
// => You wear Steel Breastplate of Fire in your chest
// gaining +50 armor protection.
const Armor = stampit().compose(Item, Wearable, Enchantable);
// => an armor is an item that you can wear and that can be enchanted
const Weapon = stampit().compose(Item, Throwable, Wieldable);
// => a weapon is an item that you can throw or wield
const Potion = stampit().compose(Item, Drinkable, Throwable);
// => a potion is an item that you can drink or throw
Pretty cool right? You end up with a very declarative, readable, flexible and extensible way to work with objects. Now imagine how much work and additional code you would have needed to implement the same solution using classical inheritance.
Prototypical Inheritance When Composing Stamps
You may be wondering… What happens with prototypical inheritance when you compose two stamps? Does stampit create multiple prototypes and establish a prototype chain between them?
The answer is no, whenever you compose stamps all the different methods assigned to the methods
property in each stamp are flattened into a singular prototype.
Let’s illustrate this with an example. Imagine that you want to define elemental weapons that let you perform mighty elemental attacks. In order to do this you compose the existing AComposedWeapon
stamp with a new stamp that has the elementalAttack
method:
const ElementalWeapon = stampit({
methods: {
elementalAttack(target){
console.log(`You wield the ${this.description} and perform ` +
`a terrifying elemental attack on the ${target}`);
}
}
}).compose(AComposedWeapon);
When you instantiate a new sword of fire you can readily verify how the aFireSword
object does not have a prototype with a single elementalAttack
method. Instead, the prototype contains all methods defined in all stamps that have being composed to create ElementalWeapon
:
const aFireSword = ElementalWeapon({
description: 'magic sword of fire'
});
console.log(Object.getPrototypeOf(aFireSword));
// => [object Object] {
// elementalAttack: ...
// examine: ...
// examineMaterial: ...
// examineWeight: ...
// throw: ...
// toString: ...
// wield: ...
// }
If there are naming collisions between composed stamps the last one wins and overwrites the conflicting method, just like with Object.assign
.
Data Privacy When Composing Stamps
Another interesting advantage of using closures to define private data and being able to later compose stamps with each other is that private data doesn’t collide. If you have a private member with the same name in two different stamps and you compose them together they will act as two completely different variables.
Let’s illustrate this with another example (example craze!!). If you remember from previous sections the AComposedWeapon
stamp allowed weapons to be enchanted (via the Enchanted
stamp) and stored these magic spells inside a private variable called enchantments
. What would happen if we were to rewrite our elemental weapon to also have a private property called enchantments
?
// We redefine the elemental weapon to store its
// elemental properties as enchantments of some sort:
const AnElementalWeapon = stampit({
init({enchantments=[]}){
Object.assign(this, {
elementalAttack(target){
console.log(`You wield the ${this.description} and ` +
`perform a terrifying elemental attack of ` +
`${enchantments} on the ${target}`);
}});
}
}).compose(AComposedWeapon);
In this example we have redefined the element weapon to store its powers like an enchantment (that is, inside an enchantments
array). We moved the elementalAttack
method from the methods
properties to the init
property so that it will enclose the enchantments
private member that will, from now on, store the elemental attack.
We go ahead and create a new super elemental weapon: an igneous lance!
const igneousLance = AnElementalWeapon({
description: 'igneous Lance',
enchantments: ['fire']
});
But what happens with this lance that effectively has two enchantments
private members (from the AnElementalWeapon
and Enchanted
stamps)? Well, we can easily verify that they do not affect each other by putting the lance into action:
igneousLance.elementalAttack('rabbit');
// => You wield the igneous Lance and perform a
// terrifying elemental attack of fire on the rabbit
igneousLance.enchant('protect + 1');
// => You enchant the igneous Lance with protect + 1
igneousLance.elementalAttack('goat');
// => You wield the igneous Lance and perform a
// terrifying elemental attack of fire on the goat
Why don’t the enchantments
variables collide? Even though I often use the word private members to refer to these variables, the reality is that they are not part of the object being created by the stamps. Different enchantments
variables are enclosed by the enchant
and elementalAttack
functions and it is these two different values that are used when calling these two functions. Since they are two different variables that belong to two completely different scopes no collision takes place even though both variables have the same name.
Stamp Fluent API
In addition to the API that we’ve used in the previous examples where you pass a configuration object to the stampit
method:
const stamp = stampit({
// methods inherited via prototypical inheritance
methods: {...},
// properties and methods mixed in via Object.assign (mixin)
props: {...},
// closure that can contain private members
init(options, context){...},
// properties and methods mixed in through a recursive algorithm
// (deeply cloned mixin)
deepProps: {...}
});
You can use the fluent interface if it is more to your liking:
const stamp = stampit().
// methods inherited via prototypical inheritance
methods({...}).
// properties and methods mixed in via Object.assign (mixin)
props({...}).
// closure that can contain private members
init(function(options, context){...}).
// properties and methods mixed in through a recursive algorithm
// (deeply cloned mixin)
deepProps({...}).
// compose with other stamps
compose(...);
For instance, we can redefine the Armor
stamp as a chain of methods using this new interface:
const FluentArmor = stampit()
.methods({
wear(){
console.log(`You wear ${this} in your ` +
`${this.position} gaining +${this.protection} ` +
`armor protection.`);
}})
.props({
// these act as defaults
position: 'chest',
protection: 50
})
.init(function init({
position=this.position,
protection=this.protection}){
this.position = position;
this.protection = protection;
})
.compose(Item, Enchantable);
Which works just like you’d expect:
{lang=“javascript”}
const fluentArmor = FluentArmor({
description: 'leather jacket',
protection: 70
});
fluentArmor.wear();
// => You wear leather jacket in your chest
// gaining +70 armor protection
It is important to understand that each method of the fluent interface returns a new stamp. That is, you don’t modify the current stamp but go creating new stamps with added capabilities as you go adding more methods. This makes the fluent interface particularly useful when you want to build on top of existing stamps or behaviors.
Concluding: Stamps vs Mixins vs Traits
Stamps are like mixins on steroids. They offer a great declarative API to create and compose your factories of objects (stamps) with baked in support for composing prototypes, mixing in features, deep copying composition and private variables.
Stamps truly embrace the nature of JavaScript and take advantage of all of its object oriented programming techniques: prototypical inheritance, concatenative inheritance with mixins and information hiding through closures.
The only drawback in comparison with mixins is that they require that you use a third party library whereas Object.assign
is native to JavaScript.
In relation to traits, these still offer a safer composition experience with support for required properties and proactive name conflict resolution.
Be it mixins, traits or stamps, they are all awesome techniques to make your object oriented programming more modular, reusable, flexible and extensible, really taking advantage of the dynamic nature of JavaScript.
This chapter wraps the different object composition techniques that I wanted to offer to you as an alternative to classical object oriented programming. I hope you have enjoyed learning about them and are at least a little bit curious to try them out in your next project.
Interested in Learning More JavaScript? Buy the Book!
Are you a C# Developer interested in learning JavaScript? Then take a look at the JavaScript-mancy book, a complete compendium of JavaScript for C# developers.
Have a great week ahead!! :)
More Articles In This Series
- Chapter 0: The Basic Ingredients of JavaScriptMancy
- Chapter 1: The Many a One JavaScript Quirks
- Functions
- Object Oriented Programming
- Basics
- ES6
- OOP
- Introduction to OOP in JavaScript for C# Developers
- Summoning Fundamentals: Introduction to OOP In JavaScript – Encapsulation
- Summoning Fundamentals: Introduction to OOP In JavaScript – Inheritance
- Summoning Fundamentals: Introduction to OOP In JavaScript – Polymorphism
- White Tower Summoning: Mimicking C# Classical Inheritance in Javascript
- White Tower Summoning Enhanced: The Marvels of ES6 Classes
- Black Tower Summoning: Object Composition with Mixins
- Safer JavaScript Composition With Traits and Traits.js
- JavaScript Ultra Flexible Object Oriented Programming with Stamps
- Object Oriented JavaScript for C# Programmers
- Tooling
- Data Structures
- Functional Programming
- And more stuff to be shaped out:
- Functional Programming with JavaScript
- Modules
- Asynchronous Programming
- The JavaScript Ecosystem
- Stamps were initially devised by a mythical figure in the JavaScript world: Eric Eliott. If you have some time to spare go check his stuff at ericelliottjs.com or JavaScript Scene.↩
- These properties or methods are part of the object itself as opposed to being part of the prototype. Therefore they won’t be shared across all instances created using a stamp.↩
Written by Jaime González García , dad, husband, software engineer, ux designer, amateur pixel artist, tinkerer and master of the arcane arts. You can also find him on Twitter jabbering about random stuff.