Slide 1

Slide 1 text

No content

Slide 2

Slide 2 text

A Unified Theory of JavaScript Style, Part II The Art of the JavaScript Metaobject Protocol

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

we'll talk about Proxies, Encapsulation, and Composition

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

but think about Flexibility and Deoupling at Scale

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

basics Open Mixins

Slide 9

Slide 9 text

var sam = { firstName: 'Sam', lastName: 'Lowry', fullName: function () { return this.firstName + " " + this.lastName; }, rename: function (first, last) { this.firstName = first; this.lastName = last; return this; } }

Slide 10

Slide 10 text

var sam = { firstName: 'Sam', lastName: 'Lowry' }; var Person = { fullName: function () { return this.firstName + " " + this.lastName; }, rename: function (first, last) { this.firstName = first; this.lastName = last; return this; } };

Slide 11

Slide 11 text

function extend () { var consumer = arguments[0], providers = [].slice.call(arguments, 1), key, i, provider; for (i = 0; i < providers.length; ++i) { provider = providers[i]; for (key in provider) { if (provider.hasOwnProperty(key)) { consumer[key] = provider[key]; }; }; }; return consumer; }

Slide 12

Slide 12 text

Mixins are many to _ extend(sam, Person); var peck = { firstName: 'Sam', lastName: 'Peckinpah' }; extend(peck, Person);

Slide 13

Slide 13 text

Mixins are _ to many var HasCareer = { career: function () { return this.chosenCareer; }, setCareer: function (career) { this.chosenCareer = career; return this; } }; extend(peck, HasCareer); peck.setCareer('Director');

Slide 14

Slide 14 text

No content

Slide 15

Slide 15 text

basics Private Mixins

Slide 16

Slide 16 text

function extendPrivately (receiver, mixin) { var methodName, privateProperty = Object.create(null); for (methodName in mixin) { if (mixin.hasOwnProperty(methodName)) { receiver[methodName] = mixin[methodName].bind(privateProperty); }; }; return receiver; };

Slide 17

Slide 17 text

