stamps - a better way to object composition
TRANSCRIPT
StampsRethinking best practices
medium.com/@koresar
ctor()method X()property A
ctor()method X()property A
property B
ctor()
property Aproperty Bmethod Y()method Z()
method X()
ctor()method X()property Aproperty B
method Y()method Z()
…ctor()method X()property Aproperty Bmethod Y()method Z()property Cctor(override)ponyZuckerbergyolothe godthe true godthe new true goduseless shituseful thing()leprosariummatrixnew useless shithell on Earthpainsuffersuffersuffer
Typical class inheritance story
But actually I needed only these:
ctor()property Bmethod X()useful thing()
Typical class inheritance story
• 1 constructor• 211 methods• 130 properties• 55 events
(which essentially are properties too)
• 1 static field• 1 attribute
(aka annotation in Java terminology)
Rethinking inheritance{ ctor() method X() property B useful thing()}
…
ctor()method X()
property B
useful thing
pain
suffer
hell on Earth
useless shit
property A
method Y()method Z()
Zuckerberg
property C
Java/C# developers are like
To make that happen we invented stamps
Stamps
…
ctor()method X()
property B
useful thing
pain
suffer
hell on Earth
useless shit
property A
method Y()method Z()
Zuckerberg
property C
• Stamps are composable behaviours
• Stamps are not a library or a module
• Stamps are a specification (like Promises)
• Various stamp implementations are compatible with each other
• Stamps are factory functions (like classes)
import compose from 'stamp-implementation';// not a real module
Stamps
The only thing in the specification
const Ctor = compose({ initializers: [function (...args) { console.log('Hello Node Ninjas') }]});
Ctor(...args); // “Hello Node Ninjas”
Stamps
Constructor aka initializer
const MethodX = compose({ methods: { X() { // ... } }});
MethodX().X();
StampsA method
const PropertyB = compose({ properties: { B: 42 }});
PropertyB().B; // 42
Stamps
A property
const UsefulThing = compose({ staticProperties: { usefulThing() { // ... } });
UsefulThing.usefulThing();
Stamps
A static property (method)
const Stamp1 = compose( Ctor, MethodX, PropertyB, UsefulThing);
Stamps
Composing stamps
const myObject = Stamp1();myObject.X();myObject.B; // 42Stamp1.usefulThing();
Stamps
Using the Stamp1
ctor()
method X()
property B
useful thing
const Stamp1 = compose( Ctor, MethodX, PropertyB, UsefulThing);
import compose from 'stamp-implementation';
const Ctor = compose({ initializers: [ function () { /* … */ } ]});const MethodX = compose({ methods: { X() { /* … */ } }});const PropertyB = compose({ properties: { B: 42 }});const UsefulThing = compose({ staticProperties: { usefulThing() { /* … */ } }});
const Stamp1 = compose(Ctor, MethodX, PropertyB, UsefulThing);const myObject = Stamp1();myObject.X(); myObject.B; // 42Stamp1.usefulThing();
Stamps
ctor()
method X()
property B
useful thing
const Stamp1 = compose({ initializers: [() => { // ... }], methods: { X() { // ... } }, properties: { B: 42 }, staticProperties: { usefulThing() { // ... } }});
const myObject = Stamp1();myObject.X(); myObject.B; // 42Stamp1.usefulThing();
Same but as a single stamp
stamp.compose() methodconst Stamp1 = compose(Ctor, MethodX, PropertyB, UsefulThing);
Every compose call:• creates a new stamp• merges the metadata of the provided stamps
etc
const Stamp1 = Ctor.compose(MethodX, PropertyB, UsefulShit);
const Stamp1 = Ctor.compose(MethodX).compose(PropertyB).compose(UsefulThing);
const Stamp1 = Ctor.compose(MethodX.compose(PropertyB.compose(UsefulThing)));
const Stamp1 = compose(Ctor, MethodX).compose(PropertyB, UsefulThing);
Similar to Promises .then() Stamps have .compose()
Collected Stamp1 metadata
const Stamp1 = compose(Ctor, MethodX, PropertyB, UsefulThing);console.log(Stamp1.compose);{ [Function] initializers: [ [Function] ], methods: { X: [Function: X] }, properties: { B: 42 }, staticProperties: { usefulThing: [Function: usefulThing] } }
Stamp1.compose has:property “initializers”property “methods”property “properties”property “staticProperties”
ctor()method X()property Buseful thing
Now let’s take a classic Java example and convert it to stamps.
The purpose of the example is not to solve a problembut to show an idea behind the stamps.
@TesterInfo(priority = Priority.HIGH,createdBy = "Zavulon",tags = {"sales","test"}
)public class TestExample extends BaseTest {
TestExample(TestRunner r) { this.runner = r;
}
@Testvoid testA() {
// ...}
@Test(enabled = false)void testB() {
// …}
}
Example: Java metadata and class configurationConfiguring
class metadata in Java
(annotations)
Configuring object instance
in Java(dependency injection)
Configuring class
members in Java
(interface implementation)
• class extension/inheritance• Java annotations• interface implementations• dependency injection pattern• has-a composition pattern• is-a composition pattern• proxy design pattern• wrapper design pattern• decorator design pattern• …
How to setup a class behavior in Java?
This is nothing else, but a process of configuring your class,
a process of collecting metadata
How to setup a stamp behavior?• compose• compose• compose• compose• compose• compose• compose• compose• compose• compose
stamp
const BaseTest = compose({ staticProperties: {
suite(info) { return this.compose({ deepConfiguration: info }); },
test(options, func) { return this.compose({ methods: { [func.name]: func }, configuration: { [func.name]: options } }); }
}});
TesterStamp.suite()TesterStamp.test()
Example: a stamp with two static methods
Example: compare Java and stamp@TesterInfo(
priority = Priority.HIGH,createdBy = "Zavulon",tags = {"sales","test" }
)public class TestExample extends BaseTest { TestExample(TestRunner r) { this.runner = r;
}
@Testvoid testA() {
// ...}
@Test(enabled = false)void testB() {
// …}
}
const TestExample = BaseTest
.suite({ priority: Priority.HIGH, createdBy: 'Zavulon', tags: ['sales', 'test']}).compose({properties: {runner}})
.test(null, function testA() { // ... }).test({enabled: false}, function testB() { // ... });
const BaseTest = compose({ staticProperties: {
suite(info) { return this.compose({ deepConfiguration: info }); },
test(options, func) { return this.compose({ methods: { [func.name]: func }, configuration: { [func.name]: options } }); }
}});
TesterStamp.suite().compose().test().test();
Example: a stamp with two static methods
Let’s see what the resulting stamp metadata looks like{ deepConfiguration: { priority: 'HIGH', createdBy: 'Zavulon', tags: ['sales', 'test'] }, properties: { runner: ... }, configuration: { testA: {}, testB: {enabled: false}, testC: {enabled: true} }, methods: { testA() {}, testB() {}, testC() {} }}
TestExample.compose
Stamp’s metadata in specification
* methods - instance methods (prototype)* properties - instance properties* deepProperties - deeply merged instance properties* propertyDescriptors - JavaScript standard property descriptors* staticProperties - stamp properties* staticDeepProperties - deeply merged stamp properties* staticPropertyDescriptors - JavaScript standard property descriptors* initializers - list of initializers* configuration - arbitrary data* deepConfiguration - deeply merged arbitrary data
The “magical” metadata merging algorithm
const dstMetadata = {};mergeMetadata(dstMetadata, srcMetadata);
/** * Combine two stamp metadata objects. Mutates `dst` object. */function mergeMetadata(dst, src) { _.assign(dst.methods, src.methods); _.assign(dst.properties, src.properties); _.assign(dst.propertyDescriptors, src.propertyDescriptors); _.assign(dst.staticProperties, src.staticProperties); _.assign(dst.staticPropertyDescriptors, src.staticPropertyDescriptors); _.assign(dst.configuration, src.configuration);
_.merge(dst.deepProperties, src.deepProperties); _.merge(dst.staticDeepProperties, src.staticDeepProperties); _.merge(dst.deepConfiguration, src.deepConfiguration);
dst.initializers = dst.initializers.concat(src.initializers);}
Awesome features you didn’t notice
The .compose() method is detachableimport {ThirdPartyStamp} from 'third-party-stuff';
I wish the .then() method of Promises was as easy detachable as the .compose() method.
Like that:const Promise = thirdPartyPromise.then;
Detaching the .compose() method
And reusing it as a compose() function to create new stampsconst compose = ThirdPartyStamp.compose;
const Stamp1 = compose({ properties: { message: "Look Ma! I'm creating stamps without importing an implementation!" }});
You can override the .compose()import compose from 'stamp-specification';
function infectedCompose(...args) { console.log('composing the following: ', args); args.push({staticProperties: {compose: infectedCompose}}); return compose.apply(this, args);}
const Ctor = infectedCompose({ initializers: [function () { /* ... */ }]});const MethodX = infectedCompose({ methods: { X() { /* ... */ } }});const PropertyB = infectedCompose({ properties: { B: 42 }});const UsefulThing = infectedCompose({ staticProperties: { usefulThing() { /* ... */ } }});
const Stamp1 = infectedCompose(Ctor, MethodX) .compose(PropertyB.compose(UsefulThing));
console.log getsexecuted 7 times
{
You can create APIs like that
const MyUser = compose({ initializers: [function ({password}) { this.password = password; console.log(this.password.length); }]});
// Cannot read property 'password' of undefinedMyUser();
// Cannot read property 'length' of nullMyUser({password: null});
import MyUser from './my-user';import ArgumentChecker from './argument-checker';
const MySafeUser = ArgumentChecker.checkArguments({ password: 'string'}).compose(MyUser);
// throws "Argument 'password' must be a string" MySafeUser();
// throws "Argument 'password' must be a string" MySafeUser({password: null});
You can create APIs like that
You can create APIs like that 1 import compose from 'stamp-specification'; 2 3 const MyUser = compose({ 4 initializers: [function ({password}) { 5 this.password = password; 6 console.log(this.password.length); 7 }] 8 }); 9 10 // Cannot read property 'password' of undefined 11 MyUser(); 12 13 // Cannot read property 'length' of null 14 MyUser({password: null}); 15 16 17 const MySafeUser = ArgumentChecker.checkArguments({ 18 password: 'string' 19 }) 20 .compose(MyUser); 21 22 // Argument 'password' must be a string 23 MySafeUser();
const ArgumentChecker = compose({ staticProperties: { checkArguments(keyValueMap) { // deep merge all the pairs
// to the ArgumentChecker object
return this.compose({deepConfiguration: { ArgumentChecker: keyValueMap }}); } },...
ArgumentChecker stamp
...
initializers: [(options = {}, {stamp}) => { // take the map of key-value pairs
// and iterate over it
const map = stamp.compose.deepConfiguration.ArgumentChecker; for (const [argName, type] of map) { if (typeof options[argName] !== type) throw new Error(`Argument "${argName}" must be a ${type}`); } }]});
const ArgumentChecker = compose({ staticProperties: { checkArguments(keyValueMap) {// deep merge all the pairs to the ArgumentChecker object return this.compose({deepConfiguration: { ArgumentChecker: keyValueMap }}); } }, initializers: [function (options = {}, {stamp}) {// take the map of key-value pairs and iterate over it const map = stamp.compose.deepConfiguration.ArgumentChecker; for (const [argName, type] of map) { if (typeof options[argName] !== type) throw new Error( `Argument "${argName}" must be a ${type}`); } }]});
ArgumentChecker stamp
ArgumentChecker stamp using stampit module
const ArgumentChecker = stampit() // <- creating a new empty stamp
.statics({ checkArguments(keyValueMap) {
return this.deepConf({ArgumentChecker: keyValueMap}); } })
.init(function (options = {}, {stamp}) { const map = stamp.compose.deepConfiguration.ArgumentChecker; for (const [argName, type] of map) { if (typeof options[argName] !== type) throw new Error(`"${argName}" is missing`); } });
• Instead of classes obviously• When you have many similar but
different models:• games
(craft wooden old unique improved dwarf sword)
• subscription types (Free - Pro - Enterprise, yearly - monthly, direct debit - invoice, credit card - bank account, etc.)
• … your case• As a dependency injection for complex
business logic• http request handlers• interdependent (micro)service logic• .. your case
• UI Components(React, Ember, Vue, Angular, Meteor?)
When to use Stamps
• When you need to squeeze every CPU cycle from your app• games (LOL!)• drivers
• In small utility modules (like left-pad)
When NOT to use Stamps
medium.com/@koresar
So, if you are building a new language, please, omit classes.
Consider stamps instead(or a similar composable behaviours)
Specs: https://github.com/stampit-org/stamp-specificationDevelopment: https://github.com/stampit-org
stampit_jsNews:
kore_sar(C) Vasyl Boroviak
Chat: https://gitter.im/stampit-org/stampit