Ultra Flexible JavaScript Object Oriented Programming With Stamps

| Comments

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! Stamps1!

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:

1
2
3
4
5
6
7
8
9
10
11
12
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
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:

1
2
3
4
5
6
7
8
// wielding something
const Wieldable = stampit({
  methods: {
    wield(){
      console.log(`You wield ${this.description}`);
    }
  }
});

Something that can be thrown:

1
2
3
4
5
6
7
8
9
// throwing something
const Throwable = stampit({
  methods: {
    throw(target){
      console.log(`You throw the ${this.description} ` +
                  `towards the ${target}`);
    }
  }
});

And something that can be described:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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 like wield, throw and toString
  • props: lets us set a default value for an object description
  • init: allows stamp consumers initialize objects with a given description

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:

1
2
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:

1
2
3
4
5
6
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?

1
2
3
4
5
6
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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:

1
2
3
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():

1
2
3
4
5
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 2. 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 using Object.assign and thus copied over the new object as-is.
  • Properties within the deepProps object are deeply cloned and then merged using Object.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:

  1. obtain a detailed description when under thorough examination (examine)
  2. enchant the weapon with powerful spells and enchantments (enchant)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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:

1
2
3
4
5
6
7
8
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:

1
2
3
4
5
6
7
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:

1
2
3
4
const madeOfIron = {
    weight: '4 stones',
    material: 'iron'
};

And passed it as part of the props object:

1
2
3
4
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:

1
2
3
4
5
6
7
8
9
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):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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:

  1. 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.
  2. A context object with these three properties:
1
2
3
4
5
{
  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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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 of 10

So now, we can call the stamp passing a configuration of our choosing:

1
2
3
4
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:

1
2
3
4
5
6
7
8
9
10
11
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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:

1
2
const Item = stampit()
           .compose(Describable, Weighted, MadeOfMaterial);

And define the new AComposedWeapon stamp in terms of it:

1
2
3
4
5
6
7
8
9
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
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.

1
const Potion = stampit().compose(Item, Throwable, Drinkable);

We can verify that the potion works as we want it to:

1
2
3
4
5
6
7
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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:

1
const Armor = stampit().compose(Item, Wearable, Enchantable);

Let’s take this Armor for a test run and create a powerful steel breastplate of fire:

1
2
3
4
5
6
7
8
9
10
11
12
13
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.
1
2
3
4
5
6
7
8
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:

1
2
3
4
5
6
7
8
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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?

1
2
3
4
5
6
7
8
9
10
11
12
13
// 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!

1
2
3
4
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:

1
2
3
4
5
6
7
8
9
10
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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”}

1
2
3
4
5
6
7
8
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.

A JavaScriptmancy sample cover

More Articles in These Series


  1. 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.

  2. 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.

Comments