stamps - a better way to object composition

43
Stamps Rethinking best practices

Upload: vasyl-boroviak

Post on 19-Feb-2017

103 views

Category:

Software


1 download

TRANSCRIPT

Page 1: Stamps - a better way to object composition

StampsRethinking best practices

Page 2: Stamps - a better way to object composition

medium.com/@koresar

Page 3: Stamps - a better way to object composition

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()

Page 4: Stamps - a better way to object composition

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)

Page 5: Stamps - a better way to object composition

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

Page 6: Stamps - a better way to object composition

To make that happen we invented stamps

Page 7: Stamps - a better way to object composition

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)

Page 8: Stamps - a better way to object composition

import compose from 'stamp-implementation';// not a real module

Stamps

The only thing in the specification

Page 9: Stamps - a better way to object composition

const Ctor = compose({ initializers: [function (...args) { console.log('Hello Node Ninjas') }]});

Ctor(...args); // “Hello Node Ninjas”

Stamps

Constructor aka initializer

Page 10: Stamps - a better way to object composition

const MethodX = compose({ methods: { X() { // ... } }});

MethodX().X();

StampsA method

Page 11: Stamps - a better way to object composition

const PropertyB = compose({ properties: { B: 42 }});

PropertyB().B; // 42

Stamps

A property

Page 12: Stamps - a better way to object composition

const UsefulThing = compose({ staticProperties: { usefulThing() { // ... } });

UsefulThing.usefulThing();

Stamps

A static property (method)

Page 13: Stamps - a better way to object composition

const Stamp1 = compose( Ctor, MethodX, PropertyB, UsefulThing);

Stamps

Composing stamps

Page 14: Stamps - a better way to object composition

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);

Page 15: Stamps - a better way to object composition

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

Page 16: Stamps - a better way to object composition

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

Page 17: Stamps - a better way to object composition

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()

Page 18: Stamps - a better way to object composition

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

Page 19: Stamps - a better way to object composition

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.

Page 20: Stamps - a better way to object composition

@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)

Page 21: Stamps - a better way to object composition

• 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

Page 22: Stamps - a better way to object composition
Page 23: Stamps - a better way to object composition

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

Page 24: Stamps - a better way to object composition

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() { // ... });

Page 25: Stamps - a better way to object composition

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

Page 26: Stamps - a better way to object composition

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

Page 27: Stamps - a better way to object composition

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

Page 28: Stamps - a better way to object composition

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);}

Page 29: Stamps - a better way to object composition

Awesome features you didn’t notice

Page 30: Stamps - a better way to object composition

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!" }});

Page 31: Stamps - a better way to object composition

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

{

Page 32: Stamps - a better way to object composition

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});

Page 33: Stamps - a better way to object composition

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

Page 34: Stamps - a better way to object composition

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();

Page 35: Stamps - a better way to object composition

const ArgumentChecker = compose({ staticProperties: { checkArguments(keyValueMap) { // deep merge all the pairs

// to the ArgumentChecker object

return this.compose({deepConfiguration: { ArgumentChecker: keyValueMap }}); } },...

ArgumentChecker stamp

Page 36: Stamps - a better way to object composition

...

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}`); } }]});

Page 37: Stamps - a better way to object composition

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

Page 38: Stamps - a better way to object composition

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`); } });

Page 39: Stamps - a better way to object composition

• 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

Page 40: Stamps - a better way to object composition

• 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

Page 41: Stamps - a better way to object composition

medium.com/@koresar

Page 42: Stamps - a better way to object composition

So, if you are building a new language, please, omit classes.

Consider stamps instead(or a similar composable behaviours)

Page 43: Stamps - a better way to object composition

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