var peck = { firstName: 'Sam', lastName: 'Peckinpah' }; extendPrivately(peck, HasCareer); peck.setCareer("Director; peck.chosenCareer //=> undefined

Slide 18

Slide 18 text

No content

Slide 19

Slide 19 text

basics Forwarding

Slide 20

Slide 20 text

function forward (receiver, metaobject, methods) { if (methods == null) { methods = Object.keys(metaobject).filter(function (methodName) { return typeof(metaobject[methodName]) == 'function'; }); } methods.forEach(function (methodName) { receiver[methodName] = function () { var result = metaobject[methodName].apply(metaobject, arguments); return result === metaobject ? this : result; }; }); return receiver; };

Slide 21

Slide 21 text

No content

Slide 22

Slide 22 text

forwarding is important Let's stick a tack in this for later

Slide 23

Slide 23 text

Four shades of gray 1.A mixin uses the receiver's method body, and executes in the receiver's context. 2.A private mixin uses the receiver's method body, but executes in another object's context. 3.Forwarding uses another object's method body, and executes in another object's context. 4.What uses another object's method body, but executes in the receiver's context?

Slide 24

Slide 24 text

function delegate (receiver, metaobject, methods) { if (methods == null) { methods = Object.keys(metaobject).filter(function (methodName) { return typeof(metaobject[methodName]) == 'function'; }); } methods.forEach(function (methodName) { receiver[methodName] = function () { return metaobject[methodName].apply(receiver, arguments); }; }); return receiver; };

Slide 25

Slide 25 text

No content

Slide 26

Slide 26 text

Could there be another way to delegate to a metaobject?

Slide 27

Slide 27 text

Yes. Object.create(metaobject);

Slide 28

Slide 28 text

No content

Slide 29

Slide 29 text

inheritence is Delegation through Prototypes

Slide 30

Slide 30 text

No content

Slide 31

Slide 31 text

Now that we've categorized our basic tools...

Slide 32

Slide 32 text

No content

Slide 33

Slide 33 text

we need to think At Scale

Slide 34

Slide 34 text

No content

Slide 35

Slide 35 text

Problems at Scale

Slide 36

Slide 36 text

Problems at Scale - object coupling

Slide 37

Slide 37 text

Problems at Scale - object coupling - inflexibility

Slide 38

Slide 38 text

Problems at Scale - object coupling - inflexibility - metaobject coupling

Slide 39

Slide 39 text

No content

Slide 40

Slide 40 text

object coupling

Slide 41

Slide 41 text

“OOP to me means only messaging, local retention & protection & hiding of state-process*” Dr. Alan Kay

Slide 42

Slide 42 text

No content

Slide 43

Slide 43 text

we have a pattern for private state: Forwarding

Slide 44

Slide 44 text

function proxy (baseObject, optionalPrototype) { var proxyObject = Object.create(optionalPrototype || null), methodName; for (methodName in baseObject) { if (typeof(baseObject[methodName]) === 'function') { (function (methodName) { proxyObject[methodName] = function () { var result = baseObject[methodName].apply( baseObject, arguments ); return (result === baseObject) ? proxyObject : result; } })(methodName); } } return proxyObject; }

Slide 45

Slide 45 text

var stack = { array: [], index: -1, push: function (value) { return this.array[this.index += 1] = value; }, pop: function () { var value = this.array[this.index]; this.array[this.index] = void 0; if (this.index >= 0) { this.index -= 1; } return value; }, isEmpty: function () { return this.index < 0; } };

Slide 46

Slide 46 text

var stackProxy = proxy(stack); stackProxy.push('first'); stackProxy //=> { push: [Function], pop: [Function], isEmpty: [Function] } stackProxy.pop(); //=> 'first'

Slide 47

Slide 47 text

No content

Slide 48

Slide 48 text

Inflexibility

Slide 49

Slide 49 text

prototype inheritence is One to Many

Slide 50

Slide 50 text

mixins are Many to Many

Slide 51

Slide 51 text

person var Person = { fullName: function () { return this.firstName + " " + this.lastName; }, rename: function (first, last) { this.firstName = first; this.lastName = last; return this; } };

Slide 52

Slide 52 text

has-career var HasCareer = { career: function () { return this.chosenCareer; }, setCareer: function (career) { this.chosenCareer = career; return this; } };

Slide 53

Slide 53 text

modern careerist var Careerist = extend( Object.create(null), Person, HasCareer ); var sam = Object.create(Careerist);

Slide 54

Slide 54 text

traditional careerist function Careerist () {} Careerist.prototype = extend( Object.create(null), Person, HasCareer ); var sam = new Careerist();

Slide 55

Slide 55 text

No content

Slide 56

Slide 56 text

Metaobject to Metaobject Coupling

Slide 57

Slide 57 text

No content

Slide 58

Slide 58 text

Encapsulation for Metaobjects

Slide 59

Slide 59 text

No content

Slide 60

Slide 60 text

Open recursion considered harmful

Slide 61

Slide 61 text

The fragile base class problem is a fundamental architectural problem of object-oriented programming systems where base classes (superclasses) are considered "fragile" because seemingly safe modifications to a base class, when inherited by the derived classes, may cause the derived classes to malfunction.

Slide 62

Slide 62 text

Encapsulation for Metaobjects

Slide 63

Slide 63 text

No content

Slide 64

Slide 64 text

encapsulating an Object

Slide 65

Slide 65 text

No content

Slide 66

Slide 66 text

encapsulating this

Slide 67

Slide 67 text

var number = 0; function encapsulate (behaviour) { var safekeepingName = "__" + ++number + "__", encapsulatedObject = {}; function createContext (methodReceiver) { return proxy(methodReceiver); } function getContext (methodReceiver) { var context = methodReceiver[safekeepingName]; if (context == null) { context = createContext(methodReceiver); Object.defineProperty(methodReceiver, safekeepingName, { enumerable: false, writable: false, value: context }); } return context; } Object.keys(behaviour).forEach(function (methodName) { var methodBody = behaviour[methodName]; encapsulatedObject[methodName] = function () { var context = getContext(this), result = description[methodName].apply(context, arguments); return (result === context) ? this : result; }; }); return encapsulatedObject; }

Slide 68

Slide 68 text

encapsulation lacks A Sense of Self function createContext (methodReceiver) { return Object.defineProperty( proxy(methodReceiver), 'self', { writable: false, enumerable: false, value: methodReceiver } ); }

Slide 69

Slide 69 text

Private Methods var MultiTalented = encapsulate({ _englishList: function (list) { var butLast = list.slice(0, list.length - 1), last = list[list.length - 1]; return butLast.length > 0 ? [butLast.join(', '), last].join(' and ') : last; }, initialize: function () { this._careers = []; return this; }, addCareer: function (career) { this._careers.push(career); return this; }, careers: function () { return this._englishList(this._careers); } });

Slide 70

Slide 70 text

private methods Are a Key Encapsulation Idea

Slide 71

Slide 71 text

if only we knew which methods were private... function createContext (methodReceiver) { var innerProxy = proxy(methodReceiver); privateMethods.forEach(function (methodName) { innerProxy[methodName] = behaviour[methodName]; }); return Object.defineProperty( innerProxy, 'self', { writable: false, enumerable: false, value: methodReceiver } ); }

Slide 72

Slide 72 text

function encapsulate (behaviour) { var privateMethods = methods.filter(function (methodName) { return methodName[0] === '_'; }), publicMethods = methods.filter(function (methodName) { return methodName[0] !== '_'; }); // ... return publicMethods.reduce(function (acc, methodName) { var methodBody = behaviour[methodName]; acc[methodName] = function () { var context = getContext(this), result = behaviour[methodName].apply(context, arguments); return (result === context) ? this : result; }; return acc; }, {}); }

Slide 73

Slide 73 text

No content

Slide 74

Slide 74 text

encapsulation's beard is Half Constructed

Slide 75

Slide 75 text

has-name is independent var HasName = encapsulate({ name: function () { return this.name; }, setName: function (name) { this.name = name; return this; } });

Slide 76

Slide 76 text

has-career is independent var HasCareer = encapsulate({ career: function () { return this.name; }, setCareer: function (name) { this.name = name; return this; } });

Slide 77

Slide 77 text

is-delf describing has Dependencies var IsSelfDescribing = encapsulate({ description: function () { return this.name() + ' is a ' + this.career(); } });

Slide 78

Slide 78 text

we can Name our Dependencies var IsSelfDescribing = encapsulate({ name: undefined, career: undefined, description: function () { return this.name() + ' is a ' + this.career(); } });

Slide 79

Slide 79 text

methods-of-type function methodsOfType (behaviour, type) { var methods = [], methodName; for (methodName in behaviour) { if (typeof(behaviour[methodName]) === type) { methods.push(methodName); } }; return methods; }

Slide 80

Slide 80 text

Identifying Dependencies function encapsulate (behaviour) { var safekeepingName = "__" + ++number + "__", methods = Object.keys(behaviour).filter(function (methodName) { return typeof behaviour[methodName] === 'function'; }), privateMethods = methods.filter(function (methodName) { return methodName[0] === '_'; }), publicMethods = methods.filter(function (methodName) { return methodName[0] !== '_'; }), dependencies = Object.keys(behaviour).filter(function (methodName) { return typeof behaviour[methodName] === 'undefined'; });

Slide 81

Slide 81 text

Partial Proxies function partialProxy (baseObject, methods, proxyPrototype) { var proxyObject = Object.create(proxyPrototype || null); methods.forEach(function (methodName) { proxyObject[methodName] = function () { var result = baseObject[methodName].apply(baseObject, arguments); return (result === baseObject) ? proxyObject : result; } }); return proxyObject; }

Slide 82

Slide 82 text

Using Partial Proxies function createContext (methodReceiver) { var innerProxy = partialProxy( methodReceiver, publicMethods.concat(dependencies) ); privateMethods.forEach(function (methodName) { innerProxy[methodName] = behaviour[methodName]; }); return Object.defineProperty( innerProxy, 'self', { writable: false, enumerable: false, value: methodReceiver } ); }

Slide 83

Slide 83 text

encapsulation's Key Features 1.The Receiver is encapsulated in an unenumerated property. 2.Separation of context and self. 3.Private methods. 4.Named Dependencies

Slide 84

Slide 84 text

No content

Slide 85

Slide 85 text

Composing Metaobjects

Slide 86

Slide 86 text

var SingsSongs = encapsulate({ _songs: null, initialize: function () { this._songs = []; return this; }, addSong: function (name) { this._songs.push(name); return this; }, songs: function () { return this._songs; } });

Slide 87

Slide 87 text

var HasAwards = encapsulate({ _awards: null, initialize: function () { this._awards = []; return this; }, addAward: function (name) { this._awards.push(name); return this; }, awards: function () { return this._awards; } });

Slide 88

Slide 88 text

var AwardWinningSongwriter = extend( Object.create(null), SingsSongs, HasAwards ), tracy = Object.create(AwardWinningSongwriter); tracy.initialize(); tracy.songs() //=> undefined

Slide 89

Slide 89 text

No content

Slide 90

Slide 90 text

Liskov Substitution Principle Substitutability is a principle in object-oriented programming. It states that, in a computer program, if S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e., objects of type S may substitute objects of type T) without altering any of the desirable properties of that program

Slide 91

Slide 91 text

function isUndefined (value) { return typeof value === 'undefined'; } function isntUndefined (value) { return typeof value !== 'undefined'; } function isFunction (value) { return typeof value === 'function'; }

Slide 92

Slide 92 text

function orderStrategy2 () { if (arguments.length === 1) { return arguments[0]; } else { var fns = __slice.call(arguments, 0); return function composed () { var args = arguments, context = this, values = fns.map(function (fn) { return fn.apply(context, args); }).filter(isntUndefined); if (values.length > 0) { return values[values.length - 1]; } } } }

Slide 93

Slide 93 text

function propertiesToArrays (metaobjects) { return metaobjects.reduce(function (collected, metaobject) { var key; for (key in metaobject) { if (key in collected) { collected[key].push(metaobject[key]); } else collected[key] = [metaobject[key]] } return collected; }, {}) }

Slide 94

Slide 94 text

function resolveUndefineds (collected) { return Object.keys(collected).reduce(function (resolved, key) { var values = collected[key]; if (values.every(isUndefined)) { resolved[key] = undefined; } else resolved[key] = values.filter(isntUndefined); return resolved; }, {}); }

Slide 95

Slide 95 text

function applyProtocol(seed, resolveds, protocol) { return Object.keys(resolveds).reduce( function (applied, key) { var value = resolveds[key]; if (isUndefined(value)) { applied[key] = value; } else if (value.every(isFunction)) { applied[key] = protocol.apply(null, value); } else throw "Don't know what to do with " + value; return applied; }, seed); }

Slide 96

Slide 96 text

function canBeMergedInto (object1, object2) { var prototype1 = Object.getPrototypeOf(object1), prototype2 = Object.getPrototypeOf(object2); if (prototype1 === null) return prototype2 === null; if (prototype2 === null) return true; if (prototype1 === prototype2) return true; return Object.prototype.isPrototypeOf.call(prototype2, prototype1); }

Slide 97

Slide 97 text

var callLeft2 = (function () { if (typeof callLeft == 'function') { return callLeft; } else if (typeof allong === 'object' && typeof allong.es === 'object' && typeof allong.es.callLeft === 'function') { return allong.es.callLeft; } else { return function callLeft2 (fn, arg2) { return function callLeft2ed (arg1) { return fn.call(this, arg1, arg2); }; }; } })();

Slide 98

Slide 98 text

function seedFor (objectList) { var seed = objectList[0] == null ? Object.create(null) : Object.create( Object.getPrototypeOf(objectList[0]) ), isCompatibleWithSeed = callLeft2(canBeMergedInto, seed); if (!objectList.every(isCompatibleWithSeed)) { throw 'incompatible prototypes'; } return seed; }

Slide 99

Slide 99 text

function composeMetaobjects () { var metaobjects = __slice.call(arguments, 0), arrays = propertiesToArrays(metaobjects), resolved = resolveUndefineds(arrays), seed = seedFor(metaobjects), composed = applyProtocol(seed, resolved, orderStrategy2); return composed; }

Slide 100

Slide 100 text

No content

Slide 101

Slide 101 text

compose-metaobjects In Action

Slide 102

Slide 102 text

var Songwriter = encapsulate({ initialize: function () { this._songs = []; return this.self; }, addSong: function (name) { this._songs.push(name); return this.self; }, songs: function () { return this._songs; } });

Slide 103

Slide 103 text

var Subscribable = encapsulate({ initialize: function () { this._subscribers = []; return this.self; }, subscribe: function (callback) { this._subscribers.push(callback); }, unsubscribe: function (callback) { this._subscribers = this._subscribers.filter( function (subscriber) { return subscriber !== callback; }); }, subscribers: function () { return this._subscribers; }, notify: function () { receiver = this; this._subscribers.forEach( function (subscriber) { subscriber.apply(receiver.self, arguments); }); } });

Slide 104

Slide 104 text

var SubscribableSongwriter = composeMetaobjects( Object.create(Songwriter), Subscribable, encapsulate({ notify: undefined, addSong: function () { this.notify(); } }) );

Slide 105

Slide 105 text

var SongwriterView = { initialize: function (model, name) { this.model = model; this.name = name; this.model.subscribe(this.render.bind(this)); return this; }, _englishList: function (list) { var butLast = list.slice(0, list.length - 1), last = list[list.length - 1]; return butLast.length > 0 ? [butLast.join(', '), last].join(' and ') : last; }, render: function () { var songList = this.model.songs().length > 0 ? [" has written " + this._englishList(this.model.songs().map(function (song) { return "'" + song + "'"; }))] : []; console.log(this.name + songList); return this; } };

Slide 106

Slide 106 text

var paulSimon = Object.create(SubscribableSongwriter).initialize(), paulView = Object.create(SongwriterView).initialize(paulSimon, 'Paul Simon'); paulSimon.addSong('Cecilia') //=> Paul Simon has written 'Cecilia' {} paulSimon.songs()

Slide 107

Slide 107 text

No content

Slide 108

Slide 108 text

lessons

Slide 109

Slide 109 text

No content

Slide 110

Slide 110 text

lesson one Fexibility follows from combining small metaobjects with focused responsibilities

Slide 111

Slide 111 text

No content

Slide 112

Slide 112 text

lesson two Encapsulation reduces convenience, but increases flexibility

Slide 113

Slide 113 text

No content

Slide 114

Slide 114 text

lesson three Careful composition creates cohesion without coupling

Slide 115

Slide 115 text

No content

Slide 116

Slide 116 text

the biggest lesson? [S]ingle Responsibility [O]pen/Closed Principle [L]iskov Substitutability [I]nterface Segregation [D]ependency Inversion

Slide 117

Slide 117 text

Reginald Braithwaite GitHub, Inc. raganwald.com @raganwald NDC Conference, Oslo, Norway, June 5, 2014