Fun with Stamps. Episode 3. Comparing with the ES2015 classes

Vasyl Boroviak
5 min readMay 8, 2016

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

I, and many others, believe that classic OOP inheritance is bad or even evil.

Favor object composition over class inheritance. (C) GoF

Stamps are more like mixins, but instead of objects it “mixes” factory functions. In this article I will show and compare a class with a stamp implementing the same logic.

Class inheritance example

Standard classic OOP class and a child class. Many of us are familiar with the syntax.

class Point {
constructor({x, y}) {
this.x = x;
this.y = y;
}

toString() {
return `(${this.x}, ${this.y})';
}
}
class ColorPoint extends Point {
constructor({x, y, color}) {
super({x, y});
this.color = color;
}

toString() {
return `${super.toString()} in ${this.color}`;
}
}

const cp = new ColorPoint({x: 25, y: 8, color: 'green'});
cp.toString(); // '(25, 8) in green'

Rewriting example to stamps

Here is the same code using pure stamps. (Scroll down for shorter syntax version.)

import compose from '@stamp/compose'; // stamp implementationconst Coords = compose({
initializers: [function ({x, y}) {
this.x = x;
this.y = y;
}],
methods: {
coordsToString() {
return `(${this.x}, ${this.y})`;
}
}
});
const Point = compose(Coords, { methods: {
toString() {
return this.coordsToString();
}
}});
const Color = compose({
initializers: [function ({color}) {
this.color = color;
}]
});
const ColorPoint = compose(Color, Point, { methods: {
toString() {
return this.coordsToString() + ' in ' + this.color;
}
}});
const cp = ColorPoint({ x: 25, y: 8, color: 'green' });
cp.toString(); // '(25, 8) in green'

I had to introduce coordsToString function. Stamps deliberately do not allow “super” calls. There is no such thing as “super” in stamps by design.

Now, let’s see how short it can be using the @stamp/ — set of modules which simplify stamps usage.

import {argOverProp} from '@stamp/arg-over-prop';const Coords = argOverProp({x: 0, y: 0}).compose({ methods: {
coordsToString() {
return `(${this.x}, ${this.y})`;
}
}});
const Point = Coords.compose({ methods: {
toString() {
return this.coordsToString();
}
}});
const Color = ArgOverProp.argOverProp('color');const ColorPoint = Color.compose(Point, { methods: {
toString() {
return this.coordsToString() + ' in ' + this.color;
}
}});
const cp = ColorPoint({ x: 25, y: 8, color: 'green' });
cp.toString(); // '(25, 8) in green'

This code is also 20 lines long, as the original class-based code. Although, there are few benefits.

  1. Every capitalized variable (aka stamp, aka composable) is a factory. It creates objects. For example, you can use the Coords factory to create objects with the x and y properties and the coordsToString method.
  2. Also, every stamp can be composed with other stamps. For example, you can create a factory which creates objects without the toString functions — compose(Coords, Color).
  3. Every stamp can be reused separately in other stamps. For example, you can reuse the Color for a Circle or a Square stamp.
  4. Each of the 4 stamps can be easily unit tested separately.

Downsides:

  1. Reading the code it’s hard to understand what’s going on without knowing the new Stamp paradigm.

Constructors

Stamps replace the ES2015 constructors with initializers. A stamp can have as many initializers as necessary.

class Point {
constructor({x, y}) {
this.x = x;
this.y = y;
}
}
const point = new Point({x: 13, y: 42});

The same implemented with pure stamps:

const Point = compose({
initializers: [function ({x, y}) {
this.x = x;
this.y = y;
}]
});
const point = Point({x: 13, y: 42});

The same implemented using the ArgOverProp stamp:

import {argOverProp} from '@stamp/arg-over-prop';const Point = argOverProp('x', 'y');const point = Point({x: 13, y: 42});

The argOverProp function can be easily reimplemented yourself. It returns a stamp which has that single initializer similar to the one above.

function argOverProp(...args) {
return compose({initializers: [function(options) {
args.forEach(prop => this[prop] = options[prop]);
}]});
}

The arvOverProp is not the only handy utility function of the stamp ecosystem.

Constructor arguments

What if a stamp has multiple initializers? They might expect different arguments.

const Stamp = compose({initializers: [
function({num}) { // expecting a number
console.log(num);
},
function({str}) { // expecting a string
console.log(str);
}
]});
const obj = Stamp(???); // what should I pass?

You should pass an options object.

const obj = Stamp({
num: 6.26,
str: 'a string value'
});

And your initializers should destructure the first argument.

const Stamp = compose({initializers: [
function({num}) { // destructuring the options
console.log(num);
},
function({str}) { // destructuring the options
console.log(str);
}
]});

This is a convention of stamp reusability. If you want your stamp to be compatible with other stamps then you have to expect the first initializer argument to be the options object.

Multiple arguments

Stamps are factory functions. Sometimes you need to pass more than a single argument to your factory. Here is how an initializer can get access to all the arguments:

const Stamp = compose({initializers: [
function(options, {args}) { // getting all the factory arguments
console.log('arguments are:', args);
}
]});
Stamp(5, 'green', {}); // arguments are: [ 5, 'green', {} ]

You can use the init utility function to get the same stamp but with somewhat shorter syntax:

import {init} from 'stampit';const Stamp = init((options, {args}) => 
console.log('arguments are:', args)
);
Stamp(5, 'green', {}); // arguments are: [ 5, 'green', {} ]

Private state

To have a private state with stamps you should use initializers:

const HavePrivate = compose({
initializers: [function(key) {
const myPrivate = { secretKey: key }; // private state
this.printPrivate = () => console.log(myPrivate);
}]
});
const obj = HavePrivate('42');
obj.printPrivate(); // { secretKey: '42' }

As you can see we are attaching the printPrivate method to this object. The method is closured and have access to the myPrivate variable.

Alternative way to have private state — @stamp/privatize

In the stamp ecosystem you can find the @stamp/privatize stamp which can add private properties and methods to your stamps. It creates a proxy object which wraps methods of your stamp(s):

const Original = compose({
properties: {password: '123'},
methods: {
setPassword(value) { this.password = value; },
getPassword() { return this.password; }
}
});
import {privatizeMethods} from '@stamp/privatize';const Stamp = Original.compose(privatizeMethods('setPassword'));
const obj = Stamp();
console.log(obj.password); // undefined
console.log(obj.setPassword); // undefined
console.log(obj.getPassword()); // "123"

Conclusion

Stamps are better than the ES2015 classes. But your code would need to include a modules from the stamp ecosystem.

--

--