JavaScript Spessore A thick shot of objects, metaobjects, & protocols raganwald This book is for sale at http://leanpub.com/javascript-spessore This version was published on 2014-06-24
This is a Leanpub book. Leanpub empowers authors and publishers with the Lean Publishing process. Lean Publishing is the act of publishing an in-progress ebook using lightweight tools and many iterations to get reader feedback, pivot until you have the right book and build traction once you do. Š2013 - 2014 Reginald Braithwaite
Also By raganwald Kestrels, Quirky Birds, and Hopeless Egocentricity What I’ve Learned From Failure How to Do What You Love & Earn What You’re Worth as a Programmer CoffeeScript Ristretto JavaScript Allongé
Contents Prefaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Taking a page out of LiSP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . JavaScript Allongé and allong.es . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
i ii iii
The Big Idea . . . . . . . . . . . . . . . . . . . Is JavaScript Functional or Object-Oriented? JavaScript Algebras . . . . . . . . . . . . . . Composeabilitity . . . . . . . . . . . . . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
1 3 9 14
JavaScript’s Objects . . . . . . . . . . . . . . . . . . Data Structures . . . . . . . . . . . . . . . . . . . Plain Old JavaScript Objects . . . . . . . . . . . . Encapsulating State with Closures . . . . . . . . . Composition and Extension . . . . . . . . . . . . This and That . . . . . . . . . . . . . . . . . . . . What Context Applies When We Call a Function? Extending Objects . . . . . . . . . . . . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
15 17 20 23 29 34 38 44
Object Recipes . . . . . . . Structs and Value Objects Accessors . . . . . . . . Hiding Object Properties Proxies . . . . . . . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
47 49 56 61 68
Instances and Prototypes . . . . . . . . . . . . . . . . . . . . . . . . . . . Prototypes are Simple, it’s the Explanations that are Hard To Understand Binding Functions to Contexts . . . . . . . . . . . . . . . . . . . . . . . Object Methods . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Extending Objects with Delegation . . . . . . . . . . . . . . . . . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
70 72 81 85 88
Methods . . . . . . . . . . . . . . . . What is a Method? . . . . . . . . . The Letter and the Spirit of the Law Composite Methods . . . . . . . . . Method Objects . . . . . . . . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. 94 . 96 . 98 . 103 . 110
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . .
. . . . .
. . . . .
. . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
CONTENTS
Predicate Dispatch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117 Metaobjects . . . . . . . . . . . . . . Why Metaobjects? . . . . . . . . . . Mixins, Forwarding, and Delegation Later Binding . . . . . . . . . . . . Conflict Resolution Policies . . . . . Delegation via Prototypes . . . . . . Singleton Prototypes . . . . . . . . Shared Prototypes . . . . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
122 124 125 135 141 155 161 167
Encapsulation and Composition . . . . . . . The Encapsulation Problem . . . . . . . . . Proxies . . . . . . . . . . . . . . . . . . . . Encapsulation for Metaobjects . . . . . . . encapsulate(...) . . . . . . . . . . . . . Self . . . . . . . . . . . . . . . . . . . . . . Privacy . . . . . . . . . . . . . . . . . . . . Closing Encapsulated Objects . . . . . . . Decoupling with Partial Proxies . . . . . . Composing Metaobjects . . . . . . . . . . Transforming and Decorating Metaobjects Playing Well with Classes . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
177 179 182 185 196 201 207 216 223 230 246 254
Inheritance, Ontologies, and Semantic Types Class Hierarchies . . . . . . . . . . . . . . . Getting the Semantics Right . . . . . . . . . Structural vs. Semantic Typing . . . . . . . . Interlude: “is-a” and “was-a” . . . . . . . . . The Expression Problem . . . . . . . . . . . Multiple Dispatch . . . . . . . . . . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
259 260 263 265 272 274 279
Metaobject Protocols Genesis . . . . . . . The Class Class . . Class Mixins . . . . Well, Actually… . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
287 289 299 303 308
Appendix: Source Code . . . . . . Encapsulation and Composition Methods . . . . . . . . . . . . . Utility Functions . . . . . . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
316 317 326 330
Prefaces
ยน ยนGroup (c) 2013 J MacPherson, some rights reserved
Prefaces
ii
Taking a page out of LiSP Teaching Lisp by implementing Lisp is a long-standing tradition. If you set out to learn to program with Lisp, you will find read book after book, lecture after lecture, and blog post after blog post, all explaining how to implement Lisp in Lisp. Christian Queinnec’s Lisp in Small Pieces² is particularly notable, not just implementing a Lisp in Lisp, but covering a wide range of different semantics within Lisp. Lisp in Small Pieces’s approach is to introduce a feature of Lisp, then develop an implementation. The book covers Lisp-1 vs. Lisp-2³, then discusses how to implement namespaces, building a simple Lisp-1 and a simple Lisp-2. Another chapter discusses scoping, and again you build interpreters for dynamic and block scoped Lisps. Building interpreters (and eventually compilers) may seem esoteric compared to tutorials demonstrating how to build a blogging engine, but there’s a method to this madness. If you implement block scoping in a “toy” language, you gain a deep understanding of how closures really work in any language. If you write a Lisp that rewrites function calls in Continuation Passing Style⁴, you can’t help but feel comfortable using JavaScript callbacks in Node.js⁵. Implementing a language feature teaches you a tremendous amount about how the feature works in a relatively short amount of time. And that goes double for implementing variations on the same feature–like dynamic vs block scoping or single vs multiple namespaces.
j(oop)s In this book, we are going to implement a variety of object-oriented programming language semantics, in JavaScript. We will implement different object semantics, implement different kinds of metaobjects, and implement different kinds of method protocols. We’ll see how to use JavaScript’s basic building blocks to implement things like private state, multiple inheritence, protected methods, and more. Unlike other books and tutorials, we won’t focus on how to write object-oriented programs. We won’t worry about patterns like “Facade,” or walk through an “extract method” refactoring. We’ll trust that there are more than enough existing resources covering these topics, and focus instead on the areas generally given short shrift by existing texts. Our approach will be to focus on implementing OOP’s basic tools. This will teach us (1) A great deal about how features like delegation and traits actually work, and (2) How to implement them in languages (like JavaScript) that do not provide much more than cursory support for OOP. ²http://www.amazon.com/gp/product/B00AKE1U6O/ref=as_li_ss_tl?ie=UTF8&camp=1789&creative=390957&creativeASIN= B00AKE1U6O&linkCode=as2&tag=raganwald001-20 ³A “Lisp-1” has a single namespace for both functions and other values. A “Lisp-2” has separate namespaces for functions and other values. To the extend that JavaScript resembles a Lisp, it resembles a Lisp-1. See The function namespace. ⁴https://en.wikipedia.org/wiki/Continuation-passing_style ⁵http://nodejs.org/about/
Prefaces
iii
JavaScript Allongé and allong.es JavaScript Spessore⁶ is written for the reader who has read JavaScript Allongé⁷ or has equivalent experience with JavaScript, especially as it pertains to functions, closures, and prototypes. “This is a must-read for any developer who wants to know Javascript better… Reg has a way of explaining things in a way that connected the dots for me. This is probably the only programming book I’ve re-read cover to cover a dozen times or more.”–etrinh “I think it’s one of the best tech books I’ve read since Sedgewick’s Algorithms in C.”– Andrey Sidorov “Your explanation of closures in JavaScript Allongé is the best I’ve read.”–Emehrkay “It’s a different approach to JavaScript than you’ll find in most other places and shines a light on some of the more elegant parts of JavaScript the language.”–@jeremymorrell Even if you know the material, you may want to read JavaScript Allongé to familiarize yourself its approach to functional combinators. You can read it for free online⁸.
allong.es allong.es⁹ is a JavaScript library inspired by JavaScript Allongé. It contains many utility functions that are used in JavaScript Spessore’s examples, such as map, variadic and tap. It’s free, and you can even type a lot of the examples from this book into its try allong.es¹⁰ page and see them work.
⁶https://leanpub.com/javascript-spessore ⁷https://leanpub.com/javascript-allonge ⁸https://leanpub.com/javascript-allonge/read ⁹http://allong.es ¹⁰http://allong.es/try/
The Big Idea
¹¹ ¹² Detail of a Mazzer Mini espresso grinder. The grind is at least as important as the press, yet most people spend more time and money on their espresso machine than they do on their grinder.
¹¹https://www.flickr.com/photos/102043207@N06/11232144966 ¹²Mazzer Mini Grinder, some rights reserved
The Big Idea
In This Chapter In this chapter, we propose that JavaScript’s “Big Idea” is that you can use functions to transform and compose primitives, functions, and objects. We will suggest that programming with objects is not separate and distinct from programming with functions, but simply another way to work with JavaScript’s big idea. This thinking will set the stage for the object-oriented programming techniques we’ll explore throughout the book.
.
2
The Big Idea
3
Is JavaScript Functional or Object-Oriented? One of JavaScript’s defining characteristics is its treatment of functions as first-class values. Like numbers, strings, and other kinds of objects, references to functions can be passed as arguments to functions, returned as the result from functions, bound to variables, and generally treated like any other value.¹³ Here’s an example of passing a reference to a function around. This simple array-backed stack has an undo function. It works by creating a function representing the action of undoing the last update, and then pushing that onto a stack of actions to be undone: var stack = { array: [], undoStack: [], push: function (value) { this.undoStack.push(function () { this.array.pop(); }); return this.array.push(value); }, pop: function () { var popped = this.array.pop(); this.undoStack.push(function () { this.array.push(popped); }); return popped; }, isEmpty: function () { return array.length === 0; }, undo: function () { this.undoStack.pop().call(this); } }; stack.push('hello'); stack.push('there'); stack.push('javascript'); stack.undo(); ¹³JavaScript does not actually pass functions or any other kind of object around, it passes references to functions and other objects around. But it’s awkward to describe var I = function (a) { return a; } as binding a reference to a function to the variable I, so we often take an intellectually lazy shortcut, and say the function is bound to the variable I. This is much the same shorthand as saying that somebody added me to the schedule for an upcoming conference: They really added my name to the list, which is a kind of reference to me.
4
The Big Idea
stack.undo(); stack.pop(); //=> 'hello'
Functions-as-values is a powerful idea. And people often look at the idea of functions-as-values and think, “Oh, JavaScript is a functional programming language.” No. In computer science, functional programming is a programming paradigm, a style of building the structure and elements of computer programs, that treats computation as the evaluation of mathematical functions and avoids state and mutable data. –Wikipedia¹⁴
Functional programming might have meant “functions as first-class values” in the 1960s when Lisp was young. But time marches on, and we must march alongside it. JavaScript does not avoid state, and JavaScript embraces mutable data, so JavaScript does not value “functional programming.”
¹⁵
objects JavaScript’s other characteristic is its support for objects. Although JavaScript’s features seem paltry compared to rich OO languages like Scala, its extreme minimalism means that you can actually build almost any OO paradigm up from basic pieces. Now, people often hear the word “objects” and think kingdom of nouns¹⁶. But objects are not necessarily nouns, or at least, not models for obvious, tangible entities in the real world. One example concerns state machines¹⁷. We could implement a cell in Conway’s Game of Life¹⁸ using if statements and a boolean property to determine whether the cell was alive or dead:¹⁹
¹⁴https://en.wikipedia.org/wiki/Functional_programming ¹⁵https://www.flickr.com/photos/bensisto/4193046623 ¹⁶http://steve-yegge.blogspot.ca/2006/03/execution-in-kingdom-of-nouns.html ¹⁷https://en.wikipedia.org/wiki/Finite-state_machine ¹⁸https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life ¹⁹This exercise was snarfed from The Four Rules of Simple Design
The Big Idea
var __slice = [].slice; 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; }; var Universe = { // ... numberOfNeighbours: function (location) { // ... } }; var Alive = 'alive', Dead = 'dead'; var Cell = { numberOfNeighbours: function () { return Universe.numberOfNeighbours(this.location); }, stateInNextGeneration: function () { if (this.state === Alive) { return (this.numberOfNeighbours() === 3) ? Alive : Dead; } else { return (this.numberOfNeighbours() === 2 || this.numberOfNeighbours() === 3)
5
6
The Big Idea
? Alive : Dead; } } }; var someCell = extend({ state: Alive, location: {x: -15, y: 12} }, Cell);
You could say that the “state” of the cell is represented by the primitive value 'alive' for alive, or 'dead' for dead. But that isn’t modeling the state in any way, that’s just a name. The true state of the object is implicit in the object’s behaviour, not explicit in the value of the .state property. Here’s a design where we make the state explicit instead of implicit: function delegateToOwn (receiver, propertyName, methods) { var temporaryMetaobject; if (methods == null) { temporaryMetaobject = receiver[propertyName]; methods = Object.keys(temporaryMetaobject).filter(function (methodName) { return typeof(temporaryMetaobject[methodName]) === 'function'; }); } methods.forEach(function (methodName) { receiver[methodName] = function () { var metaobject = receiver[propertyName]; return metaobject[methodName].apply(receiver, arguments); }; }); return receiver; }; var Alive = { alive: function () { return true; }, stateInNextGeneration: function () { return (this.numberOfNeighbours() === 3) ? Alive
7
The Big Idea
: Dead; } }; var Dead = { alive: function () { return false; }, stateInNextGeneration: function () { return (this.numberOfNeighbours() === 2 || this.numberOfNeighbours() === 3) ? Alive : Dead; } }; var Cell = { numberOfNeighbours: function () { return thisGame.numberOfNeighbours(this.location); } } delegateToOwn(Cell, 'state', ['alive', 'aliveInNextGeneration']); var someCell = extend({ state: Alive, location: {x: -15, y: 12} }, Cell);
In this design, delegateToOwn delegates the methods .alive and .stateInNextGeneration to whatever object is the value of a Cell’s state property. So when we write someCell.state = Alive, then the Alive object will handle someCell.alive and someCell.aliveInNextGeneration. And when we write someCell.state = Dead, then the Dead object will handle someCell.alive and someCell.aliveInNextGeneration. Now we’ve taken the implicit states of being alive or dead and transformed them into the first-class values Alive and Dead. Not a string that is used implicitly in some other code, but all of “The stuff that matters about aliveness and deadness.” This is not different than the example of passing functions around: They’re both the same thing, taking something would be implicit in another design and/or another language, and making it explicit, making it a value. And making the whole thing a value, not just a boolean or a string, the complete entity. This example is the same thing as the example of a stack that handles undo with a stack of functions:
The Big Idea
8
Behaviour is treated as a first-class value, whether it be a single function or an object with multiple methods.
The Big Idea
9
JavaScript Algebras As we saw in Is JavaScript Functional or Object-Oriented?, functions are excellent for representing single-purposed units of behaviour such as an action to undo. Objects can obviously model nouns in the domain, but they can also model states and other more complex manifestations of behaviour. Functions have another role to play in object-oriented programs, one that lies outside of domain behaviour. Functions form the basis for an algebra of values. Consider these functions, begin1 and begin. They’re handy for writing function advice²⁰, for creating sequences of functions to be evaluated for side effects, and for resolving method conflicts when composing behaviour: var __slice = [].slice; function begin1 () { var fns = __slice.call(arguments, 0); return function () { var args = arguments, values = fns.map(function (fn) { return fn.apply(this, args); }, this), concretes = values.filter(function (value) { return value !== void 0; }); if (concretes.length > 0) { return concretes[0]; } } } function begin () { var fns = __slice.call(arguments, 0); return function () { var args = arguments, values = fns.map(function (fn) { return fn.apply(this, args); }, this), concretes = values.filter(function (value) { ²⁰https://en.wikipedia.org/wiki/Advice_
The Big Idea
10
return value !== void 0; }); if (concretes.length > 0) { return concretes[concretes.length - 1]; } } }
Both begin1 and begin create a function by composing two or more other functions. Composition is the primary activity in constructing programs: Making a complex piece of behaviour out of smaller, simpler pieces of behaviour. In a sense, all programming is composition. However, when we use functions to compose functions, we deliberately restrict the way in which the functions are invoked in relation to each other. This makes it easier to understand what is happening, because the functions we use for composition– like begin1 and begin–have very simple and well-defined ways of constructing new functions from existing functions. Another valuable use for functions is to transform other functions. The Underscore²¹ library includes a few, as does allong.es²². Here’s one of the simplest: function not (fn) { return function () { return !fn.apply(this, arguments); } } not takes a function and return a function that is the logical inverse. When fn(...) returns a truthy value, not(fn)(...) returns false. When fn(...) returns a falsey value, not(fn)(...) returns true.
Functions that compose and transform other functions allow us to build functions out of smaller, interchangeable components in a structured way. This is called creating an algebra of functions: A set of operations for creating, composing, and transforming functions.
an algebra of values Functions that compose and transform functions into other functions are very powerful, but things do not stop there. It’s obvious that functions can take objects as arguments and return objects. Functions (or methods) that take an object representing a client and return an object representing and account balance are a necessary and important part of software. ²¹http://underscorejs.org ²²http://allong.es
The Big Idea
11
But just as we created an algebra of functions, we can create an algebra of objects. Meaning, we can write functions that take objects and return other objects that represent a transformation of their arguments. Here’s a function that transforms an object into a proxy for that object: 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; }
Have you ever wanted to make an object’s properties private while making its methods public? You wanted a proxy for the object: var stackWithPrivateState = proxy(stack); stack.array //=> [] stackWithPrivateState.array //=> undefined stackWithPrivateState.push('hello'); stackWithPrivateState.push('there'); stackWithPrivateState.push('javascript'); stackWithPrivateState.undo(); stackWithPrivateState.undo(); stackWithPrivateState.pop(); //=> 'hello'
The proxy function transforms an object into another object with a similar purpose. Functions can compose objects as well, here’s one of the simplest examples:
The Big Idea
var __slice = [].slice; function meld () { var melded = {}, providers = __slice.call(arguments, 0), key, i, provider, except; for (i = 0; i < providers.length; ++i) { provider = providers[i]; for (key in provider) { if (provider.hasOwnProperty(key)) { melded[key] = provider[key]; }; }; }; return melded; }; var Person = { fullName: function () { return this.firstName + " " + this.lastName; }, rename: function (first, last) { this.firstName = first; this.lastName = last; return this; } }; var HasCareer = { career: function () { return this.chosenCareer; }, setCareer: function (career) { this.chosenCareer = career; return this; }, describe: function () { return this.fullName() + " is a " + this.chosenCareer;
12
The Big Idea
13
} }; var PersonWithCareer = meld(Person, HasCareer); //=> { fullName: [Function], rename: [Function], career: [Function], setCareer: [Function], describe: [Function] }
Functions that transform objects or compose objects act at a higher level than functions that query objects or update objects. They form an algebra that allows us to build objects by transformation and composition, just as we can use functions like begin to build functions by composition. JavaScript treats functions and objects as first-class values. And the power arising from this is the ability to write functions that transform and compose first-class values, creating an algebra of values.
The Big Idea
14
Composeabilitity As a language, JavaScript is agnostic about architecture. It does not have Java’s heavyweight class hierarchy. It does not have Smalltalk’s UI framework. Thus, it is possible to build your own monolithic architecture that dictates the API of all objects that fit into it and wires them together. It’s also possible to design around lots of smaller componets that “compose.” This is the principle behind JavaScript Algebras, to create collections of functions or objects and to write operations that compose new itesm from existing items. We’ll see those ideas play out in coming sections. For example, Predicate Dispatch is one way to compose new functions from smaller functions that each handle one “case” for an algorithm. Encapsulation and Composition of Metaobjects composes metaobjects (like prototypes) together our of smaller, independent metaobjects with focused responsibilities. The key to making composeability work well is always to have parts that are as independent of each other as possible. This makes combining and recombining them easy and practical. While heavyweight OO is a valid approach, our focus in this book will be on lightweight components that compose, an approach that plays to JavaScript’s strengths and makes it easy to pick as few or as many of the techniques we discuss for a particular project.
JavaScript’s Objects
²³
²³Transparent Sanremo espresso machine, London Coffee Festival, Truman Brewery, Brick Lane, Hackney, London, UK (c) 2013 Cory Doctorow, some rights reserved
JavaScriptâ&#x20AC;&#x2122;s Objects
In this chapter, we will take a look at how very simple objects work in JavaScript.
16
JavaScript’s Objects
17
Data Structures A data structure is a particular way of storing and organizing data in a computer so that it can be used efficiently. Different kinds of data structures are suited to different kinds of applications, and some are highly specialized to specific tasks.–Wikipedia²⁴
A data structure in JavaScript is a value that refers to zero or more other values that represent data. Many people call data structures “containers,” suggesting that a data structure contains data. Technically, data structures contain references to data, but “container” is a reasonable metaphor. In JavaScript, primitives like numbers and strings are not data structures. Arrays and “Objects” are built-in data structures, and you can use them to build other data structures like b-trees. (JavaScript can be confusing: Arrays are also objects, and while a string is not an object, an instance of String is an object.) Our interest is in Javascript’s objects. Technically, what JavaScript calls an object is actually a dictionary: An associative array, map, symbol table, or dictionary is an abstract data type composed of a collection of (key, value) pairs, such that each possible key appears at most once in the collection. Operations associated with this data type allow: the addition of pairs to the collection; the removal of pairs from the collection; the modification of the values of existing pairs; and the lookup of the value associated with a particular key.–Wikipedia²⁵ Data structures in many languages are passive: You write separate functions to add data to them or to query them for data. For example, we can use dictionaries to make an old-school linked list:²⁶ var emptyValue = {}; function empty () { return emptyValue; } function cons (value, list) { return { _a: value, ²⁴https://en.wikipedia.org/wiki/Data_structure ²⁵https://en.wikipedia.org/wiki/Dictionary_ ²⁶These function names go back to Lisp 1.5. The story is that it was written for a hardware architecture that had two fast CPU registers, an address
register and a decrement register. The core data type was called a “cons cell,” and represented two pointers. One was stored in the address register, the other in the decrement register. The function car meant “contents of address register,” and the function cdr meant “contents of the decrement register.” cons meant “create a new cons cell.” The would have called map “mapcar,” but we’re sticking with the more contemporary term.
JavaScript’s Objects
18
_d: list }; } function car (list) { return list._a; } function cdr (list) { return list._d; } function map (fn, list) { if (list === empty()) { return empty(); } else return cons( fn(car(list)), map(fn, cdr(list)) ); } function pp (list) { if (list === empty()) { return ""; } else return ("" + car(list) + " " + pp(cdr(list))).trim(); } var oneTwoThree = cons(1, cons(2, cons(3, empty()))); function cubed (n) { return n * n * n; } pp(map(cubed, oneTwoThree)) //=> '1 8 27'
The “big idea” of programming with data structures is that you have a standardized data structure and you write many functions that operate on the data structure. The data structure is organized by convention: Each function understands the implementation of the data structure intimately and is careful to preserve its intended structure. Many years ago, people noticed a problem with data structures: They often had several data structures with similar needs but different implementations. Linked lists are particularly fast when
JavaScript’s Objects
19
prepending items. But if we want indexed access, they are order-n slow. We might implement a Vector or Array data type for that. We might need an OrderedDictionary data structure that provides a key-value store. Linked lists, vectors, and ordered collections all need an iterator function of some kind. map does the job for linked lists based on cons cells. map does it for vectors and ordered collections, but you couldn’t have two functions with the same name in the old days when names were all global. One branch of language design concerned itself with managing namespaces. Languages like Modula2²⁷ provided ways to bundle all the functions for a data structure like a vector into a separate compilation unit with its own namespace. You could do the same thing with an ordered collection, and there are special features for writing code that refers to one, the other, or both without conflict. Once the namespace conflicts were out of the way, the programming world had everything needed to work with data structures. We can easily emulate this style of development in JavaScript: Use arrays and dictionaries, use [] and for loops for all access, and write functions to implement any behaviour we need. ²⁷https://en.wikipedia.org/wiki/Modula-2
JavaScript’s Objects
20
Plain Old JavaScript Objects In JavaScript, objects are data structures that provide a map from names to values. Many other languages call these “dictionaries” or “associative arrays.” Some call them “Hashes,” an abbreviation for “HashMap” or “Hash Table.” In most cases, the word Dictionary²⁸ describes how it works, while terms like HashMap or Hash Table²⁹ describe dictionary implementations that are optimized for fast lookup. The most common syntax for creating JavaScript objects is called a “literal object expression:” { year: 2012, month: 6, day: 14 }
Two objects created this way have differing identities: { year: 2012, month: 6, day: 14 } === { year: 2012, month: 6, day: 14 } //=> false
Objects use [] to access the values by name, using a string: { year: 2012, month: 6, day: 14 }['day'] //=> 14
Names in literal object expressions needn’t be alphanumeric strings. For anything else, enclose the label in quotes: { 'first name': 'reginald', 'last name': 'lewis' }['first name'] //=> 'reginald'
If the name is an alphanumeric string conforming to the same rules as names of variables, there’s a simplified syntax for accessing the values: { year: 2012, month: 6, day: 14 }['day'] === { year: 2012, month: 6, day: 14 }.day //=> true
Like all containers, objects can contain any value, including functions:
²⁸https://en.wikipedia.org/wiki/Dictionary_ ²⁹https://en.wikipedia.org/wiki/Hash_table
JavaScript’s Objects
21
var Arithmetic = { abs: function abs (number) { return number < 0 ? -number : number; }, power: function power (number, exponent) { if (exponent <= 0) { return 1; } else return number * power(number, exponent-1); } }; Arithmetic.abs(-5) //=> 5
namespaces Given that JavaScript objects are dictionaries makes them useful for creating namespaces³⁰: In general, a namespace is a container for a set of identifiers (also known as symbols, names). Namespaces provide a level of indirection to specific identifiers, thus making it possible to distinguish between identifiers with the same exact name. For example, a surname could be thought of as a namespace that makes it possible to distinguish people who have the same first name. In computer programming, namespaces are typically employed for the purpose of grouping symbols and identifiers around a particular functionality.– Wikipedia³¹
Napespaces are typically stateless: They contain functions and/or other constants, but not variables intended to be updated directly or indirectly. Namespaces are also typically “closed for extension:” Functions and constants are not added to a namespace dynamically. The prime purpose of a namespace is to provide disambiguation for a set of related named constants and functions. Arithmetic is an example of a namespace. It could be handy: If we were writing a fitness application, we might want to use a variable called abs for another purpose, and we wouldn’t want to accidentally shadow or overwrite our function for determining the absolute value of a number.
pojos as data structures Namespaces are typically created statically and named. But objects need not by named, and they need not be static. Dictionaries can be used when we need a dynamic data structure that has arbitrary ³⁰https://en.wikipedia.org/wiki/Namespace ³¹https://en.wikipedia.org/wiki/Namespace
JavaScript’s Objects
22
key-value pairs added and removed, and they can also be used to implement data structures (or parts of data structures) with named components. Here’re some functions that operate on a dictionary: function isNameAvailable (usersByName, name) { return usersByName[name] === undefined; } function addUser (usersByName, user) { if (isNameAvailable(user.name)) { usersByName[name] = user; return user; } else throw "" + use.name + " already exists"; }
In contrast, here’s a data structure implemented with a dictionary, the “cons cell” we saw earlier: function cons (value, list) { return { _a: value, _d: list }; }
We’re using a dictionary as an implementation technique, but we are really writing functions that operate on a tuple with elements named _a and _d. A lot of “objects” in JavaScript are data structures with named elements. They happened to be implemented with objects that act as dictionaries, but they aren’t intended to be used as arbitrary key-value stores. K> JavaScript objects are dictionaries. Dictionaries can be used to make data structures: The distinction is in how there are intended to be used. If an object is a container for arbitrary keyvalue pairs, it is a dictionary. If an object is a container for specific, named values, it’s a custom data structure that is implemented with a dictionary.
JavaScript’s Objects
23
Encapsulating State with Closures OOP to me means only messaging, local retention and protection and hiding of stateprocess, and extreme late-binding of all things. –Alan Kay³²
Building our own data structures has a namespace problem, as we discussed. Fortunately, the namespace problem can be solved. But there’s another problem, an encapsulation³³ problem. Code that operates on a data structure is written with its implementation in mind, and becomes tightly coupled to it. Coupling is transitive, so all the code that is directly coupled to a particular linked list is also indirectly coupled to all the other code coupled to the linked list. If you make a change to the way the linked list works, it can break all of that other code. This coupling problem has been known since the 1960s at least, and languages like [Modula-2] and [Smalltalk] solved this by separating a data structure’s public interface from its private implementation. The public interface is a set of functions that are intended to be used by “consumers” of the data structure. The private implementation would be hidden from view. Since consumers could not interact with the implementation, they could only become coupled to the carefully chosen public interface, not with any implementation details.
what is hiding of state-process, and why does it matter? In computer science, information hiding is the principle of segregation of the design decisions in a computer program that are most likely to change, thus protecting other parts of the program from extensive modification if the design decision is changed. The protection involves providing a stable interface which protects the remainder of the program from the implementation (the details that are most likely to change). Written another way, information hiding is the ability to prevent certain aspects of a class or software component from being accessible to its clients, using either programming language features (like private variables) or an explicit exporting policy. –Wikipedia³⁴
Consider a stack³⁵ data structure. There are three basic operations: Pushing a value onto the top (push), popping a value off the top (pop), and testing to see whether the stack is empty or not (isEmpty). These three operations are the stable interface. ³²http://userpage.fu-berlin.de/~ram/pub/pub_jf47ht81Ht/doc_kay_oop_en ³³“A language construct that facilitates the bundling of data with the methods (or other functions) operating on that data.”–Wikipedia ³⁴https://en.wikipedia.org/wiki/Information_hiding ³⁵https://en.wikipedia.org/wiki/Stack_
JavaScript’s Objects
24
Many stacks have an array for holding the contents of the stack. This is relatively stable. You could substitute a linked list, but in JavaScript, the array is highly efficient. You might need an index, you might not. You could grow and shrink the array, or you could allocate a fixed size and use an index to keep track of how much of the array is in use. The design choices for keeping track of the head of the list are often driven by performance considerations. If you expose the implementation detail such as whether there is an index, sooner or later some programmer is going to find an advantage in using the index directly. For example, she may need to know the size of a stack. The ideal choice would be to add a size function that continues to hide the implementation. But she’s in a hurry, so she reads the index directly. Now her code is coupled to the existence of an index, so if we wish to change the implementation to grow and shrink the array, we will break her code. The way to avoid this is to hide the array and index from other code and only expose the operations we have deemed stable. If and when someone needs to know the size of the stack, we’ll add a size function and expose it as well. Hiding information (or “state”) is the design principle that allows us to limit the coupling between components of software.
how do we hide state using javascript? We’ve been introduced to JavaScript’s objects, and it’s fairly easy to see that objects can be used to model what other programming languages call (variously) records, structs, frames, or what-haveyou. And given that their elements are mutable, they can clearly model state. Given an object that holds our state (an array and an index³⁶), we can easily implement our three operations as functions. Bundling the functions with the state does not require any special “magic” features. JavaScript objects can have elements of any type, including functions: var stack = (function () { var obj = { array: [], index: -1, push: function (value) { return obj.array[obj.index += 1] = value }, pop: function () { var value = obj.array[obj.index]; obj.array[obj.index] = void 0; if (obj.index >= 0) { obj.index -= 1 } ³⁶Yes, there’s another way to track the size of the array, but we don’t need it to demonstrate encapsulation and hiding of state.
JavaScript’s Objects
25
return value }, isEmpty: function () { return obj.index < 0 } }; return obj; })(); stack.isEmpty() //=> true stack.push('hello') //=> 'hello' stack.push('JavaScript') //=> 'JavaScript' stack.isEmpty() //=> false stack.pop() //=> 'JavaScript' stack.pop() //=> 'hello' stack.isEmpty() //=> true
method-ology In this text, we lurch from talking about “functions that belong to an object” to “methods.” Other languages may separate methods from functions very strictly, but in JavaScript every method is a function but not all functions are methods. The view taken in this book is that a function is a method of an object if it belongs to that object and interacts with that object in some way. So the functions implementing the operations on the stack are all absolutely methods of the stack. But these two wouldn’t be methods. Although they “belong” to an object, they don’t interact with it:
JavaScript’s Objects
26
{ min: function (x, y) { if (x < y) { return x } else { return y } } max: function (x, y) { if (x > y) { return x } else { return y } } }
hiding state Our stack does bundle functions with data, but it doesn’t hide its state. “Foreign” code could interfere with its array or index. So how do we hide these? We already have a closure, let’s use it: var stack = (function () { var array = [], index = -1; return { push: function (value) { array[index += 1] = value }, pop: function () { var value = array[index]; if (index >= 0) { index -= 1 } return value }, isEmpty: function () { return index < 0 }
JavaScript’s Objects
27
} })(); stack.isEmpty() //=> true stack.push('hello') //=> 'hello' stack.push('JavaScript') //=> 'JavaScript' stack.isEmpty() //=> false stack.pop() //=> 'JavaScript' stack.pop() //=> 'hello' stack.isEmpty() //=> true
We don’t want to repeat this code every time we want a stack, so let’s make ourselves a “stack maker.” The temptation is to wrap what we have above in a function: var StackMaker = function () { return (function () { var array = [], index = -1; return { push: function (value) { array[index += 1] = value }, pop: function () { var value = array[index]; if (index >= 0) { index -= 1 } return value }, isEmpty: function () { return index < 0 } } })() }
JavaScript’s Objects
28
But there’s an easier way :-) var StackMaker = function () { var array = [], index = -1; return { push: function (value) { array[index += 1] = value }, pop: function () { var value = array[index]; if (index >= 0) { index -= 1 } return value }, isEmpty: function () { return index < 0 } } }; stack = StackMaker()
Now we can make stacks freely, and we’ve hidden their internal data elements. We have methods and encapsulation, and we’ve built them out of JavaScript’s fundamental functions and objects. A little further on, we’ll look at JavaScript’s support for class-oriented programming and some of the idioms that functions bring to the party.
is encapsulation “object-oriented?” We’ve built something with hidden internal state and “methods,” all without needing special def or private keywords. Mind you, we haven’t included all sorts of complicated mechanisms to support inheritance, mixins, and other opportunities for debating the nature of the One True Object-Oriented Style on the Internet. Then again, the key lesson experienced programmers repeat–although it often falls on deaf ears–is Composition instead of Inheritance. So maybe we aren’t missing much. http://www.c2.com/cgi/wiki?CompositionInsteadOfInheritance
.
JavaScript’s Objects
29
Composition and Extension A deeply fundamental practice is to build components out of smaller components. The choice of how to divide a component into smaller components is called factoring, after the operation in number theory ³⁷. The simplest and easiest way to build components out of smaller components in JavaScript is also the most obvious: Each component is a value, and the components can be put together into a single object or encapsulated with a closure. Here’s an abstract “model” that supports undo and redo composed from a pair of stacks (see Encapsulating State) and a Plain Old JavaScript Object: // helper function // // For production use, consider what to do about // deep copies and own keys var shallowCopy = function (source) { var dest = {}, key; for (key in source) { dest[key] = source[key] } return dest }; // our model maker var ModelMaker = function (initialAttributes) { var attributes = shallowCopy(initialAttributes || {}), undoStack = StackMaker(), redoStack = StackMaker(), obj = { set: function (attrsToSet) { var key; undoStack.push(shallowCopy(attributes)); if (!redoStack.isEmpty()) { redoStack = StackMaker() } for (key in (attrsToSet || {})) { ³⁷And when you take an already factored component and rearrange things so that it is factored into a different set of subcomponents without altering its behaviour, you are refactoring.
JavaScript’s Objects
30
attributes[key] = attrsToSet[key] } return obj }, undo: function () { if (!undoStack.isEmpty()) { redoStack.push(shallowCopy(attributes)); attributes = undoStack.pop() } return obj }, redo: function () { if (!redoStack.isEmpty()) { undoStack.push(shallowCopy(attributes)); attributes = redoStack.pop() } return obj }, get: function (key) { return attributes(key) }, has: function (key) { return attributes.hasOwnProperty(key) }, attributes: function { shallowCopy(attributes) } }; return obj };
The techniques used for encapsulation work well with composition. In this case, we have a “model” that hides its attribute store as well as its implementation that is composed of an undo stack and redo stack.
extension Another practice that many people consider fundamental is to extend an implementation. Meaning, they wish to define a new data structure in terms of adding new operations and semantics to an existing data structure. Consider a queue³⁸: ³⁸http://duckduckgo.com/Queue_
31
JavaScript’s Objects
var QueueMaker = function () { var array = [], head = 0, tail = -1; return { pushTail: function (value) { return array[tail += 1] = value }, pullHead: function () { var value; if tail >= head { value = array[head]; array[head] = void 0; head += 1; return value } }, isEmpty: function () { return tail < head } } };
Now we wish to create a deque³⁹ by adding pullTail and pushHead operations to our queue.⁴⁰ Unfortunately, encapsulation prevents us from adding operations that interact with the hidden data structures. This isn’t really surprising: The entire point of encapsulation is to create an opaque data structure that can only be manipulated through its public interface. The design goals of encapsulation and extension are always going to exist in tension. Let’s “de-encapsulate” our queue:
³⁹https://en.wikipedia.org/wiki/Double-ended_queue ⁴⁰Before you start wondering whether a deque is-a
“implemented in terms of a.”
queue, we said nothing about types and classes. This relationship is called was-a, or
JavaScriptâ&#x20AC;&#x2122;s Objects
var QueueMaker = function () { var queue = { array: [], head: 0, tail: -1, pushTail: function (value) { return queue.array[queue.tail += 1] = value }, pullHead: function () { var value; if (queue.tail >= queue.head) { value = queue.array[queue.head]; queue.array[queue.head] = void 0; queue.head += 1; return value } }, isEmpty: function () { return queue.tail < queue.head } }; return queue };
Now we can extend a queue into a deque: var DequeMaker = function () { var deque = QueueMaker(), INCREMENT = 4; return extend(deque, { size: function () { return deque.tail - deque.head + 1 }, pullTail: function () { var value; if (!deque.isEmpty()) { value = deque.array[deque.tail]; deque.array[deque.tail] = void 0; deque.tail -= 1;
32
JavaScript’s Objects
return value } }, pushHead: function (value) { var i; if (deque.head === 0) { for (i = deque.tail; i <= deque.head; i++) { deque.array[i + INCREMENT] = deque.array[i] } deque.tail += INCREMENT deque.head += INCREMENT } return deque.array[deque.head -= 1] = value } }) };
Presto, we have reuse through extension, at the cost of encapsulation. Encapsulation and Extension exist in a natural state of tension. A program with elaborate encapsulation resists breakage but can also be difficult to refactor in other ways. Be mindful of when it’s best to Compose and when it’s best to Extend.
33
JavaScript’s Objects
34
This and That Let’s take another look at extensible objects. Here’s a Queue: var QueueMaker = function () { var queue = { array: [], head: 0, tail: -1, pushTail: function (value) { return queue.array[queue.tail += 1] = value }, pullHead: function () { var value; if (queue.tail >= queue.head) { value = queue.array[queue.head]; queue.array[queue.head] = void 0; queue.head += 1; return value } }, isEmpty: function () { return queue.tail < queue.head } }; return queue }; queue = QueueMaker() queue.pushTail('Hello') queue.pushTail('JavaScript')
Let’s make a copy of our queue using the extend recipe: copyOfQueue = extend({}, queue); queue !== copyOfQueue //=> true
Wait a second. We know that array values are references. So it probably copied a reference to the original array. Let’s make a copy of the array as well:
JavaScript’s Objects
35
copyOfQueue.array = []; for (var i = 0; i < 2; ++i) { copyOfQueue.array[i] = queue.array[i] }
Now let’s pull the head off the original: queue.pullHead() //=> 'Hello'
If we’ve copied everything properly, we should get the exact same result when we pull the head off the copy: copyOfQueue.pullHead() //=> 'JavaScript'
What!? Even though we carefully made a copy of the array to prevent aliasing, it seems that our two queues behave like aliases of each other. The problem is that while we’ve carefully copied our array and other elements over, the closures all share the same environment, and therefore the functions in copyOfQueue all operate on the first queue’s private data, not on the copies. This is a general issue with closures. Closures couple functions to environments, and that makes them very elegant in the small, and very handy for making opaque data structures. Alas, their strength in the small is their weakness in the large. When you’re trying to make reusable components, this coupling is sometimes a hindrance.
. Let’s take an impossibly optimistic flight of fancy: var AmnesiacQueueMaker = function () { return { array: [], head: 0, tail: -1, pushTail: function (myself, value) { return myself.array[myself.tail += 1] = value }, pullHead: function (myself) { var value;
JavaScript’s Objects
36
if (myself.tail >= myself.head) { value = myself.array[myself.head]; myself.array[myself.head] = void 0; myself.head += 1; return value } }, isEmpty: function (myself) { return myself.tail < myself.head } } }; queueWithAmnesia = AmnesiacQueueMaker(); queueWithAmnesia.pushTail(queueWithAmnesia, 'Hello'); queueWithAmnesia.pushTail(queueWithAmnesia, 'JavaScript')
The AmnesiacQueueMaker makes queues with amnesia: They don’t know who they are, so every time we invoke one of their functions, we have to tell them who they are. You can work out the implications for copying queues as a thought experiment: We don’t have to worry about environments, because every function operates on the queue you pass in. The killer drawback, of course, is making sure we are always passing the correct queue in every time we invoke a function. What to do?
what’s all this? Any time we must do the same repetitive thing over and over and over again, we industrial humans try to build a machine to do it for us. JavaScript is one such machine: BanksQueueMaker = function () { return { array: [], head: 0, tail: -1, pushTail: function (value) { return this.array[this.tail += 1] = value }, pullHead: function () { var value; if (this.tail >= this.head) {
JavaScript’s Objects
37
value = this.array[this.head]; this.array[this.head] = void 0; this.head += 1; return value } }, isEmpty: function () { return this.tail < this.head } } }; banksQueue = BanksQueueMaker(); banksQueue.pushTail('Hello'); banksQueue.pushTail('JavaScript')
Every time you invoke a function that is a member of an object, JavaScript binds that object to the name this in the environment of the function just as if it was an argument.⁴¹ Now we can easily make copies: copyOfQueue = extend({}, banksQueue) copyOfQueue.array = [] for (var i = 0; i < 2; ++i) { copyOfQueue.array[i] = banksQueue.array[i] } banksQueue.pullHead() //=> 'Hello' copyOfQueue.pullHead() //=> 'Hello'
Presto, we now have a way to copy arrays. By getting rid of the closure and taking advantage of this, we have functions that are more easily portable between objects, and the code is simpler as well. There is more to this than we’ve discussed here. We’ll explore things in more detail later, in What Context Applies When We Call a Function?. Closures tightly couple functions to the environments where they are created limiting their flexibility. Using this alleviates the coupling. Copying objects is but one example of where that flexibility is needed. ⁴¹JavaScript also does other things with this as well, but this is all we care about right now.
JavaScript’s Objects
38
What Context Applies When We Call a Function? In This and That, we learned that when a function is called as an object method, the name this is bound in its environment to the object acting as a “receiver.” For example: var someObject = { returnMyThis: function () { return this; } }; someObject.returnMyThis() === someObject //=> true
We’ve constructed a method that returns whatever value is bound to this when it is called. It returns the object when called, just as described.
it’s all about the way the function is called JavaScript programmers talk about functions having a “context” when being called. this is bound to the context.⁴² The important thing to understand is that the context for a function being called is set by the way the function is called, not the function itself. This is an important distinction. Consider closures: As we discussed in Closures and Scope, a function’s free variables are resolved by looking them up in their enclosing functions’ environments. You can always determine the functions that define free variables by examining the source code of a JavaScript program, which is why this scheme is known as Lexical Scope⁴³. A function’s context cannot be determined by examining the source code of a JavaScript program. Let’s look at our example again: var someObject = { someFunction: function () { return this; } }; someObject.someFunction() === someObject //=> true
What is the context of the function someObject.someFunction? Don’t say someObject! Watch this: ⁴²Too bad the language binds the context to the name this instead of the name context! ⁴³https://en.wikipedia.org/wiki/Scope_(computer_science)#Lexical_scoping
JavaScript’s Objects
39
var someFunction = someObject.someFunction; someFunction === someObject.someFunction //=> true someFunction() === someObject //=> false
It gets weirder: var anotherObject = { someFunction: someObject.someFunction } anotherObject.someFunction === someObject.someFunction //=> true anotherObject.someFunction() === anotherObject //=> true anotherObject.someFunction() === someObject //=> false
So it amounts to this: The exact same function can be called in two different ways, and you end up with two different contexts. If you call it using someObject.someFunction() syntax, the context is set to the receiver. If you call it using any other expression for resolving the function’s value (such as someFunction()), you get something else. Let’s investigate: (someObject.someFunction)() == someObject //=> true someObject['someFunction']() === someObject //=> true var name = 'someFunction'; someObject[name]() === someObject //=> true
Interesting!
JavaScript’s Objects
40
var baz; (baz = someObject.someFunction)() === this //=> true
How about: var arr = [ someObject.someFunction ]; arr[0]() == arr //=> true
It seems that whether you use a.b() or a['b']() or a[n]() or (a.b)(), you get context a. var returnThis = function () { return this }; var aThirdObject = { someFunction: function () { return returnThis() } } returnThis() === this //=> true aThirdObject.someFunction() === this //=> true
And if you don’t use a.b() or a['b']() or a[n]() or (a.b)(), you get the global environment for a context, not the context of whatever function is doing the calling. To simplify things, when you call a function with . or [] access, you get an object as context, otherwise you get the global environment.
setting your own context There are actually two other ways to set the context of a function. And once again, both are determined by the caller. At the very end of objects everywhere?, we’ll see that everything in JavaScript behaves like an object, including functions. We’ll learn that functions have methods themselves, and one of them is call. Here’s call in action:
JavaScript’s Objects
41
returnThis() === aThirdObject //=> false returnThis.call(aThirdObject) === aThirdObject //=> true anotherObject.someFunction.call(someObject) === someObject //=> true
When You call a function with call, you set the context by passing it in as the first parameter. Other arguments are passed to the function in the normal manner. Much hilarity can result from call shenanigans like this: var a = [1,2,3], b = [4,5,6]; a.concat([2,1]) //=> [1,2,3,2,1] a.concat.call(b,[2,1]) //=> [4,5,6,2,1]
But now we thoroughly understand what a.b() really means: It’s synonymous with a.b.call(a). Whereas in a browser, c() is synonymous with c.call(window).
apply, arguments, and contextualization JavaScript has another automagic binding in every function’s environment. arguments is a special object that behaves a little like an array.⁴⁴ For example: var third = function () { return arguments[2] } third(77, 76, 75, 74, 73) //=> 75
Hold that thought for a moment. JavaScript also provides a fourth way to set the context for a function. apply is a method implemented by every function that takes a context as its first argument, and it takes an array or array-like thing of arguments as its second argument. That’s a mouthful, let’s look at an example: ⁴⁴Just enough to be frustrating, to be perfectly candid!
JavaScript’s Objects
42
third.call(this, 1,2,3,4,5) //=> 3 third.apply(this, [1,2,3,4,5]) //=> 3
Now let’s put the two together. Here’s another travesty: var a = [1,2,3], accrete = a.concat; accrete([4,5]) //=> Gobbledygook!
We get the result of concatenating [4,5] onto an array containing the global environment. Not what we want! Behold: var contextualize = function (fn, context) { return function () { return fn.apply(context, arguments); } } accrete = contextualize(a.concat, a); accrete([4,5]); //=> [ 1, 2, 3, 4, 5 ]
Our contextualize function returns a new function that calls a function with a fixed context. It can be used to fix some of the unexpected results we had above. Consider: var aFourthObject = {}, returnThis = function () { return this; }; aFourthObject.uncontextualized = returnThis; aFourthObject.contextualized = contextualize(returnThis, aFourthObject); aFourthObject.uncontextualized() === aFourthObject //=> true aFourthObject.contextualized() === aFourthObject //=> true
Both are true because we are accessing them with aFourthObject. Now we write:
JavaScriptâ&#x20AC;&#x2122;s Objects
43
var uncontextualized = aFourthObject.uncontextualized, contextualized = aFourthObject.contextualized; uncontextualized() === aFourthObject; //=> false contextualized() === aFourthObject //=> true
When we call these functions without using aFourthObject., only the contextualized version maintains the context of aFourthObject. Weâ&#x20AC;&#x2122;ll return to contextualizing methods later, in Binding. But before we dive too deeply into special handling for methods, we need to spend a little more time looking at how functions and methods work.
JavaScript’s Objects
Extending Objects It’s very common to want to “extend” a simple object by adding properties to it: var inventory = { apples: 12, oranges: 12 }; inventory.bananas = 54; inventory.pears = 24;
It’s also common to want to add a shallow copy⁴⁵ of the properties of one object to another: for (var fruit in shipment) { inventory[fruit] = shipment[fruit] }
Both needs can be met with this recipe for extend: var extend = variadic( function (consumer, providers) { var 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 });
You can copy an object by extending an empty object:
⁴⁵https://en.wikipedia.org/wiki/Object_copy#Shallow_copy
44
JavaScriptâ&#x20AC;&#x2122;s Objects
extend({}, { apples: 12, oranges: 12 }) //=> { apples: 12, oranges: 12 }
You can extend one object with another: var inventory = { apples: 12, oranges: 12 }; var shipment = { bananas: 54, pears: 24 } extend(inventory, shipment) //=> { apples: 12, // oranges: 12, // bananas: 54, // pears: 24 }
And when we discuss prototypes, we will use extend to turn this: var Queue = function () { this.array = []; this.head = 0; this.tail = -1 }; Queue.prototype.pushTail = function (value) { // ... }; Queue.prototype.pullHead = function () { // ... }; Queue.prototype.isEmpty = function () { // ... }
Into this:
45
JavaScript’s Objects
46
var Queue = function () { extend(this, { array: [], head: 0, tail: -1 }) }; extend(Queue.prototype, { pushTail: function (value) { // ... }, pullHead: function () { // ... }, isEmpty: function () { // ... } });
As we build more complex objects with more complex structures, we will revisit “extend” and improve on it.
Object Recipes
⁴⁶
⁴⁶Londinium 1 Lever Espresso Machine (c) 2013 Alejandro Erickson, some rights reserved
Object Recipes
In This Chapter In this chapter, weâ&#x20AC;&#x2122;ll review some of the things you can do with JavaScriptâ&#x20AC;&#x2122;s objects, especially with the way properties are defined and used.
.
48
Object Recipes
49
Structs and Value Objects Sometimes we want to share objects by reference for performance and space reasons, but we donâ&#x20AC;&#x2122;t want them to be mutable. One motivation is when we want many objects to be able to share a common entity without worrying that one of them may inadvertently change the common entity. JavaScript provides a way to make properties immutable: var rentAmount = {}; Object.defineProperty(rentAmount, 'dollars', { enumerable: true, writable: false, value: 420 }); Object.defineProperty(rentAmount, 'cents', { enumerable: true, writable: false, value: 0 }); rentAmount.dollars //=> 420
// Strict Mode: !function () { "use strict" rentAmount.dollars = 600; }(); //=> TypeError: Cannot assign to read only property 'dollars' of #<Object> // Beware: Non-Strict Mode rentAmount.dollars = 600; //=> 600 rentAmount.dollars //=> 420
Object Recipes
50
Object.defineProperty is a general-purpose method for providing fine-grained control over the properties of any object. When we make a property enumerable, it shows up whenever we list the
object’s properties or iterate over them. When we make it writable, assignments to the property change its value. If the property isn’t writable, assignments are ignored. When we want to define multiple properties, we can also write: var rentAmount = {}; Object.defineProperties(rentAmount, { dollars: { enumerable: true, writable: false, value: 420 }, cents: { enumerable: true, writable: false, value: 0 } }); rentAmount.dollars //=> 420 rentAmount.dollars = 600; //=> 600 rentAmount.dollars //=> 420
We can make properties immutable, but that doesn’t prevent us from adding properties to an object: rentAmount.feedbackComments = [] rentAmount.feedbackComments.push("The rent is too damn high.") rentAmount //=> { dollars: 420, cents: 0, feedbackComments: [ 'The rent is too damn high.' ] }
Immutable properties make an object closed for modification. This is a separate matter from making it closed for extension. But we can do that too:
Object Recipes
51
Object.preventExtensions(rentAmount); function addCurrency(amount, currency) { "use strict"; amount.currency = currency; return currency; } addCurrency(rentAmount, "CAD") //=> TypeError: Can't add property currency, object is not extensible
structs Many other languages have a formal data structure that has one or more named properties that are open for modification, but closed for extension. Hereâ&#x20AC;&#x2122;s a function that makes a Struct: // Mutable: function Struct (template) { if (Struct.prototype.isPrototypeOf(this)) { var struct = this; Object.keys(template).forEach(function (key) { Object.defineProperty(struct, key, { enumerable: true, writable: true, value: template[key] }); }); return Object.preventExtensions(struct); } else return new Struct(template); } var rentAmount2 = Struct({dollars: 420, cents: 0}); addCurrency(rentAmount2, "ISK"); //=> TypeError: Can't add property currency, object is not extensible // Immutable: function Value (template) {
Object Recipes
52
if (Value.prototype.isPrototypeOf(this)) { var immutableObject = this; Object.keys(template).forEach(function (key) { Object.defineProperty(immutableObject, key, { enumerable: true, writable: false, value: template[key] }); }); return Object.preventExtensions(immutableObject); } else return new Value(template); } Value.prototype = new Struct({}); function copyAmount(to, from) { "use strict" to.dollars = from.dollars; to.cents = from.cents; return to; } var rentValue = Value({dollars: 1000, cents: 0}); copyAmount(rentValue, rentAmount); //=> TypeError: Cannot assign to read only property 'dollars' of #<Struct>
Structs and Values are a handy way to prevent inadvertent errors and to explicitly communicate that an object is intended to be used as a struct and not as a dictionary.⁴⁷
value objects A dictum that we will repeat from time to time is: With few exceptions, a programming system cannot be improved solely by removing features that can be subject to abuse. Instead, a system is improved by removing harmful features in such a way that they enable the addition of other, more powerful features that were “blocked” by the existence of harmful features. ⁴⁷JavaScript
also provides a single method that can close an object for modification, extension, and configuration at the same time:
Object.freeze(...).
53
Object Recipes
Our Value type above removes the ability to modify or extend a value. This does remove the possibility of making an accidental mistake. But does it make something else possible? Yes. Consider this problem: var juneRent = {dollars: 420, cents: 0}, julyRent = {dollars: 420, cents: 0}; juneRent === julyRent; //=> false
The june and july rents aren’t ===, because === performs an object identity check for non-primitive values. juneRent and julyRent are structurally equivalent, but not references to the same object, so they aren’t ===. No problem, let’s whip up a stuctural equivalence function: function eqv (a, b) { var akeys, bkeys; if (a === b) { return true; } else if (typeof a === 'number') { return false; } else if (typeof a === 'boolean') { return false; } else if (typeof a === 'string') { return false; } else { akeys = Object.keys(a); bkeys = Object.keys(b); if (akeys.length !== bkeys.length) { return false; } else return akeys.every(function (key) { return eqv(a[key], b[key]); }); }
54
Object Recipes
} eqv(juneRent, julyRent); //=> true
This looks very handy. For example, if we are fetching rows of data in JSON from a server, structural equivalence is the only way to compare two rows for “equality.” But there is a flaw. Note: eqv(juneRent, julyRent); //=> true juneRent.cents = 6;
Our test for equivalence can be broken, it only tests what was true at one moment in time. Thus, objects that are Values don’t behave like numbers, booleans, or strings that are values. If 3 === 3, it will always be ===, because numbers, booleans, and strings are immutable. We can create new values with functions and operators, but the old ones don’t change. If we want object equivalence to mean anything, we need it to only apply to immutable objects. That sounds familiar, and thats why we called an immutable Struct a Value. Let’s do one more thing with it: (function () { function eqv (a, b) { var akeys, bkeys; if (a === b) { return true; } else if (a instanceof Value && b instanceof Value){ akeys = Object.keys(a); bkeys = Object.keys(b); if (akeys.length !== bkeys.length) { return false; } else return akeys.every(function (key) { return eqv(a[key], b[key]); }); } else return false;
Object Recipes
55
} Value.eqv = eqv; Value.prototype.eqv = function (that) { return eqv(this, that); }; return Value; })(); Value.eqv(juneRent, julyRent); //=> false var juneRentValue = new Value({dollars: 420, cents: 0}), julyRentValue = new Value({dollars: 420, cents: 0}); Value.eqv(juneRentValue, julyRentValue); //=> true juneRentValue.eqv(julyRentValue); //=> true
Immutability makes structural equivalence testing meaningful, which in turn makes value objects meaningful.
Object Recipes
56
Accessors TODO: This belongs in a general sense to describe private values and private methods for a naïve object. Need to discuss constructing multiple objects with this technique? The Java and Ruby folks are very comfortable with a general practice of not allowing objects to modify each other’s properties. They prefer to write getters and setters, functions that do the getting and setting. If we followed this practice, we might write: var mutableAmount = (function () { var _dollars = 0; var _cents = 0; return immutable({ setDollars: function (amount) { return (_dollars = amount); }, getDollars: function () { return _dollars; }, setCents: function (amount) { return (_cents = amount); }, getCents: function () { return _cents; } }); })(); mutableAmount.getDollars() //=> 0 mutableAmount.setDollars(420); mutableAmount.getDollars() //=> 420
We’ve put functions in the object for getting and setting values, and we’ve hidden the values themselves in a closure, the environment of an Immediately Invoked Function Expression⁴⁸ (“IIFE”). Of course, this amount can still be mutated, but we are now mediating access with functions. We could, for example, enforce certain validity rules: ⁴⁸https://en.wikipedia.org/wiki/Immediately-invoked_function_expression
Object Recipes
var mutableAmount = (function () { var _dollars = 0; var _cents = 0; return immutable({ setDollars: function (amount) { if (amount >= 0 && amount === Math.floor(amount)) return (_dollars = amount); }, getDollars: function () { return _dollars; }, setCents: function (amount) { if (amount >= 0 && amount < 100 && amount === Math.floor(amount)) return (_cents = amount); }, getCents: function () { return _cents; } }); })(); mutableAmount.setDollars(-5) //=> undefined mutableAmount.getDollars() //=> 0
Immutability is easy, just leave out the â&#x20AC;&#x153;setters:â&#x20AC;? var rentAmount = (function () { var _dollars = 420; var _cents = 0; return immutable({ getDollars: function () { return _dollars; }, getCents: function () { return _cents; } }); })();
57
Object Recipes
58
mutableAmount.setDollars(-5) //=> undefined mutableAmount.getDollars() //=> 0
using accessors for properties Languages like Ruby allow you to write code that looks like youâ&#x20AC;&#x2122;re doing direct access of properties but still mediate access with functions. JavaScript allows this as well. Letâ&#x20AC;&#x2122;s revisit Object.defineProperties: var mediatedAmount = (function () { var _dollars = 0; var _cents = 0; var amount = {}; Object.defineProperties(amount, { dollars: { enumerable: true, set: function (amount) { if (amount >= 0 && amount === Math.floor(amount)) return (_dollars = amount); }, get: function () { return _dollars; } }, cents: { enumerable: true, set: function (amount) { if (amount >= 0 && amount < 100 && amount === Math.floor(amount)) return (_cents = amount); }, get: function () { return _cents; } } }); return amount; })(); //=> { dollars: [Getter/Setter],
Object Recipes
cents: [Getter/Setter] } mediatedAmount.dollars = 600; mediatedAmount.dollars //=> 600 mediatedAmount.cents = 33.5 mediatedAmount.cents //=> 0
We can leave out the setters if we wish: var mediatedImmutableAmount = (function () { var _dollars = 420; var _cents = 0; var amount = {}; Object.defineProperties(amount, { dollars: { enumerable: true, get: function () { return _dollars; } }, cents: { enumerable: true, get: function () { return _cents; } } }); return amount; })(); mediatedImmutableAmount.dollars = 600; mediatedImmutableAmount.dollars //=> 420
Once again, the failure is silent. Of course, we can change that:
59
Object Recipes
var noisyAmount = (function () { var _dollars = 0; var _cents = 0; var amount = {}; Object.defineProperties(amount, { dollars: { enumerable: true, set: function (amount) { if (amount !== _dollars) throw new Error("You can't change that!"); }, get: function () { return _dollars; } }, cents: { enumerable: true, set: function (amount) { if (amount !== _cents) throw new Error("You can't change that!"); }, get: function () { return _cents; } } }); return amount; })(); noisyAmount.dollars = 500 //=> Error: You can't change that!
60
Object Recipes
61
Hiding Object Properties Many “OO” programming languages have the notion of private instance variables, properties that cannot be accessed by other entities. JavaScript has no such notion, we have to use specific techniques to create the illusion of private state for objects.
enumerability In JavaScript, there is only one kind of “privacy” for properties. But it’s not what you expect. When an object has properties, you can access them with the dot notation, like this: var dictionary = { abstraction: "an abstract or general idea or term", encapsulate: "to place in or as if in a capsule", object: "anything that is visible or tangible and is relatively stable in form" }; dictionary.encapsulate //=> 'to place in or as if in a capsule'
You can also access properties indirectly through the use of [] notation and the value of an expression: dictionary[abstraction] //=> ReferenceError: abstraction is not defined
Whoops, the value of an expression: The expression abstraction looks up the value associated with the variable “abstraction.” Alas, such a variable hasn’t been defined in this code, so that’s an error. This works, because ‘abstraction’ is an expression that evaluates to the string we want: dictionary['abstraction'] //=> 'an abstract or general idea or term'
One kind of privacy concerns who has access to properties. In JavaScript, all code has access to all properties of every object. There is no way to create a property of an object such that some functions can access it and others cannot. So what kind of privacy does JavaScript provide? In order to access a property, you have to know its name. If you don’t know the names of an object’s properties, you can access the names in several ways. Here’s one:
Object Recipes
62
Object.keys(dictionary) //=> [ 'abstraction', 'encapsulate', 'object' ]
This is called enumerating an object’s properties. Not only are they “public” in the sense that any code that knows the property’s names can access it, but also, any code at all can enumerate them. You can do neat things with enumerable properties, such as: var descriptor = map(Object.keys(dictionary), function (key) { return key + ': "' + dictionary[key] + '"'; }).join('; '); descriptor //=> 'abstraction: "an abstract or general idea or term"; encapsulate: "to place i\ n or as if in a capsule"; object: "anything that is visible or tangible and is relatively sta\ ble in form"'
So, our three properties are accessible and also enumerable. Are there any properties that are accessible, but not enumerable? There sure can be. You recall that we can define properties using Object.defineProperty. One of the options is called, appropriately enough, enumerable. Let’s define a getter that isn’t enumerable: Object.defineProperty(dictionary, 'length', { enumerable: false, get: function () { return Object.keys(this).length } }); dictionary.length //=> 3
Notice that length obviously isn’t included in Object.keys, otherwise our little getter would return 4, not 3. And it doesn’t affect our little descriptor expression, let’s evaluate it again:
Object Recipes
63
map(Object.keys(dictionary), function (key) { return key + ': "' + dictionary[key] + '"'; }).join('; ') //=> 'abstraction: "an abstract or general idea or term"; encapsulate: "to place i\ n or as if in a capsule"; object: "anything that is visible or tangible and is relatively sta\ ble in form"'
Non-enumerable properties don’t have to be getters: Object.defineProperty(dictionary, 'secret', { enumerable: false, writable: true, value: "kept from the knowledge of any but the initiated or privileged" }); dictionary.secret //=> 'kept from the knowledge of any but the initiated or privileged' dictionary.length //=> 3 secret is indeed a secret. It’s fully accessible if you know it’s there, but it’s not enumerable, so it doesn’t show up in Object.keys.
One way to “hide” properties in JavaScript is to define them as properties with enumerable: false.
closures We saw earlier that it is possible to fake private instance variables by hiding references in a closure, e.g. function immutable (propertiesAndValues) { return tap({}, function (object) { for (var key in propertiesAndValues) { if (propertiesAndValues.hasOwnProperty(key)) { Object.defineProperty(object, key, { enumerable: true, writable: false, value: propertiesAndValues[key] });
Object Recipes
64
} } }); } var rentAmount = (function () { var _dollars = 420; var _cents = 0; return immutable({ dollars: function () { return _dollars; }, cents: function () { return _cents; } }); })(); _dollars and _cents aren’t properties of the rentAmount object at all, they’re variables within the environment of an IIFE. The functions associated with dollars and cents are within its scope, so
they have access to its variables. This has some obvious space and performance implications. There’s also the general problem that an environment like a closure is its own thing in JavaScript that exists outside of the language’s usual features. For example, you can iterate over the enumerable properties of an object, but you can’t iterate over the variables being used inside of an object’s functions. Another example: you can access a property indirectly with [expression]. You can’t access a closure’s variable indirectly without some clever finagling using eval. Finally, there’s another very real problem: Each and every function belonging to each and every object must be a distinct entity in JavaScript’s memory. Let’s make another amount using the same pattern as above: var rentAmount2 = (function () { var _dollars = 600; var _cents = 0; return immutable({ dollars: function () { return _dollars; }, cents: function () { return _cents; }
Object Recipes
65
}); })();
We now have defined four functions: Two getters for rentAmount, and two for rentAmount2. Although the two dollars functions have identical code, they’re completely different entities to JavaScript because each has a different enclosing environment. The same thing goes for the two cents functions. In the end, we’re going to create an enclosing environment and two new functions every time we create an amount using this pattern.
naming conventions Let’s compare this to a different approach. We’ll write almost the identical code, but we’ll rely on a naming convention to hide our values in plain sight: function dollars () { return this._dollars; } function cents () { return this._cents; } var rentAmount = immutable({ dollars: dollars, cents: cents }); rentAmount._dollars = 420; rentAmount._cents = 0;
Our convention is that other entities should not modify any property that has a name beginning with _. There’s no enforcement, it’s just a practice. Other entities can use getters and setters. We’ve created two functions, and we’re using this to make sure they refer to the object’s environment. With this pattern, we need two functions and one object to represent an amount. One problem with this approach, of course, is that everything we’re using is enumerable:
Object Recipes
Object.keys(rentAmount) //=> [ 'dollars', 'cents', '_dollars', '_cents' ]
We’d better fix that: Object.defineProperties(rentAmount, { _dollars: { enumerable: false, writable: true }, _cents: { enumerable: false, writable: true } });
Let’s create another amount: var raisedAmount = immutable({ dollars: dollars, cents: cents }); Object.defineProperties(raisedAmount, { _dollars: { enumerable: false, writable: true }, _cents: { enumerable: false, writable: true } }); raisedAmount._dollars = 600; raisedAmount._cents = 0;
We create another object, but we can reuse the existing functions. Let’s make sure:
66
Object Recipes
67
rentAmount.dollars() //=> 420 raisedAmount.dollars() //=> 600
What does this accomplish? Well, it “hides” the raw properties by making them enumerable, then provides access (if any) to other objects through functions that can be shared amongst multiple objects. As we saw earlier, this allows us to choose whether to expose setters as well as getters, it allows us to validate inputs, or even to have non-enumerable properties that are used by an object’s functions to hold state. The naming convention is useful, and of course you can use whatever convention you like. My personal preference for a very long time was to preface private names with my, such as myDollars. Underscores work just as well, and that’s what we’ll use in this book.
summary JavaScript does not have a way to enforce restrictions on accessing an object’s properties: Any code that knows the name of a property can access the value, setter, or getter that has been defined for the object. Private data can be faked with closures, at a cost in memory. javaScript does allow properties to be non-enumerable. In combination with a naming convention and/or setters and getters, a reasonable compromise can be struck between fully private instance variables and completely open access.
Object Recipes
68
Proxies When we discuss Metaobjects, we’ll look at a technique called forwarding, wherein one object’s functions call the exact same function in another object. In the simple case where the only methods an object has are those that it forwards to another object, we call that object a proxy⁴⁹ for the other object, which we call the base object, because it looks and behaves like the base object. For example: var stackBase = { 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; } }; var stackProxy = { push: function (value) { return stackBase.push(value); }, pop: function () { return stackBase.pop(); }, isEmpty: function () { return stackBase.isEmpty(); } };
A proxy behaves like the base object, but hides the base object’s properties: ⁴⁹https://en.wikipedia.org/wiki/Proxy_pattern
Object Recipes
69
stackProxy.push('hello'); stackProxy.push('base'); stackProxy.pop(); //=> 'base' stackProxy //=> { push: [Function], pop: [Function], isEmpty: [Function] }
Of course, we can automate the writing of functions: function proxy (baseObject) { var proxyObject = Object.create(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; } var stack = proxy(stackBase); stack.push('auto'); stack.pop() //=> 'auto'
A proxy isn’t the original object, but it is often more than good enough. In some designs, the base objects are only ever manipulated through proxies. We create proxies with a transformation function. Later on, we’ll see more sophisticated transformation functions that can create partial proxies (proxies for only some of the base object’s behaviours), as well as proxies that control the creation of private state.
Instances and Prototypes
⁵⁰
⁵⁰Vacuum Pots (c) 2012 Olin Viydo, some rights reserved
Instances and Prototypes
In This Chapter In this chapter, we’ll look at JavaScript’s extremely simple (and undeniably useful) mechanism for delegating behaviour to prototypes. In later chapters we’ll discuss mixins, forwarding, and other kinds of delegation. But for now, let’s look at what JavaScript makes obvious and easy, prototypes.
.
71
Instances and Prototypes
72
Prototypes are Simple, it’s the Explanations that are Hard To Understand As you recall from our code for making objects extensible, we wrote a function that returned a Plain Old JavaScript Object. The colloquial term for this kind of function is a “Factory Function.” Let’s strip a function down to the very bare essentials: var Ur = function () {};
This doesn’t look like a factory function: It doesn’t have an expression that yields a Plain Old JavaScript Object when the function is applied. Yet, there is a way to make an object out of it. Behold the power of the new keyword: new Ur() //=> {}
We got an object back! What can we find out about this object? new Ur() === new Ur() //=> false
Every time we call new with a function and get an object back, we get a unique object. We could call these “Objects created with the new keyword,” but this would be cumbersome. So we’re going to call them instances. Instances of what? Instances of the function that creates them. So given var i = new Ur(), we say that i is an instance of Ur. For reasons that will be explained after we’ve discussed prototypes, we also say that Ur is the constructor of i, and that Ur is a constructor function. Therefore, an instance is an object created by using the new keyword on a constructor function, and that function is the instance’s constructor.
prototypes There’s more. Here’s something interesting: Ur.prototype //=> {}
What’s this prototype? Let’s run our standard test:
Instances and Prototypes
73
(function () {}).prototype === (function () {}).prototype //=> false
Every function is initialized with its own unique prototype. What does it do? Let’s try something: Ur.prototype.language = 'JavaScript'; var continent = new Ur(); //=> {} continent.language //=> 'JavaScript'
That’s very interesting! Instances seem to behave as if they had the same elements as their constructor’s prototype. Let’s try a few things: continent.language = 'CoffeeScript'; continent //=> {language: 'CoffeeScript'} continent.language //=> 'CoffeeScript' Ur.prototype.language 'JavaScript'
You can set elements of an instance, and they “override” the constructor’s prototype, but they don’t actually change the constructor’s prototype. Let’s make another instance and try something else. var another = new Ur(); //=> {} another.language //=> 'JavaScript'
New instances don’t acquire any changes made to other instances. Makes sense. And: Ur.prototype.language = 'Sumerian' another.language //=> 'Sumerian'
Even more interesting: Changing the constructor’s prototype changes the behaviour of all of its instances. This strongly implies that there is a dynamic relationship between instances and their constructors, rather than some kind of mechanism that makes objects by copying.⁵¹ Speaking of prototypes, here’s something else that’s very interesting: ⁵¹For many programmers, the distinction between a dynamic relationship and a copying mechanism is too fine to worry about. However, it makes many dynamic program modifications possible.
Instances and Prototypes
74
continent.constructor //=> [Function] continent.constructor === Ur //=> true
Every instance acquires a constructor element that is initialized to their constructor. This is true even for objects we don’t create with new in our own code: {}.constructor //=> [Function: Object]
If that’s true, what about prototypes? Do they have constructors? Ur.prototype.constructor //=> [Function] Ur.prototype.constructor === Ur //=> true
Very interesting! We will take another look at the constructor element when we discuss extending objects with delegation. But let’s get back to prototypes: function C () {} C.prototype.bodyOfWater = 'sea' C.prototype //=> { bodyOfWater: 'sea' } var c = new C(); Object.getPrototypeOf(c) //=> { bodyOfWater: 'sea' } C.prototype.isPrototypeOf(c) //=> true getPrototypeOf and isPrototypeOf are very useful. Let’s see how:
Instances and Prototypes
75
var oldProto = C.prototype; C.prototype = { bodyOfWater: 'ocean' }; Object.getPrototypeOf(c) //=> { bodyOfWater: 'sea' } C.prototype.isPrototypeOf(c) //=> false oldProto.isPrototypeOf(c) //=> true
Changing a the object bound to a function’s prototype property doesn’t change the prototype for any objects that have already been created with new.
create You can create objects and assign them prototypes without new. This object doesn’t have a prototype: var obj = Object.create(null); Object.getPrototypeOf(obj) //=> null
This object has the same prototype as an object we create using literal object syntax, or an object we create using new Object() obj = Object.create(Object.prototype); Object.getPrototypeOf(obj) //=> {} Object.getPrototypeOf(obj) === Object.getPrototypeOf({}) //=> true Object.getPrototypeOf(obj) === Object.getPrototypeOf(new Object()) //=> true
This object has a prototype of our choosing:
Instances and Prototypes
76
var ourPrototype = { bodyOfWater: 'ocean' }; obj = Object.create(ourPrototype); ourPrototype.isPrototypeOf(obj) //=> true
We can create an object with any prototype we like, without writing a constructor or using new.
revisiting this idea of queues Letâ&#x20AC;&#x2122;s rewrite our Queue to use new and .prototype, using this and our extendsObject helper from Composition and Extension: var Queue = function () { extend(this, { array: [], head: 0, tail: -1 }) }; extend(Queue.prototype, { pushTail: function (value) { return this.array[this.tail += 1] = value }, pullHead: function () { var value; if (!this.isEmpty()) { value = this.array[this.head] this.array[this.head] = void 0; this.head += 1; return value } }, isEmpty: function () { return this.tail < this.head } })
You recall that when we first looked at this, we only covered the case where a function that belongs to an object is invoked. Now we see another case: When a function is invoked by the new operator, this is set to the new object being created. Thus, our code for Queue initializes the queue.
Instances and Prototypes
77
You can see why this is so handy in JavaScript: We wouldn’t be able to define functions in the prototype that worked on the instance if JavaScript didn’t give us an easy way to refer to the instance itself.
objects everywhere? Now that you know about prototypes, it’s time to acknowledge something that even small children know: Everything in JavaScript behaves like an object, everything in JavaScript behaves like an instance of a function, and therefore everything in JavaScript behaves as if it delegates some methods to its constructor’s prototype and/or has some elements of its own. For example: 3.14159265.toPrecision(5) //=> '3.1415' 'FORTRAN, SNOBOL, LISP, BASIC'.split(', ') //=> [ 'FORTRAN', # 'SNOBOL', # 'LISP', # 'BASIC' ] [ 'FORTRAN', 'SNOBOL', 'LISP', 'BASIC' ].length //=> 4
Functions themselves are instances, and they have methods. For example, every function has a method call. call’s first argument is a context: When you invoke .call on a function, it invoked the function, setting this to the context. It passes the remainder of the arguments to the function. It seems like objects are everywhere in JavaScript!
impostors You may have noticed that we use “weasel words” to describe how everything in JavaScript behaves like an instance. Everything behaves as if it was created by a function with a prototype. The full explanation is this: As you know, JavaScript has “value types” like String, Number, and Boolean. As noted in the first chapter, value types are also called primitives, and one consequence of the way JavaScript implements primitives is that they aren’t objects. Which means they can be identical to other values of the same type with the same contents, but the consequence of
Instances and Prototypes
78
certain design decisions is that value types don’t actually have methods or constructors. They aren’t instances of some constructor. So. Value types don’t have methods or constructors. And yet: "Spence Olham".split(' ') //=> ["Spence", "Olham"]
Somehow, when we write "Spence Olham".split(' '), the string "Spence Olham" isn’t an instance, it doesn’t have methods, but it does a damn fine job of impersonating an instance of a String constructor. How does "Spence Olham" impersonate an instance? JavaScript pulls some legerdemain. When you do something that treats a value like an object, JavaScript checks to see whether the value actually is an object. If the value is actually a primitive,⁵² JavaScript temporarily makes an object that is a kinda-sorta copy of the primitive and that kindasorta copy has methods and you are temporarily fooled into thinking that "Spence Olham" has a .split method. These kinda-sorta copies are called String instances as opposed to String primitives. And the instances have methods, while the primitives do not. How does JavaScript make an instance out of a primitive? With new, of course. Let’s try it: new String("Spence Olham") //=> "Spence Olham"
The string instance looks just like our string primitive. But does it behave like a string primitive? Not entirely: new String("Spence Olham") === "Spence Olham" //=> false
Aha! It’s an object with its own identity, unlike string primitives that behave as if they have a canonical representation. If we didn’t care about their identity, that wouldn’t be a problem. But if we carelessly used a string instance where we thought we had a string primitive, we could run into a subtle bug: if (userName === "Spence Olham") { getMarried(); goCamping() } ⁵²Recall that Strings, Numbers, Booleans and so forth are value types and primitives. We’re calling them primitives here.
Instances and Prototypes
79
That code is not going to work as we expect should we accidentally bind new String("Spence Olham") to userName instead of the primitive "Spence Olham". This basic issue that instances have unique identities but primitives with the same contents have the same identities–is true of all primitive types, including numbers and booleans: If you create an instance of anything with new, it gets its own identity. There are more pitfalls to beware. Consider the truthiness of string, number and boolean primitives: '' ? 'truthy' : 'falsy' //=> 'falsy' 0 ? 'truthy' : 'falsy' //=> 'falsy' false ? 'truthy' : 'falsy' //=> 'falsy'
Compare this to their corresponding instances: new String('') ? 'truthy' : 'falsy' //=> 'truthy' new Number(0) ? 'truthy' : 'falsy' //=> 'truthy' new Boolean(false) ? 'truthy' : 'falsy' //=> 'truthy'
Our notion of “truthiness” and “falsiness” is that all instances are truthy, even string, number, and boolean instances corresponding to primitives that are falsy. There is one sure cure for “JavaScript Impostor Syndrome.” Just as new PrimitiveType(...) creates an instance that is an impostor of a primitive, PrimitiveType(...) creates an original, canonicalized primitive from a primitive or an instance of a primitive object. For example: String(new String("Spence Olham")) === "Spence Olham" //=> true
Getting clever, we can write this:
Instances and Prototypes
80
var original = function (unknown) { return unknown.constructor(unknown) } original(true) === true //=> true original(new Boolean(true)) === true //=> true
Of course, original will not work for your own creations unless you take great care to emulate the same behaviour. But it does work for strings, numbers, and booleans.
Instances and Prototypes
81
Binding Functions to Contexts Recall that in What Context Applies When We Call a Function?, we adjourned our look at setting the context of a function with a look at a contextualize helper function: var contextualize = function (fn, context) { return function () { return fn.apply(context, arguments) } }, a = [1,2,3], accrete = contextualize(a.concat, a); accrete([4,5]) //=> [ 1, 2, 3, 4, 5 ]
How would this help us in a practical way? Consider building an event-driven application. For example, an MVC application would bind certain views to update events when their models change. The Backbone⁵³ framework uses events just like this: var someView = ..., someModel = ...; someModel.on('change', function () { someView.render() });
This tells someModel that when it invoked a change event, it should call the anonymous function that in turn invoked someView’s .render method. Wouldn’t it be simpler to simply write: someModel.on('change', someView.render);
It would, except that the implementation for .on and similar framework methods looks something like this: Model.prototype.on = function (eventName, callback) { ... callback() ... }
Although someView.render() correctly sets the method’s context as someView, callback() will not. What can we do without wrapping someView.render() in a function call as we did above? ⁵³http://backbonejs.org
Instances and Prototypes
82
binding methods Before enumerating approaches, let’s describe what we’re trying to do. We want to take a method call and treat it as a function. Now, methods are functions in JavaScript, but as we’ve learned from looking at contexts, method calls involve both invoking a function and setting the context of the function call to be the receiver of the method call. When we write something like: var unbound = someObject.someMethod;
We’re binding the name unbound to the method’s function, but we aren’t doing anything with the identity of the receiver. In most programming languages, such methods are called “unbound” methods because they aren’t associated with, or “bound” to the intended receiver. So what we’re really trying to do is get ahold of a bound method, a method that is associated with a specific receiver. We saw an obvious way to do that above, to wrap the method call in another function. Of course, we’re responsible for replicating the arity of the method being bound. For example: var boundSetter = function (value) { return someObject.setSomeValue(value); };
Now our bound method takes one argument, just like the function it calls. We can use a bound method anywhere: someDomField.on('update', boundSetter);
This pattern is very handy, but it requires keeping track of these bound methods. One thing we can do is bind the method “in place,” using the let pattern like this: someObject.setSomeValue = (function () { var unboundMethod = someObject.setSomeValue; return function (value) { return unboundMethod.call(someObject, value); } })();
Now we know where to find it:
Instances and Prototypes
83
someDomField.on('update', someObject.setSomeValue);
This is a very popular pattern, so much so that many frameworks provide helper functions to make this easy. Underscore⁵⁴, for example, provides _.bind to return a bound copy of a function and _.bindAll to bind methods in place: // bind *all* of someObject's methods in place _.bindAll(someObject); // bind setSomeValue and someMethod in place _.bindAll(someObject, 'setSomeValue', 'someMethod');
There are two considerations to ponder. First, we may be converting an instance method into an object method. Specifically, we’re creating an object method that is bound to the object. Most of the time, the only change this makes is that it uses slightly more memory (we’re creating an extra function for each bound method in each object). But if you are a little more dynamic and actually change methods in the prototype, your changes won’t “override” the object methods that you created. You’d have to roll your own binding method that refers to the prototype’s method dynamically or reorganize your code. This is one of the realities of “meta-programming.” Each technique looks useful and interesting in isolation, but when multiple techniques are used together, they can have unpredictable results. It’s not surprising, because most popular languages consider classes and methods to be fairly global, and they handle dynamic changes through side-effects. This is roughly equivalent to programming in 1970s-era BASIC by imperatively changing global variables. If you aren’t working with old JavaScript environments in non-current browsers, you needn’t use a framework or roll your own binding functions: JavaScript has a .bind⁵⁵ method defined for functions: someObject.someMethod = someObject.someMethod.bind(someObject); .bind also does some currying for you, you can bind one or more arguments in addition to the
context. For example:
⁵⁴http://underscorejs.org ⁵⁵https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/bind
Instances and Prototypes
AccountModel.prototype.getBalancePromise(forceRemote) = { // if forceRemote is true, always goes to the remote // database for the most real-time value, returns // a promise. }; var account = new AccountModel(...); var boundGetRemoteBalancePromise = account. getBalancePromise. bind(account, true);
Very handy, and not just for binding contexts! Getting the context right for methods is essential. The commonplace terminology is that we want bound methods rather than unbound methods. Current flavours of JavaScript provide a .bind method to help, and frameworks like Underscore also provide helpers to make binding methods easy.
84
Instances and Prototypes
85
Object Methods An instance method is a function defined in the constructor’s prototype. Every instance acquires this behaviour unless otherwise “overridden.” Instance methods usually have some interaction with the instance, such as references to this or to other methods that interact with the instance. A constructor method is a function belonging to the constructor itself. There is a third kind of method, one that any object (obviously including all instances) can have. An object method is a function defined in the object itself. Like instance methods, object methods usually have some interaction with the object, such as references to this or to other methods that interact with the object. Object methods are really easy to create with Plain Old JavaScript Objects, because they’re the only kind of method you can use. Recall from This and That: QueueMaker = function () { return { array: [], head: 0, tail: -1, pushTail: function (value) { return this.array[this.tail += 1] = value }, pullHead: function () { var value; if (this.tail >= this.head) { value = this.array[this.head]; this.array[this.head] = void 0; this.head += 1; return value } }, isEmpty: function () { return this.tail < this.head } } }; pushTail, pullHead, and isEmpty are object methods. Also, from encapsulation:
Instances and Prototypes
86
var stack = (function () { var obj = { array: [], index: -1, push: function (value) { return obj.array[obj.index += 1] = value }, pop: function () { var value = obj.array[obj.index]; obj.array[obj.index] = void 0; if (obj.index >= 0) { obj.index -= 1 } return value }, isEmpty: function () { return obj.index < 0 } }; return obj; })();
Although they donâ&#x20AC;&#x2122;t refer to the object, push, pop, and isEmpty semantically interact with the opaque data structure represented by the object, so they are object methods too.
object methods within instances Instances of constructors can have object methods as well. Typically, object methods are added in the constructor. Hereâ&#x20AC;&#x2122;s a gratuitous example, a widget model that has a read-only id: var WidgetModel = function (id, attrs) { extend(this, attrs || {}); this.id = function () { return id } } extend(WidgetModel.prototype, { set: function (attr, value) { this[attr] = value; return this; }, get: function (attr) {
Instances and Prototypes
87
return this[attr] } }); set and get are instance methods, but id is an object method: Each object has its own id closure, where id is bound to the id of the widget by the argument id in the constructor. The advantage of
this approach is that instances can have different object methods, or object methods with their own closures as in this case. The disadvantage is that every object has its own methods, which uses up much more memory than instance methods, which are shared amongst all instances. Object methods are defined within the object. So if you have several different â&#x20AC;&#x153;instancesâ&#x20AC;? of the same object, there will be an object method for each object. Object methods can be associated with any object, not just those created with the new keyword. Instance methods apply to instances, objects created with the new keyword. Instance methods are defined in a prototype and are shared by all instances.
Instances and Prototypes
88
Extending Objects with Delegation You recall from Composition and Extension that we extended a Plain Old JavaScript Queue to create a Plain Old JavaScript Deque. But what if we have decided to use JavaScript’s prototypes and the new keyword instead of Plain Old JavaScript Objects? How do we extend a queue into a deque? Here’s our Queue: var Queue = function () { extend(this, { array: [], head: 0, tail: -1 }) }; extend(Queue.prototype, { pushTail: function (value) { return this.array[this.tail += 1] = value }, pullHead: function () { var value; if (!this.isEmpty()) { value = this.array[this.head] this.array[this.head] = void 0; this.head += 1; return value } }, isEmpty: function () { return this.tail < this.head } });
And here’s what our Deque would look like before we wire things together:
Instances and Prototypes
89
var Dequeue = function () { Queue.call(this); }; Dequeue.INCREMENT = 4; extend(Dequeue.prototype, { size: function () { return this.tail - this.head + 1 }, pullTail: function () { var value; if (!this.isEmpty()) { value = this.array[this.tail]; this.array[this.tail] = void 0; this.tail -= 1; return value } }, pushHead: function (value) { var i; if (this.head === 0) { for (i = this.tail; i >= this.head; --i) { this.array[i + INCREMENT] = this.array[i] } this.tail += this.constructor.INCREMENT; this.head += this.constructor.INCREMENT } this.array[this.head -= 1] = value } });
We obviously want to do all of a Queue’s initialization, thus we called Queue.call(this).
. So what do we want from dequeues such that we can call all of a Queue’s methods as well as a Dequeue’s? Should we copy everything from Queue.prototype into Deque.prototype, like extend(Deque.prototype, Queue.prototype)? That would work, except for one thing: If we later modified Queue, say by mixing in some new methods into its prototype, those wouldn’t be picked
Instances and Prototypes
90
up by Dequeue. No, there’s a better idea. Prototypes are objects, right? Why must they be Plain Old JavaScript Objects? Can’t a prototype be an instance? Yes they can. Imagine that Deque.prototype was a proxy for an instance of Queue. It would, of course, have all of a queue’s behaviour through Queue.prototype. We don’t want it to be an actual instance, mind you. It probably doesn’t matter with a queue, but some of the things we might work with might make things awkward if we make random instances. A database connection comes to mind, we may not want to create one just for the convenience of having access to its behaviour. Here’s such a proxy: var queueProxy = new Queue();
And here’s another: var queueProxy = Object.create(Queue.prototype);
The key here is that the prototype of a Dequeue needs to be an object that has its prototype set to Queue.prototype. That way, we can add methods to Dequeue’s prototype without modifying Queue’s prototype. Let’s double-check: Queue.prototype.isPrototypeOf(queueProxy) //=> true
Let’s insert queueProxy into our code: var Dequeue = function () { Queue.call(this) }; Dequeue.INCREMENT = 4; Dequeue.prototype = queueProxy; extend(Dequeue.prototype, { size: function () { return this.tail - this.head + 1 }, pullTail: function () { var value;
Instances and Prototypes
if (!this.isEmpty()) { value = this.array[this.tail]; this.array[this.tail] = void 0; this.tail -= 1; return value } }, pushHead: function (value) { var i; if (this.head === 0) { for (i = this.tail; i >= this.head; --i) { this.array[i + INCREMENT] = this.array[i] } this.tail += this.constructor.INCREMENT; this.head += this.constructor.INCREMENT } this.array[this.head -= 1] = value } });
And it seems to work: d = new Dequeue() d.pushTail('Hello') d.pushTail('JavaScript') d.pushTail('!') d.pullHead() //=> 'Hello' d.pullTail() //=> '!' d.pullHead() //=> 'JavaScript'
Wonderful!
extracting the boilerplate Letâ&#x20AC;&#x2122;s turn our mechanism into a function:
91
Instances and Prototypes
var child = function (parent, child) { var proxy = Object.create(parent.prototype); child.prototype = proxy; return child; }
And use it in Dequeue: var Dequeue = child(Queue, function () { Queue.call(this) }); Dequeue.INCREMENT = 4; extend(Dequeue.prototype, { size: function () { return this.tail - this.head + 1 }, pullTail: function () { var value; if (!this.isEmpty()) { value = this.array[this.tail]; this.array[this.tail] = void 0; this.tail -= 1; return value } }, pushHead: function (value) { var i; if (this.head === 0) { for (i = this.tail; i >= this.head; --i) { this.array[i + INCREMENT] = this.array[i] } this.tail += this.constructor.INCREMENT; this.head += this.constructor.INCREMENT } this.array[this.head -= 1] = value } });
92
Instances and Prototypes
93
future directions Some folks just love to build their own mechanisms. When all goes well, they become famous as framework creators and open source thought leaders. When all goes badly they create inhouse proprietary one-offs that blur the line between application and framework with abstractions everywhere. If you’re keen on learning, you can work on improving the above code to handle extending constructor properties, automatically calling the parent constructor function, and so forth. Or you can decide that doing it by hand isn’t that hard so why bother putting a thin wrapper around it? It’s up to you, while JavaScript isn’t the tersest language, it isn’t so baroque that building inheritance ontologies requires hundreds of lines of inscrutable code.
Methods
⁵⁶
⁵⁶Mmm … Vivace’s Espresso! (c) 2004 Allan Haggert, some rights reserved
Methods
In This Chapter In this chapter, weâ&#x20AC;&#x2122;ll look at methods in more detail, and explore metamethods.
.
95
96
Methods
What is a Method? In object-oriented programming, a method (or member function) is a subroutine (or procedure or function) associated with an object, and which has access to its data, its member variables. –Wikipedia⁵⁷
As an abstraction, an object is an independent entity that maintains internal state and that responds to messages by reporting its internal state and/or making changes to its internal state. In the course of handling a message, an object may send messages to other objects and receive replies from them. A “method” is an another idea that is related to, but not the same as, handling a message. A method is a function that encapsulates an object’s behaviour. Methods are invoked by a calling entity much as a function is invoked by some code. The distinction may seem subtle, but the easiest way to grasp the distinction is to focus on the word “message.” A message is an entity of its own. You can store and forward a message. You can modify it. You can copy it. You can dispatch it to multiple objects. You can put it on a “blackboard” and allow objects to decide for themselves whether they want to respond to it. Methods, on the other hand, operate “closer to the metal.” They look and behave like function calls. In JavaScript, methods are functions. To be a method, a function must be the property of an object. We’ve seen methods earlier, here’s a naïve example: var dictionary = { abstraction: "an abstract or general idea or term", encapsulate: "to place in or as if in a capsule", object: "anything that is visible or tangible and is relatively stable in form", descriptor: function () { return map(['abstraction', 'encapsulate', 'object'], function (key) { return key + ': "' + dictionary[key] + '"'; }).join('; '); } }; dictionary.descriptor() //=> 'abstraction: "an abstract or general idea or term"; encapsulate: "to place i\ n or as if in a capsule"; object: "anything that is visible or tangible and is relatively stable in for\ m"' ⁵⁷https://en.wikipedia.org/wiki/Method_
Methods
97
In this example, descriptor is a method. As we saw earlier, this code has many problems, but let’s hand-wave them for a moment. Let’s be clear about our terminology. This dictionary object has a method called descriptor. The function associated with descriptor is called the method handler. When we write dictionary.descriptor(), we’re invoking or calling the descriptor method. The object is then handling the method invocation by evaluating the function. In describing objects, we refer to objects as encapsulating their internal state. The ideal is that objects never directly access or manipulate each other’s state. Instead, objects interact with each other solely through methods. There are many things that methods can do. Two of the most obvious are to query an object’s internal state and to update its state. Methods that have no purpose other than to report internal state are called queries, while methods that have no purpose other than to update an object’s internal state are called updates.
Methods
98
The Letter and the Spirit of the Law The ‘law’ that objects must only interact with each other through methods is generally accepted as an ideal, even though languages like JavaScript and Java do not enforce it. That being said, there are (roughly) two philosophies about the design of objects and their methods. • Literalists believe that the important thing is the means of interaction be methods. Literalists are noted for using a profusion of getters and setters, which leads to code that is semantically identical to code where objects interact with each other’s internal state, but every interaction is performed indirectly through a query or update. • Semanticists believe that the important thing is that objects provide abstractions over their internal state. They avoid getters and setters, preferring to provide methods that are a level of abstraction above their internal representations. By way of example, let’s imagine that we have a person object. Our first cut at it involves storing a name. Here’s a first cut at a literalist implementation: var person = Object.create(null, { _firstName: { enumerable: false, writable: true }, _lastName: { enumerable: false, writable: true }, getFirstName: { enumerable: false, writable: true, value: function () { return _firstName; } }, setFirstName: { enumerable: false, writable: true, value: function (str) { return _firstName = str; } }, getLastName: {
Methods
99
enumerable: false, writable: true, value: function () { return _lastName; } }, setLastName: { enumerable: false, writable: true, value: function (str) { return _lastName = str; } } });
This is largely pointless as it stands. Other objects now write person.setFirstName('Bilbo') instead of person.firstName = 'Bilbo', but nothing of importance has been improved. The trouble with this approach as it stands is that it is a holdover form earlier times. Much of the trouble stems from design decisions made in languages like C++ and Java to preserve C-like semantics. In those languages, once you have some code written as person.firstName = 'Bilbo', you are forever stuck with person exposing a property to direct access. Without changing the semantics, you may later want to do something like make person observable⁵⁸, that is, we add code such that other objects can be notified when the person object’s name is updated. If firstName is a property being directly updated by other entities, we have no way to insert any code to handle the updating. The same argument goes for something like validating the name. Although validating names is a morass in the real world, we might have simple ideas such as that the first name will either have at least one character or be null, but never an empty string. If other entities directly update the property, we can’t enforce this within our object. In days of old, programmers would have needed to go through the code base changing person.firstName = 'Bilbo' into person.setName('Bilbo') (or even worse, adding all the observable code and validation code to other entities). Thus, the literalist tradition grew of defining getters and setters as methods even if no additional functionality was needed immediately. With the code above, it is straightforward to introduce validation and observability:⁵⁹
⁵⁸https://en.wikipedia.org/wiki/Observer_pattern ⁵⁹Later on, we’ll see how to use method combinators to do this more elegantly.
Methods
100
var person = Object.create(null, { // ... setFirstName: { enumerable: false, writable: true, value: function (str) { // insert validation and observable boilerplate here return _firstName = str; } }, setLastName: { enumerable: false, writable: true, value: function (str) { // insert validation and observable boilerplate here return _lastName = str; } } });
That seems very nice, but balanced against this is that contemporary implementations of JavaScript allow you to write getters and setters for properties that mediate access even when other entities are using property access syntax like person.lastName = 'Baggins': var person = Object.create(null, { _firstName: { enumerable: false, writable: true }, firstName: { get: function () { return _firstName; }, set: function (str) { // insert validation and observable boilerplate here return _firstName = str; } }, _lastName: { enumerable: false, writable: true },
Methods
101
lastName: { get: function () { return _lastName; }, set: function (str) { // insert validation and observable boilerplate here return _lastName = str; } } });
The preponderance of evidence suggests that if you are a literalist, you are better off not bothering with making getters and setters for everything in JavaScript, as you can add them later if need be.
the semantic interpretation of object methods⁶⁰ What about the semantic approach? With the literalist, properties like firstName are decoupled from methods like setFirstName so that the implementation of the properties can be managed by the object. Other objects calling person.setFirstName('Frodo') are insulated from details such as whether other objects are to be notified when person is changed. But while the implementation is hidden, there is no abstraction involved. The level of abstraction of the properties is identical to the level of abstraction of the methods. The semanticist takes this one step further. To the semanticist, objects insulate other entities from implementation details like observables and validation, but objects also provide an abstraction to other entities. In our person example, first and last name is a very low-level concern, the kind of thing you think about when you’re putting things in a database and worrying about searching and sorting performance. But what would be a higher-level abstraction? Just a name. You ask someone their name, they tell you. You ask for a name, you get it. An object that takes and accepts names hides from us all the icky questions like: 1. How do we handle people who only have one name? (it’s not just celebrities) 2. Where do we store the extra middle names like Tracy Christopher Anthony Lee? 3. How do we handle formal Spanish names like Gabriel José de la Concordia García Márquez⁶¹? ⁶⁰With the greatest respect to Chongo, author of “The Homeless Interpretation of Quantum Mechanics.” ⁶¹https://en.wikipedia.org/wiki/Gabriel_Garc%C3%ADa_Márquez
Methods
102
4. What do we do with maiden names⁶² like Arlene Gwendolyn Lee née Barzey or Leocadia Blanco Álvarez de Pérez? If we expose the low-level fields to other code, we demand that they know all about our object’s internals and do the parsing where required. It may be simpler and easier to simply expose: var person = Object.create(null, { _givenNames: { enumerable: false, writable: true, value: [] }, _maternalSurname: { enumerable: false, writable: true }, _paternalSurname: { enumerable: false, writable: true }, _premaritalName: { enumerable: false, writable: true }, name: { get: function () { // ... }, set: function (str) { // ... } } });
The person object can then do the “icky” work itself. This centralizes responsibility for names. Now, honestly, people have been handling names in a very US-centric way for a very long time, and few will put up a fuss if you make objects with highly literal name implementations. But the example illustrates the divide between a literal design where other objects operate at the same level of abstraction as the object’s internals, and a semantic design, one that operates at a higher level of abstraction and is responsible for translating methods into queries and updates on the implementation. ⁶²https://en.wikipedia.org/wiki/Married_and_maiden_names
103
Methods
Composite Methods One of the primary activities in programming is to factor programs or algorithms, to break them into smaller parts that can be reused or recombined in different ways. Common industry practice is to use the words “decompose” and “factor” interchangeably to refer to any breaking of code into smaller parts. Nevertheless, we will defy industry practice and use the word “decompose” to refer to breaking code into smaller parts whether those parts are to be recombined or reused or not, and use the word “factor” to refer to the stricter case of decomposition where the intention is to recombine or reuse the parts in different ways.
Both methods and objects can and should be factored into reusable components that have a single, well-defined responsibility⁶³.⁶⁴ The simplest way to decompose a method is to “extract” one or more helper methods. For example: var person = Object.create(null, { // ... setFirstName: { enumerable: false, writable: true, value: function (str) { if (typeof(str) === 'string' && str != '') { return this._firstName = str; } } }, setLastName: { enumerable: false, writable: true, value: function (str) { if (typeof(str) === 'string' && str != '') { return this._lastName = str; } } ⁶³https://en.wikipedia.org/wiki/Single_responsibility_principle ⁶⁴Robert Martin’s rule of thumb for determining whether a method has a single responsibility is to ask when and why it would ever change. If
there is just one reason why you are likely to change a method, it has a single responsibility. If there is more than one reason why it might change, it should be decomposed into separate entities that each have a single responsibility.
Methods
104
} });
The methods setFirstName and setLastName both have a “guard clause” that will not update the object’s hidden state unless the method is passed a non-empty string. The logic can be extracted into its own “helper method:” var person = Object.create(null, { // ... isaValidName: { enumerable: false, writable: false, value: function (str) { return (typeof(str) === 'string' && str != ''); } }, setFirstName: { enumerable: false, writable: true, value: function (str) { if (this.isaValidName(str)) { return this._firstName = str; } } }, setLastName: { enumerable: false, writable: true, value: function (str) { if (this.isaValidName(str)) { return this._lastName = str; } } } });
The methods setFirstName and setLastName now call the helper method isaValidName. The usual motivation for this is known as DRY⁶⁵ or “Don’t Repeat Yourself.” The DRY principle is stated as ⁶⁵https://en.wikipedia.org/wiki/Don’t_repeat_yourself
Methods
105
“Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.” In this case, presumably there is one idea, “person names must be non-empty strings,” and placing the implementation for this in the isaValidString helper method ensures that now there is just the one authoritative source for the logic, instead of one in each name setter method. Decomposing a method needn’t always be for the purpose of DRYing up the logic. Sometimes, a method breaks down logically into a hierarchy of steps. For example: var person = Object.create(null, { // ... doSomethingComplicated: { enumerable: false, writable: true, value: function () { this.setUp(); this.doTheWork(); this.breakdown(); this.cleanUp(); } }, setUp: // ... doTheWork: // ... breakdown: // ... cleanUp: // ... });
This is as true of methods as it is of functions in general. However, objects have some extra considerations. The most conspicuous is that an object is its own namespace. When you break a method down into helpers, you are adding items to the namespace, making the object as a whole more difficult to understand. What methods call setUp? Can breakdown be called independently of cleanUp? Everything is thrown into an object higgledy-piggledy.
decluttering with closures JavaScript provides us with tools for reducing object clutter. The first is the Immediately Invoked Function Expression⁶⁶ (“IIFE”). If our four helpers exist only to decompose doSomethingComplicated, we can write: ⁶⁶https://en.wikipedia.org/wiki/Immediately-invoked_function_expression
Methods
106
var person = Object.create(null, { // ... doSomethingComplicated: { enumerable: false, writable: true, value: (function () { return function () { setUp.call(this); doTheWork.call(this); breakdown.call(this); cleanUp.call(this); }; function setUp () { // ... } function doTheWork () { // ... } function breakdown () { // ... } function cleanUp () { // ... } })() }, });
Now our four helpers exist only within the closure created by the IIFE, and thus it is impossible for any other method to call them. You could even create a setUp helper with a similar name for another function without clashing with this one. Note that we’re not invoking these functions with this., because they aren’t methods any more. And to preserve the local object’s context, we’re calling them with .call(this).
decluttering with method objects In JavaScript, methods are represented by functions. And in JavaScript, functions are objects. Functions have properties, and the properties behave just like objects we create with {} or Object.create:
Methods
107
var K = function (x) { return function (y) { return x; }; }; Object.defineProperty(K, 'longName', { enumerable: true, writable: false, value: 'The K Combinator' }); K.longName //=> 'The K Combinator' Object.keys(K) //=> [ 'longName' ]
We can take advantage of this by using a function as a container for its own helper functions. There are several easy patterns for this. Of course, you could write it all out by hand: function doSomethingComplicated () { doSomethingComplicated.setUp.call(this); doSomethingComplicated.doTheWork.call(this); doSomethingComplicated.breakdown.call(this); doSomethingComplicated.cleanUp.call(this); } doSomethingComplicated.setUp = function () { // ... } doSomethingComplicated.doTheWork = function () { // ... } doSomethingComplicated.breakdown = function () { // ... } doSomethingComplicated.cleanUp = function () { // ...
108
Methods
} var person = Object.create(null, { // ... doSomethingComplicated: { enumerable: false, writable: true, value: doSomethingComplicated } });
If weâ&#x20AC;&#x2122;d like to make it neat and tidy inline, tap is handy: var person = Object.create(null, { // ... doSomethingComplicated: { enumerable: false, writable: true, value: tap( function doSomethingComplicated () { doSomethingComplicated.setUp.call(this); doSomethingComplicated.doTheWork.call(this); doSomethingComplicated.breakdown.call(this); doSomethingComplicated.cleanUp.call(this); }, function (its) { its.setUp = function () { // ... } its.doTheWork = function () { // ... } its.breakdown = function () { // ... } its.cleanUp = function () {
109
Methods
// ... } }) } });
In terms of code, this is no simpler than the IIFE solution. However, placing the helper methods inside the function itself does make them available for use or modification by other methods. For example, you can now use a method decorator on any of the helpers: var logsTheReciver = after( function (value) { console.log(this); return value; }); person.doSomethingComplicated.doTheWork = logsTheReciver(person.doSomethingCompli\ cated.doTheWork);
This would not have been possible if doTheWork was hidden inside a closure.
summary Like â&#x20AC;&#x153;ordinaryâ&#x20AC;? functions, methods can benefit from being decomposed or factored into smaller functions. Two of the motivations for doing so are to DRY up the code and to break a method into more easily understood and obvious parts. The parts can be represented as helper methods, functions hidden in a closure, or properties of the method itself.
Methods
110
Method Objects function helpers If functions are objects, and functions can have properties, then functions can have methods. We can give functions our own methods by assigning functions to their properties. We saw this previously when we decomposed an object’s method into helper methods. Here’s the same applied to a function: function factorial (n) { return factorial.helper(n, 1); } factorial.helper = function helper (n, accumulator) { if (n === 0) { return accumulator; } else return helper(n - 1, n * accumulator); }
Functions can have all sorts of properties. One of the more intriguing possibility is to maintain an array of functions: function sequencer (arg) { var that = this; return sequencer._functions.reduce( function (acc, fn) { return fn.call(that, acc); }, arg); } Object.defineProperties(sequencer, { _functions: { enumerable: false, writable: false, value: [] } }); sequencer is an object-oriented way to implement the pipeline function from function-oriented
libraries like allong.es⁶⁷. Instead of writing something like: ⁶⁷http://allong.es
Methods
111
function square (n) { return n * n; } function increment (n) { return n + 1; } pipeline(square, increment)(6) //=> 37
We can write: sequencer._functions.push(square); sequencer._functions.push(increment); sequencer(6) //=> 37
The functions contained within the _functions array are helper methods. They work they same way as is we’d written something like: function squarePlusOne (arg) { return squarePlusOne.increment( squarePlusOne.square(arg) ); } squarePlusOne.increment = increment; squarePlusOne.square = square; squarePlusOne(6) //=> 37
The only difference is that they’re dynamically looked up helper methods instead of statically wired in place by the body of the function. But they’re still helper methods.
function methods The obvious problem with our approach is that we aren’t using our method as an object in the ideal sense. Why should other entities manipulate its internal state? If this were any other kind of object, we’d expose methods handle messages from other entities. Let’s try it:
Methods
112
Object.defineProperties(sequencer, { push: { enumerable: false, writable: false, value: function (fn) { return this._functions.push(fn); } } });
Now we can manipulate our sequencer without touching its privates: sequencer.push(square); sequencer.push(increment); sequencer(6) //=> 37
Is it a big deal to eliminate the _functions reference? Yes! 1. This hides the implementation: Do we have an array of functions? Or maybe we are composing the functions with a combinator? Who knows? 2. This prevents us manipulating internal state in unauthorized ways, such as calling sequencer._functions.reverse() sequencer.push is a proper method, a function that handles messages from other entities and in the
course of handling the message, queries and/or updates the function’s internal state. If we’re going to treat functions like objects, we ought to give them methods.
aspect-oriented programming and meta-methods When an object has a function as one of its properties, that’s a method. And we just established that functions can have methods. So… Can a method have methods? Most assuredly. Here’s an example that implements a simplified form of Aspect-Oriented Programming⁶⁸ Consider a businessObject and a businessObjectCollection. Never mind what they are, that isn’t important. We start with the idea that a businessObject can be valid or invalid, and there’s a method for querying this: ⁶⁸https://en.wikipedia.org/wiki/Aspect-oriented_programming
Methods
113
var businessObject = Object.create(null, { // ... isValid: { enumerable: false, value: function () { // ... } } });
Obviously, the businessCollection has methods for adding business objects and for finding business objects: var businessCollection = Object.create(null, { // ... add: { enumerable: false, value: function (bobj) { // ... } }, find: { enumerable: false, value: function (fn) { // ... } } });
Our businessCollection is just a collection, it doesn’t actually do anything to the business objects it holds. One day, we decide that it is an error if an invalid business object is placed in a business collection, and also an error if an invalid business object is returned when you call businessCollection.find. To “fail fast” in these situations, we decide that an exception should be thrown when this happens. Should we add some checking code to businessCollection.add and businessCollection.find? Yes, but we shouldn’t modify the methods themselves. Since businessCollection’s single responsibility is storing objects, business rules about the state of the objects being stored should be placed in some other code. What we’d like to do is “advise” the add method with code that is to be run before it runs, and advise the find method with code that runs after it runs. If our methods had methods, we could write:
Methods
114
businessCollection.add.beforeAdvice(function (bobjProvided) { if (!bobjProvided.isValid()) throw 'bad object provided to add'; }); businessCollection.find.afterAdvice(function (bobjReturned) { if (bobjReturned && !bobjReturned.isValid()) throw 'bad object returned from find'; });
As you can see, we’ve invented a little protocol. .beforeAdvice adds a function “before” a method, and .afterAdvice adds a function “after” a method. We’ll need a function to make method objects out of the desired method functions: function advisable (methodBody) { function theMethod () { var args = [].slice.call(arguments); theMethod.befores.forEach( function (advice) { advice.apply(this, args); }, this); var returnValue = theMethod.body.apply(this, arguments); theMethod.afters.forEach( function (advice) { advice.call(this, returnValue); }, this); return returnValue; } Object.defineProperties(theMethod, { befores: { enumerable: true, writable: false, value: [] }, body: { enumerable: true, writable: false, value: body
Methods
}, afters: { enumerable: true, writable: false, value: [] }, beforeAdvice: { enumerable: false, writable: false, value: function (fn) { this.befores.unshift(fn); return this; } }, afterAdvice: { enumerable: false, writable: false, value: function (fn) { this.afters.push(fn); return this; } } }); return theMethod; }
Letâ&#x20AC;&#x2122;s rewrite our businessCollection to use our advisable function: var businessCollection = Object.create(null, { // ... add: { enumerable: false, value: advisable(function (bobj) { // ... }) }, find: { enumerable: false, value: advisable(function (fn) { // ... })
115
Methods
116
} });
And now, exactly as above, we can write businessCollection.add.beforeAdvice and businessCollection.find.a separating the responsibility for error checking from the responsibility for managing a collection of business objects.
Methods
117
Predicate Dispatch Pattern matching is a feature found (with considerable rigor) in functional languages like Erlang and Haskell. In mathematics, algorithms and problems can often be solved by breaking them down into simple cases. Sometimes, those cases are reductions of the original problem. A famous example is the naïve expression of the factorial⁶⁹ function. The factorial of a non-negative integer n is the product of all positive integers less than or equal to n. For example, factorial(5) is equal to 5 * 4 * 3 * 2 * 1, or 120. The algorithm to compute it can be expressed as two cases. Let’s pretend that JavaScript has predicate-dispatch baked in: function factorial (1) { return 1; } function factorial (n > 1) { return n * factorial(n - 1); }
This can be done with an if statement, of course, but the benefit of breaking problems down by cases is that once again, we are finding a way to combine small pieces of code in a way that does not tightly couple them. We can fake this with a simple form of predicate dispatch, and we’ll see later that it will be very useful for implementing multiple dispatch.
prelude: return values Let’s start with a convention: Methods and functions must return something if they successfully hand a method invocation, or raise an exception if they catastrophically fail. They cannot return undefined (which in JavaScript, also includes not explicitly returning something). For example:
⁶⁹https://en.wikipedia.org/wiki/Factorial
Methods
118
// returns a value, so it is successful function sum (a, b) { return a + b; } // returns this, so it is successful function fluent (x, y, z) { // do something return this; } // returns undefined, so it is not successful function fail () { return undefined; } // decorates a function by making it into a fail function dont (fn) { return fail; } // logs something and fails, // because it doesn't explicitly return anything function logToConsole () { console.log.apply(null, arguments); }
guarded functions We can write ourself a simple method decorator that guards a method, and fails if the guard function fails on the arguments provided. Itâ&#x20AC;&#x2122;s self-currying to facilitate writing utility guards: function nameAndLength(name, length, body) { var abcs = [ 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'z', 'x', 'c', 'v', 'b', 'n', 'm' ], pars = abcs.slice(0, length), src = "(function " + name + " (" + pars.join(',') + ") { return body.apply\ (this, arguments); })"; return eval(src); }
Methods
119
function imitate(exemplar, body) { return nameAndLength(exemplar.name, exemplar.length, body); } function when (guardFn, optionalFn) { function guarded (fn) { return imitate(fn, function () { if (guardFn.apply(this, arguments)) return fn.apply(this, arguments); }); } return optionalFn == null ? guarded : guarded(optionalFn); } when(function (x) {return x != null; })(function () { return "hello world"; })(); //=> undefined when(function (x) {return x != null; })(function () { return "hello world"; })(1); //=> 'hello world' when is useful independently of our work here, and thatâ&#x20AC;&#x2122;s a good thing: Whenever possible, we donâ&#x20AC;&#x2122;t
just make complicated things out of simpler things, we make them out of reusable simpler things. Now we can compose our guarded functions. Match takes a list of methods, and apply them in order, stopping when one of the methods returns a value that is not undefined. function getWith (prop, obj) { function gets (obj) { return obj[prop]; } return obj === undefined ? gets : gets(obj); } function mapWith (fn, mappable) { function maps (collection) { return collection.map(fn); }
Methods
120
return mappable === undefined ? maps : maps(collection); } function pluckWith (prop, collection) { var plucker = mapWith(getWith(prop)); return collection === undefined ? plucker : plucker(collection); } function Match () { var fns = [].slice.call(arguments, 0), lengths = pluckWith('length', fns), length = Math.min.apply(null, lengths), names = pluckWith('name', fns).filter(function (name) { return name !== '\ '; }), name = names.length === 0 ? '' : names[0]; return nameAndLength(name, length, function () { var i, value; for (i in fns) { value = fns[i].apply(this, arguments); if (value !== undefined) return value; } }); } function equals (x) { return function eq (y) { return (x === y); }; } function not (fn) { var name = fn.name === ''
121
Methods
? "not" : "not_" + fn.name; return nameAndLength(name, fn.length, function () { return !fn.apply(this, arguments) }); } var worstPossibleTestForEven = Match( when(equals(0), function (n) { return true; }), when(equals(1), function (n) { return false; }), function (n) { return worstPossibleTestForOdd(n - 1)} ) var worstPossibleTestForOdd = Match( when(equals(0), function (n) { return false; }), when(equals(1), function (n) { return true; }), function (n) { return worstPossibleTestForEven(n - 1)} ) worstPossibleTestForEven(6) //=> true worstPossibleTestForOdd(42) //=> false
This is called predicate dispatch⁷⁰, we’re dispatching a function call to another function based on a series of predicates we apply to the arguments. Predicate dispatch declutters individual cases and composes functions and methods from smaller, simpler components that are decoupled from each other. ⁷⁰https://en.wikipedia.org/wiki/Predicate_dispatch
Metaobjects
⁷¹
⁷¹Krups Machines (c) 2010 Shadow Becomes White, some rights reserved
Metaobjects
In This Chapter In previous chapters, we’ve gone from discussing basic data structures to “classic” JavaScript objects that delegate behaviour to prototypes. In this chapter, we’ll back up to the beginning, developing an approach to metaobjects from first principles, working our way up from extending objects to using JavaScript’s prototypes. We’ll explore the semantics of templating behaviour, forwarding, and delegation.
.
123
124
Metaobjects
Why Metaobjects? In computer science, a metaobject is an object that manipulates, creates, describes, or implements other objects (including itself). The object that the metaobject is about is called the base object. Some information that a metaobject might store is the base object’s type, interface, class, methods, attributes, parse tree, etc. –Wikipedia⁷²
It is technically possible to write software using objects alone. When we need behaviour for an object, we can give it methods by binding functions to keys in the object: 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; } }
We call this a “naïve” object. It has state and behaviour, but it lacks division of responsibility between its state and its behaviour. This lack of separation has two drawbacks. First, it intermingles properties that are part of the model domain (such as firstName), with methods (and possibly other properties, although none are shown here) that are part of the implementation domain. Second, when we needed to share common behaviour, we could have objects share common functions, but does it not scale: There’s no sense of organization, no clustering of objects and functions that share a common responsibility. Metaobjects solve the lack-of-separation problem by separating the domain-specific properties of objects from their behaviour and implementation-specific properties. The basic principle of the metaobject is that we separate the mechanics of behaviour from the domain properties of the base object. This has immediate engineering benefits, and it’s also the foundation for designing programs with formal classes, expectations, and delegation. There are many ways to implement metaobjects, and we will explore some of these as a mechanism for exploring some of the ideas in Object-Oriented Programming. ⁷²https://en.wikipedia.org/wiki/Metaobject
Metaobjects
Mixins, Forwarding, and Delegation The simplest possible metaobject in JavaScript is a mixin. Consider our naĂŻve object: 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; } }
We can separate its domain properties from its behaviour: 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; } };
And use extend to mix the behaviour in:
125
Metaobjects
126
var __slice = [].slice; 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; }; extend(sam, Person); sam.rename //=> [Function]
This allows us to separate the behaviour from the properties in our code. Our Person object is a mixin, it provides functionality to be mixed into an object with a function like extend. Using mixins does not require copying entire functions around, each object gets references to the functions in the mixin. If we want to use the same behaviour with another object, we can do that: // ... extend(sam, Person); var peck = { firstName: 'Sam', lastName: 'Peckinpah' }; extend(peck, Person);
Metaobjects
127
Thus, many objects can mix one object in. Things get even better: One object can mix many objects in: var HasCareer = { career: function () { return this.chosenCareer; }, setCareer: function (career) { this.chosenCareer = career; return this; } }; extend(peck, Person); extend(peck, HasCareer); peck.setCareer('Director');
Since many objects can all mix the same object in, and since one object can mix many objects into itself, there is a many-to-many relationship between objects and mixins.
scope and coupling Consider a design that has four kinds of mixins, Person, HasCareer, IsAuthor, and HasChildren. When you make a change to and one mixin, say IsAuthor, you obviously have to consider how that change will affect every object that mixes IsAuthor in. By itself this is not completely revelatory: When objects interact with each other in the code, there are going to be dependencies between them, and you have to manage those dependencies. But what you may not consider is that if any of the objects that mix IsAuthor in also mix in HasChildren, we have to think about whether IsAuthor conflicts with HasChildren in some way. For example, what if we have IsAuthor count the number of books with this.number? Isn’t it possible that HasChildren may already be using .number to count how many children a parent may have? Normally, the scope of interaction between objects is limited because objects are designed to encapsulate their private state: If object a invokes a method x() on object b, we know that the scope of interaction between a and b is strictly limited to the method x(). We also know that any change in state it may create is strictly limited to the object b, because x() cannot reach back and touch a’s private state. However, two methods numberOfBooksWritten() and numberOfChildren() on the same object are tightly coupled by default, because they both interact with all of the object’s private state. And thus,
Metaobjects
128
any two mixins that are both mixed into the same object are similarly tightly coupled, because they each have full access to all of an object’s private properties. The technical term for mixins referring to an object’s private properties is open recursion⁷³. It is powerful and flexible, in exactly the same sense that having objects refer to each other’s internal properties is powerful and flexible. And just as objects can encapsulate their own private state, so can mixins.
mixins with private properties Let’s revisit our HasCareer mixin: var HasCareer = { career: function () { return this.chosenCareer; }, setCareer: function (career) { this.chosenCareer = career; return this; } }; HasCareer stores its private state in the object’s chosenCareer property. As we’ve seen, that introduces coupling if any other method touches chosenCareer. What we’d like to do is make chosenCareer private. Specifically:
1. We wish to store a copy of chosenCareer for each object that uses the HasCareer mixin. Mark Twain is a writer, Sam Peckinpah is a director. 2. chosenCareer must not be a property of each Person object, because we don’t want other methods accessing it and becoming coupled. We have a few options. The very simplest, and most “native” to JavaScript, is to use a closure.
privacy through closures We’ll write our own functional mixin⁷⁴:
⁷³https://en.wikipedia.org/wiki/Open_recursion#Open_recursion ⁷⁴https://javascriptweblog.wordpress.com/2011/05/31/a-fresh-look-at-javascript-mixins/
Metaobjects
129
function HasPrivateCareer (obj) { var chosenCareer; obj.career = function () { return chosenCareer; }; obj.setCareer = function (career) { chosenCareer = career; return this; }; return obj; } HasPrivateCareer(peck); chosenCareer is a variable within the scope of the HasCareer, so the career and setCareer methods
can both access it through lexical scope, but no other method can or ever will. This approach works well for simple cases. It only works for named variables. We can’t, for example, write a function that iterates through all of the private properties of this kind of functional mixin, because they aren’t properties, they’re variables. In the end, we have privacy, but we achieve it by not using properties at all.
privacy through objects Another way to achieve privacy in mixins is to write them as methods that operate on this, but sneakily make this refer to a different object. Let’s revisit our extend function: 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; };
We don’t need to embed variables and methods in our function, it creates one private variable (privateProperty), and then uses .bind to ensure that each method is bound to that variable instead of to the receiver object being extended with the mixin. Now we can extend any object with any mixin, ‘privately:’
Metaobjects
130
extendPrivately(twain, HasCareer); twain.setCareer('Author'); twain.career() //=> 'Author'
Has it modified twain’s properties? twain.chosenCareer //=> undefined
No. twain has .setCareer and .career methods, but .chosencareer is a property of an object created when twain was privately extended, then bound to each method using .bind⁷⁵. The advantage of this approach over closures is that the mixin and the mechanism for mixing it in are separate: You just write the mixin’s methods, you don’t have to carefully ensure that they access private state through variables in a closure.
another way to achieve privacy through objects In our scheme above, we used .bind to create methods bound to a private object before mixing references to them into our object. There is another way to do it: 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; };
This function forwards methods to another object. Any other object, it could be a metaobject specifically designed to define behaviour, or it could be a domain object that has other responsibilities. Dispensing with a lot of mixins, here is a very simple example example. We start with some kind of investment portfolio object that has a netWorth method: ⁷⁵https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
Metaobjects
131
var portfolio = { _investments: [], addInvestment: function (investment) { this._investments.push(investment); return this; }, netWorth: function () { return this._investments.reduce( function (acc, investment) { return acc + investment.value; }, 0 ); } };
And next we create an investor who has this portfolio of investments: var investor = { //... }
What if we want to make investments and to know an investor’s net worth? forward(investor, portfolio);
We’re saying “Forward all requests for addInvestment and netWorth to the portfolio object.”
forwarding Forwarding is a relationship between an object that receives a method invocation receiver and a provider object. They may be peers. The provider may be contained by the consumer. Or perhaps the provider is a metaobject. When forwarding, the provider object has its own state. There is no special binding of function contexts, instead the consumer object has its own methods that forward to the provider and return the result. Our forward function above handles all of that, iterating over the provider’s properties and making forwarding methods in the consumer. The key idea is that when forwarding, the provider object handles each method in its own context. This is very similar to the effect of our solution with .bind above, but not identical. Because there is a forwarding method in the consumer object and a handling method in the provider, the two can be varied independently. Here’s a snippet of our forward function from above:
Metaobjects
132
consumer[methodName] = function () { return metaobject[methodName].apply(metaobject, arguments); }
Each forwarding function invokes the method in the provider by name. So we can do this: portfolio.netWorth = function () { return "I'm actually bankrupt!"; }
We’re overwriting the method in the portfolio object, but not the forwarding function. So now, our investor object will forward invocations of netWorth to the new function, not the original. This is not how our .bind system worked above. That makes sense from a “metaphor” perspective. With our extendPrivately function above, we are creating an object as a way of making private state, but we don’t think of it as really being a first-class entity unto itself. We’re mixing those specific methods into a consumer. Another way to say this is that mixing in is “early bound,” while forwarding is “late bound:” We’ll look up the method when it’s invoked.
shared forwarding The premise of a private mixin is that every time you mix the metaobject’s behaviour into an object, you create a new, private object to hold the private state for the behaviour being mixed in. Thus, you can privately mix the same metaobject into many objects, and they each will have their own private state. When A and B both privately mix C in, objects A’ and B’ are created to hold private state for C, and thus A and B do not share state. Forwarding does not work this way. When objects A and B both forward to C, the private state for C is held in C, and thus A and B share state. Sometimes this is what we want. but if it isn’t, we must be very careful about using forwarding.
summarizing what we know so far So now we have three things: Mixing in a mixin; mixing in a mixin with private state for its methods (“Private Mixin”); and forwarding to a first-class object. And we’ve talked all around two questions: 1. Is the mixed-in method being early-bound? Or late-bound? 2. When a method is invoked on a receiving object, is it evaluated in the receiver’s context? Or in the metaobject’s state’s context? If we make a little table, each of those three things gets its own spot:
133
Metaobjects
Receiver’s context Metaobject’s context
Early-bound
Late-bound
Mixin Private Mixin
Forwarding
So… What goes in the missing spot? What is late-bound, but evaluated in the receiver’s context?
delegation Let’s build it. Here’s our forward function, modified to evaluate method invocation in the receiver’s context: 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; };
This new delegate function does exactly the same thing as the forward function, but the function that does the delegation looks like this: function () { return metaobject[methodName].apply(receiver, arguments); }
It uses the receiver as the context instead of the provider. This has all the same coupling implications that our mixins have, of course. And it layers in additional indirection. But unlike a mixin and like forwarding, the indirection gives us some late binding, allowing us to modify the metaobject’s methods after we have delegated behaviour from a receiver to it.
Metaobjects
134
method proxies With forwarding, we mix methods into the receiver that forward invocations to the metaobject. With delegation, we mix methods into the receiver that delegate invocations to the metaobject. These are called method proxies, because they are proxies for the methods that belong to the metaobject. Mixins and private mixins use references to the metaobject’s methods. Forwarding and delegation use proxies for the metaobject’s methods.
delegation vs. forwarding Delegation and forwarding are both very similar. One metaphor that might help distinguish them is to think of receiving an email asking you to donate some money to a worthy charity. • If you forward the email to a friend, and the friend donates money, the friend is donating their own money and getting their own tax receipt. • If you delegate responding to your accountant, the accountant donates your money to the charity and you receive the tax receipt. In both cases, the other entity does the work when you receive the email.
Metaobjects
135
Later Binding When comparing Mixins to Delegation (and comparing Private Mixins to Forwarding), we noted that the primary difference is that Mixins are early bound and Delegation is late bound. Let’s be specific. Given: var counter = {}; var Incrementor = { increment: function () { ++this._value; return this; }, value: function (optionalValue) { if (optionalValue != null) { this._value = optionalValue; } return this._value; } }; extend(counter, Incrementor);
We are mixing Incrementor into counter. At some point later, we encounter: counter.value(42);
What function handles the invocation of .value? because we mixed Incrementor into counter, it’s the same function as Incrementor.counter. We don’t look that up when counter.value(42) is evaluated, because that was bound to counter.value when we extended counter. This is early binding. However, given:
Metaobjects
136
var counter = {}; delegate(counter, Incrementor); // ...time passes... counter.value(42);
We again are most likely invoking Incrementor.value, but now we are determining this at the time counter.value(42) is evaluated. We bound the target of the delegation, Incrementor, to counter, but we are going to look the actual property of Incrementor.value up when it is invoked. This is late binding, and it is useful in that we can make some changes to Incrementor after the delegation has been set up, perhaps to add some logging. It is very nice not to have to do things like this in a very specific order: When things have to be done in a specific order, they are coupled in time. Late binding is a decoupling technique.
but wait, there’s more But we can get even later than that. Although the specific function is late bound, the target of the delegation, Incrementor, is early bound. We can late bind that too! Here’s a variation on delegate: function delegateToOwn (receiver, propertyName, methods) { var temporaryMetaobject; if (methods == null) { temporaryMetaobject = receiver[propertyName]; methods = Object.keys(temporaryMetaobject).filter(function (methodName) { return typeof(temporaryMetaobject[methodName]) == 'function'; }); } methods.forEach(function (methodName) { receiver[methodName] = function () { var metaobject = receiver[propertyName]; return metaobject[methodName].apply(receiver, arguments); }; }); return receiver; };
This function sets things up so that an object can delegate to one of its own properties. Let’s take another look at the investor example. First, we’ll set up our portfolio to separate behaviour from properties with a standard mixin:
Metaobjects
137
var HasInvestments = { addInvestment: function (investment) { this._investments.push(investment); return this; }, netWorth: function () { return this._investments.reduce( function (acc, investment) { return acc + investment.value; }, 0 ); } }; var portfolio = extend({_investments: []}, HasInvestments);
Next we’ll make that a property of our investor, and delegate to the property, not the object itself: var investor = { // ... nestEgg: portfolio } delegateToOwn(investor, 'nestEgg');
Our investor object delegates the addInvestment and netWorth methods to its own nestEgg property. So far, this is just like the delegate method above. But consider what happens if we decide to assign a new portfolio to our investor: var retirementPortfolio = { _investments: [ {name: 'IRA fund', worth: '872,000'} ] } investor.nestEgg = retirementPortfolio;
The delegateToOwn delegation now delegates to the new portfolio, because it is bound to the property name, not to the original object. This seems questionable for portfolios–what happens to the old portfolio when you assign a new one?–but has tremendous application for modeling classes of behaviour that change dynamically.
Metaobjects
138
state machines A very common use case for this delegation is when building finite state machines⁷⁶. As described in the book Understanding the Four Rules of Simple Design⁷⁷ by Corey Haines, you could implement Conway’s Game of Life⁷⁸ using if statements. Hand waving furiously over other parts of the system, you might get: var Universe = { // ... numberOfNeighbours: function (location) { // ... } }; var thisGame = extend({}, Universe); var Cell = { alive: function () { return this._alive; }, numberOfNeighbours: function () { return thisGame.numberOfNeighbours(this._location); }, aliveInNextGeneration: function () { if (this.alive()) { return (this.numberOfNeighbours() === 3); } else { return (this.numberOfNeighbours() === 2 || this.numberOfNeighbours() === 3); } } }; var someCell = extend({ _alive: true, _location: {x: -15, y: 12} }, Cell); ⁷⁶https://en.wikipedia.org/wiki/Finite-state_machine ⁷⁷https://leanpub.com/4rulesofsimpledesign ⁷⁸https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life
Metaobjects
139
One of the many insights from Understanding the Four Rules of Simple Design⁷⁹ is that this business of having an if (alive()) in the middle of a method is a hint that cells are stateful. We can extract this into a state machine using delegation to a property: var Alive = { alive: function () { return true; }, aliveInNextGeneration: function () { return (this.numberOfNeighbours() === 3); } }; var Dead = { alive: function () { return false; }, aliveInNextGeneration: function () { return (this.numberOfNeighbours() === 2 || this.numberOfNeighbours() === 3); } }; var FsmCell = { numberOfNeighbours: function () { return thisGame.numberOfNeighbours(this._location); } } delegateToOwn(Cell, '_state', ['alive', 'aliveInNextGeneration']); var someFsmCell = extend({ _state: Alive, _location: {x: -15, y: 12} }, FsmCell); someFsmCell delegates alive and aliveInNextGeneration to its _state property, and you can
change its state with assignment: someFsmCell._state = Dead; ⁷⁹https://leanpub.com/4rulesofsimpledesign
Metaobjects
140
In practice, states would be assigned en masse, but this demonstrates one of the simplest possible state machines. In the wild, most business objects are state machines, sometimes with multiple, loosely coupled states. Employees can be: â&#x20AC;˘ In or out of the office; â&#x20AC;˘ On probation, on contract, or permanent; â&#x20AC;˘ Part time or full time. Delegation to a property representing state takes advantage of late binding to break behaviour into smaller components that have cleanly defined responsibilities.
late bound forwarding The exact same technique can be used for forwarding to a property, and forwarding to a property can also be used for some kinds of state machines. Forwarding to a property has lower coupling than delegation, and is preferred where appropriate.
Metaobjects
141
Conflict Resolution Policies Mixins, Private Mixins, Forwarding, and Delegation all work smoothly when a single template or object contains all the behaviour you need. We also noted that you can mix more than one template into an object, or forward/delegate methods to more than one provider. There is a many-to-many relationship between objects and their providers. This is very good, because it allows us to create providers that have clear responsibilities. For example: var Person = { setName: function (first, last) { this._firstName = first; this._lastName= last; return this; }, fullName: function () { return this._firstName + " " + this._lastName; }, rename: function (first, last) { this._firstName = first; this._lastName = last; return this; } };
And: var IsAuthor = { addBook: function (name) { this._books.push(name); return this; }, books: function () { return this._books; } };
We can mix them all into one object:
Metaobjects
142
var raganwald = extend(raganwald, Person, IsAuthor, HasChildren); raganwald .setName('reginald', 'braithwaite') .addBook('JavaScript Spessore') .addBook('JavaScript Allongé');
Did you spot the error? You can’t add books to an array that doesn’t exist! Let’s fix that: var IsAuthor = { constructor: function () { this._books = []; return this; }, addBook: function (name) { this._books.push(name); return this; }, books: function () { return this._books; } };
Now we can write: var raganwald = extend(raganwald, Person, IsAuthor); raganwald .constructor() .setName('reginald', 'braithwaite') .addBook('JavaScript Spessore') .addBook('JavaScript Allongé');
There’s more to our author than books, of course. Let’s give him a family, using the same pattern:
Metaobjects
143
var HasChildren = { constructor: function () { this._children = []; return this; }, addChild: function (name) { this._children.push(name); return this; }, numberOfChildren: function () { return this._children.length; } };
And the we write: var raganwald = extend(raganwald, Person, IsAuthor, HasChildren); raganwald .constructor() .setName('reginald', 'braithwaite') .addBook('JavaScript Spessore') .addBook('JavaScript AllongĂŠ') .addChild('Thomas') .addChild('Clara');
Or do we?
method conflicts As you can see, both IsAuthor and HasChildren provide an initialize method. Revisiting extend, we see that when you extend an object, properties get copied in from the provider using assignment. This creates a property if none existed, but it overwrites a property that may already have been there. (The same is true for our Private Mixin, Forward, and Delegation patterns.) We see in this example the two weaknesses that our simple mixins have: 1. Simple mixins do not have any provision for initialization you have to do that yourself, and; 2. Simple mixins have a simple mechanism for resolving conflicts: The last method mixed in overwrites the previous method of the same name.
Metaobjects
144
“Overwriting” is rarely the correct policy. So let’s change the policy. For example, we might want this policy: When there are two or more methods with the same name, execute them in the same order that you mixed them in. return the value of the last one. This sounds familiar. Let’s rewrite extend: var __slice = [].slice; function sequence (fn1, fn2) { return function () { fn1.apply(this, arguments); return fn2.apply(this, arguments); } } function extendAfter () { 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)) { if (consumer[key] == null) { consumer[key] = provider[key]; } else { consumer[key] = sequence(consumer[key], provider[key]); } }; }; }; return consumer; };
We’re using functional composition to ensure that when we mix two methods with the same name in, they are evaluated in sequence. Of course, we could also implement a policy where we evaluate them in reverse order, by writing consumer[key] = sequence(provider[key], consumer[key]);
Metaobjects
145
These two policies, what we might call after and before, are both standard ideas in Aspect-Oriented Programming⁸⁰, something we discussed earlier when talking about metamethods. What they have in common with the “overwrite” policy is that the method implementation knows nothing of the possibility that there are more than one method with the same name being evaluated.
named parameters Of course, it’s easy to use a policy like “after” when a method has no parameters. But what about: var IsAuthor = { constructor: function () { this._books = __slice.call(arguments, 0); return this; }, // ... }; // ... var HasChildren = { constructor: function () { this._children = __slice.call(arguments, 0); return this; }, // ... };
This allows us to write something like this: var raganwald = extend(raganwald, Person, IsAuthor); raganwald .constructor('JavaScript Spessore', 'JavaScript Allongé');
Or this: var raganwald = extend(raganwald, Person, HasChildren); raganwald .constructor('Thomas', 'Clara');
But not both, because assigning parameters by position conflicts. languages like Smalltalk evade this problem by giving parameters names. No problem, we can fake named parameters using object literals:⁸¹ ⁸⁰https://en.wikipedia.org/wiki/Aspect-oriented_programming ⁸¹Transpile-to-JavaScript languages like CoffeeScript, and the upcoming EcmaScript 6 verison of JavaScript provide destructuring to help with
the syntax of things like named parameters. But the mechanism is identical.
Metaobjects
146
var IsAuthor = { constructor: function (defaults) { defaults = defaults || {}; this._books = defaults.books || []; return this; }, // ... }; // ... var HasChildren = { constructor: function (defaults) { defaults = defaults || {}; this._children = defaults.children || []; return this; }, // ... }; var raganwald = extendAfter(raganwald, Person, IsAuthor, HasChildren); raganwald .constructor({ books: ['JavaScript Spessore', 'JavaScript Allongé'], children: ['Thomas', 'Clara'] });
coupling methods If we can write IsAuthor so that its .constructor method knows nothing about the possibility that some other mixin like hasChildren also has a .constructor method, this is ideal. We don’t want them coupled to each other. Using a policy like “after” and named parameters, we avoid either method having any dependancy on the other. But there are times when we deliberately want one provider to have some knowledge of the other, to depend on it. For example, we might have a mixin that decorates another mixin. Some authors write books under their own name and also a nom de plume or alias (some use many aliases, but we’ll keep it simple). We’ll use a person as an alias, like this:
Metaobjects
147
var wheeler = extend({}, Person, IsAuthor) .constructor({ books: ['Cycle your way to happiness'] }) .setName('Reggie', 'Wheelser');
And write a mixin for authors who use an alias: var HasAlias = { constructor: function (defaults) { defaults = defaults || {}; this._alias = defaults.alias; return this; }, nomDePlume: function () { return this._alias.fullName(); } }; var raganwald = extend(raganwald, Person, IsAuthor, HasAlias); raganwald .constructor({ alias: wheeler, books: ['JavaScript Spessore', 'JavaScript Allongé'] });
Our “raganwald” person writes under his own name as well as the alias “Reggie Wheeler.” Now, how does he list his books? Ideally, we have a list of his own books and the books written under his alias. We’ll need a method in the HasAlias mixin, and it will need to be aware of the method it is decorating. We can do that using an around policy. Here’s extendAround: var __slice = [].slice; function around (fn1, fn2) { return function () { var argArray = [fn1.bind(this)].concat(__slice.call(arguments, 0)); return fn2.apply(this, argArray); } }
Metaobjects
148
function extendAround () { 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)) { if (consumer[key] == null) { consumer[key] = provider[key]; } else { consumer[key] = around(consumer[key], provider[key]); } }; }; }; return consumer; };
Now when one method conflict with another, it implicitly takes the previous method as a parameter. Note that there are some shenanigans to get the context right! Hereâ&#x20AC;&#x2122;s everything we need for the alias: var Person = { setName: function (first, last) { this._firstName = first; this._lastName= last; return this; }, fullName: function () { return this._firstName + " " + this._lastName; }, rename: function (first, last) { this._firstName = first; this._lastName = last; return this; } };
Metaobjects
var IsAuthor = { constructor: function (defaults) { defaults = defaults || {}; this._books = defaults.books || []; return this; }, addBook: function (name) { this._books.push(name); return this; }, books: function () { return this._books; } }; var wheeler = extend({}, Person, IsAuthor) wheeler.constructor({ books: ['Cycle your way to happiness'] }); wheeler.setName('Reggie', 'Wheeler');
And hereâ&#x20AC;&#x2122;s everything we need for an author that has an alias: var HasAlias = { constructor: function (authorInitialize, defaults) { authorInitialize(defaults); defaults = defaults || {}; this._alias = defaults.alias; return this; }, nomDePlume: function () { return this._alias.fullName(); }, books: function (authorBooks) { return authorBooks().concat(this._alias.books()); } }; var raganwald = extend({}, Person, IsAuthor) raganwald = extendAround(raganwald, HasAlias); raganwald.constructor({
149
Metaobjects
150
alias: wheeler, books: ['JavaScript Spessore', 'JavaScript Allongé'] }); raganwald.books(); //=> [ 'JavaScript Spessore', 'JavaScript Allongé', 'Cycle your way to happiness' ]
Because our extendAround uses an around policy for every method, you can see that we have had to modify its initialize method to call the author’s initialize method. This is really unnecessary, it should have an “after” policy so that the method itself is simpler and less error-prose to write. But the books method now “wraps around” the author’s books method, which is what we want. So what to do?
functional mixins with policies What we want is a way to mix a template in, with some control over the policy for each method. Here’s one such implementation: It converts a template into a functional mixin, a function that mixes the template into an object: var policies = { overwrite: function overwrite (fn1, fn2) { return fn1; }, discard: function discard (fn1, fn2) { return fn2; }, before: function before (fn1, fn2) { return function () { var fn1value = fn1.apply(this, arguments), fn2value = fn2.apply(this, arguments); return fn2value !== void 0 ? fn2value : fn1value; } }, after: function after (fn1, fn2) { return function () { var fn2value = fn2.apply(this, arguments), fn1value = fn1.apply(this, arguments);
Metaobjects
return fn2value !== void 0 ? fn2value : fn1value; } }, around: function around (fn1, fn2) { return function () { var argArray = [fn2.bind(this)].concat(__slice.call(arguments, 0)); return fn1.apply(this, argArray); } } }; function inverse (hash) { return Object.keys(hash).reduce(function (inversion, policyName) { var methodNameOrNames = hash[policyName], methodName; if (typeof(methodNameOrNames) === 'string') { methodName = methodNameOrNames; inversion[methodName] = policies[policyName]; } else if (typeof(methodNameOrNames.forEach) === 'function') { methodNameOrNames.forEach(function (methodName) { inversion[methodName] = policies[policyName]; }); } return inversion; }, {}); } function mixinWithPolicy (provider, policyDefinition) { var policiesByMethodName = inverse(policyDefinition || {}), defaultPolicy = policies.overwrite; if (policiesByMethodName['*'] != null) { defaultPolicy = policiesByMethodName['*']; delete policiesByMethodName['*']; } return function (receiver) { receiver = receiver || {}; Object.keys(provider).forEach(function (key) { if (receiver[key] == null) { receiver[key] = provider[key];
151
Metaobjects
} else if (policiesByMethodName[key] != null) { receiver[key] = policiesByMethodName[key](receiver[key], provider[key]); } else { receiver[key] = defaultPolicy(receiver[key], provider[key]); } }); return receiver; }; };
We use it like this: var IsPerson = mixinWithPolicy({ setName: function (first, last) { this._firstName = first; this._lastName= last; return this; }, fullName: function () { return this._firstName + " " + this._lastName; }, rename: function (first, last) { this._firstName = first; this._lastName = last; return this; } }); var clara = IsPerson();
We can also set up a mixin to use an â&#x20AC;&#x153;afterâ&#x20AC;? policy for all methods, by using *:
152
Metaobjects
var IsAuthor = mixinWithPolicy({ constructor: function (defaults) { defaults = defaults || {}; this._books = defaults.books || []; return this; }, addBook: function (name) { this._books.push(name); return this; }, books: function () { return this._books; } }, {after: '*'}); var HasChildren = mixinWithPolicy({ constructor: function (defaults) { defaults = defaults || {}; this._children = defaults.children || []; return this; }, addChild: function (name) { this._children.push(name); return this; }, numberOfChildren: function () { return this._children.length; } }, {after: '*'}); var gwen = IsAuthor(HasChildren(IsPerson())).constructor({ books: ['the little black cloud'], children: ['reginald', 'celeste', 'chris'] });
And we can set up policies for individual methods:
153
Metaobjects
154
var HasAlias = mixinWithPolicy({ constructor: function (defaults) { defaults = defaults || {}; this._alias = defaults.alias; return this; }, nomDePlume: function () { return this._alias.fullName(); }, books: function (authorBooks) { return authorBooks().concat(this._alias.books()); } }, {after: '*', around: 'books'}); var wheeler = IsAuthor(IsPerson()).constructor({ books: ['Cycle your way to happiness'] }).setName('Reggie', 'Wheeler'); var raganwald = HasAlias(IsAuthor(HasChildren(IsPerson()))); raganwald.constructor({ alias: wheeler, books: ['JavaScript Spessore', 'JavaScript Allongé'], children: ['Thomas', 'Clara'] });
other functional mixins The functional mixin given above mixes methods in. It’s also possible to make functions that perform private mixin, forwarding methods, or delegating methods. And naturally, you can control the policies for resolving method names as part of the function. You normally don’t need to decide whether an individual method needs to be private or forwarded within the context of a single provider, so it isn’t necessary to set up a method-by-method specification for mixin, private mixin, forward, or delegation. And functions aren’t the only way. Some implementations of this idea use objects with methods, e.g. Person.mixInto(raganwald). In that paradigm, you typically use methods to set policies, e.g. HasChildren.default('after') or hasAlias.around('books'). And implementations can vary. For example, we could use metamethods instead of function combinators. But the big idea is to combine the idea of using multiple metaobjects to provide behaviour with a policy for resolving methods when more than one metaobject provides the same method.
Metaobjects
155
Delegation via Prototypes At this point, we’re discussed separating behaviour from object properties using metaobjects while avoiding discussion of prototypes. This is deliberate, because what we have achieved is developing a vocabulary for describing what a prototype is. Let’s review what we know so far: var Person = { fullName: function () { return this.firstName + " " + this.lastName; }, rename: function (first, last) { this.firstName = first; this.lastName = last; return this; } };
So far, just like any other metaobject we’d use as a mixin, or perhaps with delegation. var sam = Object.create(Person); sam //=> {}
This is different. Instead of creating an object and associating it with a metaobject, we’re using Object.create, a built-in method that creates the object while simultaneously associating it with a prototype. The methods fullName and rename do not appear in its string representation. We’ll find out why in a moment. sam.fullName //=> [Function] sam.rename //=> [Function] sam.rename('Samuel', 'Ballard') //=> { firstName: 'Samuel', lastName: 'Ballard' } sam.fullName() //=> 'Samuel Ballard'
Metaobjects
156
And yet, they appear to be properties of sam, and we can invoke them in the usual fashion. Furthermore, we can tell that when the methods are invoked, the current context is being set to the receive, sam: That’s why invoking rename sets sam.firstName and sam.lastName. So far this is almost identical to using a mixin or delegation, but not a private mixin or forwarding because methods are evaluated in sam’s scope. The only difference appears to be how sam is displayed in the console. We recall that the big difference between a mixin and delegation is whether the methods are early or late bound. So, if we change a method in Person, then if prototypes are early bound, sam’s behaviour will not change. Whereas if methods are late bound, sam’s behaviour will change. Let’s try it: Person.fullName = function () { return this.lastName + ', ' + this.firstName; }; sam.fullName() //=> 'Ballard, Samuel'
Aha! Prototypes have delegation semantics: They are late bound, and evaluated in the receiver’s context. This is exactly why many programmers say that prototypes are a delegation mechanism. We’ve already seen delegation implemented via method proxies. Now we see it implemented via prototypes.
prototypes are strictly many-to-one. Delegation is a many-to-many relationship. One object can directly delegate to more than one metaobject, and more than one object can directly delegate to the same metaobject. This is not the case with prototypes: 1. Object.create only allows you to specific one prototype for an object you’re creating. 2. Although there is no sanctioned way to change the prototype of an object that has already been created, there is a proposal to include Object.setPrototypeOf⁸² in the next version of JavaScript. However, it will simply change the existing prototype, it will not add another prototype, so each object will still only delegate to one prototype at a time.
at the moment, the identity of the prototype is static As mentioned above, official support for changing the prototype of an object will be coming in EcmaScript 6, but it is not here now. Some shims and workarounds exist, but may not be available ⁸²https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf
Metaobjects
157
in all environments. There are also performance implications to consider, although we do not focus on that in this book. So, it is fair to say that at this moment, we can perform “later binding” delegation such as creating a state machine by delegating to an object’s property, but we cannot reliably do the same by delegating to different prototypes.
sharing prototypes Several objects can share one prototype: var sam = Object.create(Person); var saywhatagain = Object.create(Person); sam and saywhatagain both share the Person prototype, so they both share the rename and fullName
methods. But they each have their own properties, so: sam.rename('Samuel', 'Ballard'); saywhatagain.rename('Samuel', 'Jackson'); sam.fullName() //=> 'Samuel Ballard' saywhatagain.fullName() //=> 'Samuel Jackson'
The limitation of this scheme becomes apparent when we consider behaviours that need to be composed. Given Person, IsAuthor, and HasBooks, if we have some people that are authors, some that have children, some that aren’t authors and don’t have children, and some authors that have children, prototypes cannot directly manage these behaviours without duplication.
prototypes are open for extension With forwarding and delegation, the body of the method being proxied is late-bound because it is looked up by name when the method is invoked. This differs from mixins and private mixins, where the body of the method is early bound by reference at the time the metaobject is mixed in.
Metaobjects
158
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; }; var lowry = {}; delegate(lowry, Person); lowry.rename('Sam', 'Lowry'); Person.fullName = function () { return this.firstName[0] + '. ' + this.lastName; }; lowry.fullName(); //=> 'S. Lowry'
Prototypes and delegation both allow you to change the body of a method after a metaobject has been bound to an object. But what happens if we add an entirely new method to our metaobject? Person.surname = function () { return this.lastName; }
An object using our method proxies does not delegate the new method to its metaobject, because we never created a method proxy for surname: lowry.surname() //=> TypeError: Object #<Object> has no method 'surname'
Whereas, an object using a prototype does delegate the new method to the prototype:
Metaobjects
159
sam.surname() //=> 'Ballard'
Prototypes late bind the method bodies and they late bind the identities of the methods being delegated. So you can add and remove methods to a prototype, and the behaviour of all of the objects bound to that prototype will be changed. We say that prototypes are open for extension, because you can extend their behaviour after creating objects with them. We say that mixins are closed for extension, because behaviour added to a mixin does not change any of the objects that have already incorporated it.
summarizing Prototypes are a special kind of delegation mechanism that is built into JavaScript. Delegating through prototypes is: 1. Late bound on method bodies, just like delegation through method proxies; 2. Late bound on the method identities, which is superior to delegation through method proxies; 3. Evaluated in the receiver’s context, just like delegation. Prototypes are usually the first form of metaobject that many developers learn in JavaScript, and quite often the last.
…one more thing! There is one more way that delegation via prototypes differs from delegation via method proxies, and it’s very important. We recall from above that object delegating to a prototype appear differently in the console than objects delegating via method proxies: sam //=> { firstName: 'Samuel', lastName: 'Ballard' } lowry //=> { fullName: [Function], rename: [Function], firstName: 'Sam', lastName: 'Lowry' }
Metaobjects
160
The reason is very simple: The code for representing an object in the console iterates over its â&#x20AC;&#x153;ownâ&#x20AC;? properties, properties that belong to the object itself and not its prototype. In the case of sam, those are firstName and lastName, but not fullName or rename because those are properties of the prototype. Whereas in the case of lowry, fullName and rename are properties of Person, but there are also function proxies that are properties of the lowry object itself. We can test this for ourselves using the .hasOwnProperty method: sam.hasOwnProperty('fullName'); //=> false lowry.hasOwnProperty('fullName'); //=> true
One of the goals of metaobjects is to separate domain properties (such as firstName) from behaviour (such as .fullName()). All of our metaobject techniques allow us to do that in our written code, but prototypes do this extremely effectively in the runtime structure of the objects themselves. This is extremely useful.
161
Metaobjects
Singleton Prototypes We are all perfectly aware that prototypes can be used to create tree-like hierarchies of “classes.” But let’s engage our “Beginner’s Minds” and approach prototypes from first principles. Shoshin (��) is a concept in Zen Buddhism meaning “beginner’s mind.” It refers to having an attitude of openness, eagerness, and lack of preconceptions when studying a subject, even when studying at an advanced level, just as a beginner in that subject would. –Shoshin on Wikipedia⁸³
We discussed how prototypes separate behaviour from properties at runtime as well as in our written code. When we write: var Proto = { snafu: function () { return 'Situation normal, all fragged up.'; } }; var foo = Object.create(Proto);
Even though foo responds to the message .snafu, foo does not have its own snafu property: foo.snafu //=> [Function] foo.snafu() //=> 'Situation normal, all fragged up.' foo.hasOwnProperty('snafu') //=> false
The very simplest way to use this is to do exactly what we’ve done above: Create an object with its own prototype. It’s possible to have more than one object share a prototype, but for now, let’s buck convention and ignore that. In essence, we’re taking the idea of a “naïve object” like this:
⁸³https://en.wikipedia.org/wiki/Shoshin
Metaobjects
162
var bar = { fubar: function () { return this.word + ' up beyond all recognition.'; }, word: 'Fiddled' }; //=> { fubar: [Function], word: 'Fiddled' }
And turning it into a naive prototype, like this: var bar = Object.create({ fubar: function () { return this.word + ' up beyond all recognition.'; } }); bar.word = 'Fiddled'; bar //=> { word: 'Fiddled' }
building prototypes with mixins One of the very interesting things about JavaScript is that it’s a metaobject-1 language, meaning that its metaobjects have the same properties and methods as the objects they describe. This differs from metaobject-2 languages like Smalltalk or Ruby, where the metaobjects have methods and properties designed for implementing a metaobject protocol, and the methods of the objects they describe are part of the metaobject’s internal, hidden state. We will discuss later building metaobject-2s in JavaScript, but for now we can look at what we have and see how to exploit it. When looking at prototypes, we saw that an object can only have one prototype. Objects can have more than one mixin, but mixins do not separate behaviour from domain properties at runtimes. How to get around this? Let’s recall our example of mixing in with conflict resolution policies: var policies = { overwrite: function overwrite (fn1, fn2) { return fn1; }, discard: function discard (fn1, fn2) { return fn2; }, before: function before (fn1, fn2) { return function () { var fn1value = fn1.apply(this, arguments), fn2value = fn2.apply(this, arguments);
Metaobjects
return fn2value !== void 0 ? fn2value : fn1value; } }, after: function after (fn1, fn2) { return function () { var fn2value = fn2.apply(this, arguments), fn1value = fn1.apply(this, arguments); return fn2value !== void 0 ? fn2value : fn1value; } }, around: function around (fn1, fn2) { return function () { var argArray = [fn2.bind(this)].concat(__slice.call(arguments, 0)); return fn1.apply(this, argArray); } } }; function inverse (hash) { return Object.keys(hash).reduce(function (inversion, policyName) { var methodNameOrNames = hash[policyName], methodName; if (typeof(methodNameOrNames) === 'string') { methodName = methodNameOrNames; inversion[methodName] = policies[policyName]; } else if (typeof(methodNameOrNames.forEach) === 'function') { methodNameOrNames.forEach(function (methodName) { inversion[methodName] = policies[policyName]; }); } return inversion; }, {}); } function mixinWithPolicy (provider, policyDefinition) { var policiesByMethodName = inverse(policyDefinition || {}), defaultPolicy = policies.overwrite;
163
Metaobjects
if (policiesByMethodName['*'] != null) { defaultPolicy = policiesByMethodName['*']; delete policiesByMethodName['*']; } return function (receiver) { receiver = receiver || {}; Object.keys(provider).forEach(function (key) { if (receiver[key] == null) { receiver[key] = provider[key]; } else if (policiesByMethodName[key] != null) { receiver[key] = policiesByMethodName[key](receiver[key], provider[key]); } else { receiver[key] = defaultPolicy(receiver[key], provider[key]); } }); return receiver; }; }; var IsPerson = mixinWithPolicy({ setName: function (first, last) { this._firstName = first; this._lastName= last; return this; }, fullName: function () { return this._firstName + " " + this._lastName; }, rename: function (first, last) { this._firstName = first; this._lastName = last; return this; } }); var IsAuthor = mixinWithPolicy({ constructor: function (defaults) { defaults = defaults || {}; this._books = defaults.books || []; return this;
164
Metaobjects
165
}, addBook: function (name) { this._books.push(name); return this; }, books: function () { return this._books; } }, {after: '*'}); var HasChildren = mixinWithPolicy({ constructor: function (defaults) { defaults = defaults || {}; this._children = defaults.children || []; return this; }, addChild: function (name) { this._children.push(name); return this; }, numberOfChildren: function () { return this._children.length; } }, {after: '*'}); var HasAlias = mixinWithPolicy({ constructor: function (defaults) { defaults = defaults || {}; this._alias = defaults.alias; return this; }, nomDePlume: function () { return this._alias.fullName(); }, books: function (authorBooks) { return authorBooks().concat(this._alias.books()); } }, {after: '*', around: 'books'});
We can now do the exact same thing we did before, but now we mix the behaviour into prototypes for our objects:
Metaobjects
166
var wheeler = Object.create(IsAuthor(IsPerson())); wheeler .constructor({ books: ['Cycle your way to happiness'] }) .setName('Reggie', 'Wheeler'); var raganwald = Object.create(HasAlias(IsAuthor(HasChildren(IsPerson())))); raganwald .constructor({ alias: wheeler, books: ['JavaScript Spessore', 'JavaScript Allongé'], children: ['Thomas', 'Clara'] });
As we saw with the “naïve prototype”, the objects themselves do not have any of their own methods, just properties: raganwald //=> { _children: [ 'Thomas', 'Clara' ], _books: [ 'JavaScript Spessore', 'JavaScript Allongé' ], _alias: { _books: [ 'Cycle your way to happiness' ], _firstName: 'Reggie', _lastName: 'Wheeler' } }
We have all the benefits of our mixins with policies for resolving method conflicts, and we also have the full separation of domain properties from behaviour. We can also use private mixins, forwarding and delegation to provide behaviour for a prototype, and then create an object that delegates its behaviour to the prototype. This general pattern is called the singleton prototype. To summarize, objects are created with their own prototypes. We then mix behaviour into the prototype or use functional proxies to forward and delegate behaviour from the prototype to other metaobjects. This provides the advantage of separating behaviour from properties at runtime, while also allowing us to manage composable⁸⁴ chunks of behaviour and mix them into prototypes as needed. ⁸⁴https://en.wikipedia.org/wiki/Composability
Metaobjects
167
Shared Prototypes In Singleton Prototypes, we learned that we can create a very simple object and associate it with a prototype: var Person = { fullName: function () { return this.firstName + " " + this.lastName; }, rename: function (first, last) { this.firstName = first; this.lastName = last; return this; } }; var sam = Object.create(Person);
This associates behaviour with our object: sam.rename('sam', 'hill'); sam.fullName(); //=> 'sam hill'
There is no way to associate more than one prototype with the same object, but we can associate more than one object with the same prototype: var bewitched = Object.create(Person); bewitched.rename('Samantha', 'Stephens');
Although they share the prototype, their individual properties (as access with this), are separate: sam //=> { firstName: 'sam', lastName: 'hill' } bewitched //=> { firstName: 'Samantha', lastName: 'Stephens' }
This is very convenient.
prototype chains Consider our HasCareer mixin:
Metaobjects
168
var HasCareer = { career: function () { return this.chosenCareer; }, setCareer: function (career) { this.chosenCareer = career; return this; } };
We can use it as a prototype, of course. But we already want to use Person as a prototype. What can we do? Obviously, we can combine Person and HasCareer into a “fat prototype” called PersonWithCareer. This is not great, a general principle of software is that entities should have a single clearly defined responsibility. Even if we weren’t hung up on single responsibility, another issue is that not all people have careers, so we need one prototype for people, and another for people with careers. The catch is, another principle of good design is that every piece of knowledge should have one unambiguous representation. The knowledge of what makes a person falls into this category. If we were to add another method to Person, would we remember to add it to PersonWithCareer? Let’s work from two principles: 1. Any object can have an object as its prototype, and any object can be a prototype. 2. he behaviour of an object consists of all of its own behaviour, plus all the behaviour of its prototype. When we say any object can have a prototype, does that include objects used as prototypes? Yes. Objects used as prototypes can have prototypes of their own. Let’s try it. First things first: var PersonWithCareer = Object.create(Person);
Now let’s mix HasCareer into PersonWithCareer: extend(PersonWithCareer, HasCareer);
And now we can use PersonWithCareer as a prototype:
Metaobjects
169
var goldie = Object.create(PersonWithCareer); goldie.rename('Samuel', 'Goldwyn'); goldie.setCareer('Producer');
Why does this work? Imagine that we were writing a JavaScript (or other OO) language implementation. Method invocation is incredibly messy, full of special optimizations and so forth, but perhaps we only have ten days to get it done, so we might proceed like this without even thinking about prototype chains: function invokeMethod(receiver, methodName, listOfArguments) { return invokeMethodWithContext(receiver, receiver, methodName, listOfArguments); } function invokeMethodWithContext(context, receiver, methodName, listOfArguments) { var prototype; if (receiver.hasOwnProperty(methodName)) { return receiver[methodName].apply(context, listOfArguments); } else if (prototype = Object.getPrototypeOf(receiver)) { return invokeMethodWithContext(context, prototype, methodName, listOfArgument\ s); } else { throw 'Method Missing ' + methodName; } }
Very simple: If the object implements the method, invoke it with .apply. If the object doesn’t implement it but has a prototype, ask the prototype to implement it in the original receiver’s context. What if the prototype doesn’t implement it but has a prototype of its own? Well, we’ll recursively try that object too. Conceptually, this is what happens when we write: goldie.fullName() //=> 'Samuel Goldwyn'
In theory, the JavaScript engine walks up a chain starting with the goldie object, followed by our PersonWithCareer prototype followed by our Person prototype.
170
Metaobjects
trees Chaining prototypes is a useful technique, however it has some limitations. Because objects can have only one prototype, You cannot model all combinations of responsibilities solely with prototype chains. The classic example is the “W” pattern: Let’s consider three prototypes to be used for employees in a factory: Laborer, Manager, and OnProbation. All employees are either Laborer or Manager, but not both. So far, very easy, they can be prototypes. Some laborers are also on probation, as are some managers. How do we handle this with prototype chains?
Labourers, Managers, and OnPobation
Well, we can’t have Laborer or Manager share OnProbation as a prototype, because then all laborers and managers would be on probation. And if we make OnProbation have Laborer as its prototype, there’s no way to have a manager also be on probation without making it also a laborer, and that’s not allowed. Quite simply, a tree is an insufficient mechanism for modeling this relationship. Prototype chains model trees, but most domain responsibilities cannot be represented as trees, so we must either revert to using “fat prototypes,” or find another way to represent responsibilities.
prototypes and mixins In Singleton Prototypes, we saw the pattern of creating a prototype and then mixing behaviour into the prototype. We also used this technique when we created the PersonWithCareer shared prototype. We can extend this technique when we run into limitations with prototype chains:
Metaobjects
171
var Laborer = { // ... }; var Manager = { // ... }; var Probationary = { // ... }; var LaborerOnProbation = extend({}, Laborer, Probationary); var ManagerOnProbation = extend({}, Manager, Probationary);
Using mixins, we have created prototypes that model combining labor/management with probationary status.
caveat programmer Whether we’re using prototype chains or mixins, we’re introducing coupling. As discussed in Mixins, Forwarding, and Delegation, prototypes that are brought into proximity with each other (by placing them anywhere in the same chain, or by mixing them into the same object) become deeply coupled because they both have complete access to an object’s private internal state through this. To reduce this coupling, we have to find a way to insulate prototypes from each other. Techniques like private mixins or forwarding, while straightforward to use directly on an object or through a singleton prototype, require special handling when used in a shared prototype.
shared prototypes and private mixins The standard way for a method to query or update the receiver’s state is for it to be evaluated in the receiver’s context, so that it can use this. This is automatic for simple methods that are defined in the receiver or its prototype. But when a method has its context bound to another object (via .bind or some other mechanism), its state will be independent of the receiver. This is a problem when two or more objects share the same method via a shared prototype. Let’s revisit our “private mixin” pattern:
Metaobjects
172
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; }; var HasCareer = { career: function () { return this.chosenCareer; }, setCareer: function (career) { this.chosenCareer = career; return this; } }; twain = {} sam = {}; extendPrivately(twain, HasCareer); extendPrivately(sam, HasCareer); twain.setCareer('Author'); sam.setCareer('Director'); twain.career() //=> 'Author' sam.career() //=> 'Director'
Weâ&#x20AC;&#x2122;re privately extending a domain object. When we call extendPrivately, a new object is created to hold the private state. We call it once for each object, so each object has its own private state for HasCareer
Now letâ&#x20AC;&#x2122;s try it with a shared prototype:
Metaobjects
173
var Careerist = Object.create(null); extendPrivately(Careerist, HasCareer); twain = Object.create(Careerist); sam = Object.create(Careerist); twain.setCareer('Author'); sam.setCareer('Director'); twain.career() //=> 'Director' sam.career() //=> 'Director'
There is only one private state, and it is associated with the prototype, not with each object. That might be what we want, it resembles the way “class methods” work in some other languages. But if we want private state for each object, we must understand that a private mixin cannot be applied to a shared prototype.
subordinate forwarding and shared prototypes The mechanism for forwarding works for any object. It could be a metaobject designed just to implement some functionality, it could be another domain object, it works for anything. One common pattern is called subordinate forwarding. When there is a strict one-to-one relationship between a forwarding object and a forwardee, then the forwardee is a subordinate of the forwarding object. It exists only to provide behaviour for the forwarding object. Subordinate forwarding is appropriate when the state encapsulated by the forwardee is meant to represent the state of the forwarding object. Unless we are modeling some kind of joint and several ownership, an investor’s portfolio is subordinate to the investor. When people talk about modeling behaviour with composition, they are often thinking of composing an entity out of subordinate objects, then using mechanisms like forwarding to implement its behaviour. When a single object forwards to a single subordinate, this works well. But as with private mixins, we must be careful when attempting to use forwarding with a shared prototype: All of the methods in the prototype will forward to the same object, therefore every object that uses the prototype will forward to the same object, not to their own subordinates. Therefore they will not have their own, separate state.
safekeeping for private mixins Our problem with private mixins is that they place the private state in an object shared by the methods in the mixin. But when the methods are mixed into a shared prototype, every object that
Metaobjects
174
delegates to those methods is sharing the methods, and this sharing that one shared state. Let’s rewrite our extendPrivately function to store the private data in safekeeping: var number = 0; function extendPrivately (prototype, mixin) { var safekeepingName = "__" + ++number + "__", methodName; for (methodName in mixin) { if (mixin.hasOwnProperty(methodName)) { (function (methodName) { prototype[methodName] = function () { var context = this[safekeepingName], result; if (context == null) { context = {}; Object.defineProperty(this, safekeepingName, { enumerable: false, writable: false, value: context }); } result = mixin[methodName].apply(context, arguments); return (result === context) ? this : result; }; })(methodName); }; }; return prototype; }
We’re storing the context in a hidden property within the receiver rather than sharing it amongst all the methods. Let’s try it, we’ll create a shared prototype called Careerist, and we’ll mix in HasName and HasCareer. To make sure everything works as expected, we’ll use two different objects delegating to Careerist, and we’ll make sure that both HasName and HasCareer both try to modify the name property:
Metaobjects
var HasName = { name: function () { return this.name; }, setName: function (name) { this.name = name; return this; } }; var HasCareer = { career: function () { return this.name; }, setCareer: function (name) { this.name = name; return this; } }; var Careerist = {}; extendPrivately(Careerist, HasName); extendPrivately(Careerist, HasCareer); var michael = Object.create(Careerist), bewitched = Object.create(Careerist); michael.setName('Michael Sam'); bewitched.setName('Samantha Stephens'); michael.setCareer('Athlete'); bewitched.setCareer('Thaumaturge'); michael.name() //=> 'Michael Sam' bewitched.name() //=> 'Samantha Stephens' michael.career() //=> 'Athlete' bewitched.career()
175
Metaobjects
176
//=> 'Thaumaturge'
Our new function also works for singleton prototypes, or even old-fashioned extending: var coder = {}; extendPrivately(coder, HasName); extendPrivately(coder, HasCareer); coder.setName('Samuel Morse'); coder.name() //=> 'Samuel Morse'
Of course, the new function has more moving parts than the old one, and thatâ&#x20AC;&#x2122;s why we build these things up as we go along: If we start with a simple thing, we can add functionality on a piece at a time and build up to something that covers many general cases. This pattern simplifies things by reverting to the â&#x20AC;&#x153;overwriteâ&#x20AC;? policy for methods. A more robust implementation would incorporate a conflict resolution policy.
Encapsulation and Composition
⁸⁵
⁸⁵Nespresso Capsules (c) 2011 André Nieto Porras, some rights reserved
Encapsulation and Composition
In This Chapter In this chapter, weâ&#x20AC;&#x2122;ll extend our look at encapsulating metaobjects and dive deeper into the use of proxies and partial proxies to decouple metaobjects from each other.
.
178
Encapsulation and Composition
179
The Encapsulation Problem The central form of encapsulation in object-oriented programming consists of objects that invoke methods on each other. Each object’s method bodies invoke other methods and/or access the object’s own private state. Objects do not read or update each other’s private state. Objects and behaviour have dependencies, but the scope of every dependency is explicitly limited to the methods exposed and implicitly limited to the changes of state that can be observed through the methods exposed. Unlike Smalltalk, Ruby, or many other OO languages, JavaScript does not enforce this encapsulation: Objects are free to read and update each other’s properties.
the closure workaround It’s possible to use a closure to create private state. We saw this in JavaScript Allongé⁸⁶: var stack = (function () { var array = [], index = -1; return { push: function (value) { array[index += 1] = value }, pop: function () { var value = array[index]; if (index >= 0) { index -= 1 } return value }, isEmpty: function () { return index < 0 } } })();
This approach has several related problems: 1. A separate instance of each function must be created for each object. ⁸⁶https://leanpub.com/javascript-allonge
Encapsulation and Composition
180
2. It is not open for extension. You cannot define another stack method elsewhere that uses the “private” variables array or index. 3. You cannot use this directly in a shared prototype. 4. Since the private state is captured in variables, you cannot take advantage of property access features like iterating over properties. 5. You can’t vary privacy separately from methods. The code must be written one way for normal methods and another way for methods with private state. There are workarounds for these problems, but we must be mindful of its limitations.
private mixins The private mixin pattern addresses some of the problems with the closure workaround: 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; }; var HasCareer = { career: function () { return this.chosenCareer; }, setCareer: function (career) { this.chosenCareer = career; return this; } }; var somebody = {}; extendPrivately(somebody, HasCareer);
Privacy and method concerns are separated. And the private properties are in an object so they behave like all properties. But our private state is still not open for extension, and a private mixin
Encapsulation and Composition
181
can only be employed in a prototype chain with a workaround of its own. And the private properties are in a separate object from the public properties, so it is not possible to access both at the same time.
Encapsulation and Composition
182
Proxies As we recall from proxies, there is a simple pattern for encapsulation that solves many of the problems we saw earlier: We use forwarding to create a proxy⁸⁷. 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; } 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; } }; ⁸⁷https://en.wikipedia.org/wiki/Proxy_pattern
Encapsulation and Composition
183
var stackProxy = proxy(stack);
Our stackProxy has methods, but no private data: stackProxy.push('first'); stackProxy //=> { push: [Function], pop: [Function], isEmpty: [Function] } stackProxy.pop(); //=> first
The proxy completely encapsulates the stack’s private state. And this technique works no matter how we define our implementation object. We could have it mix behaviour in, delegate through a prototype chain, anything we want. Here we’re mixing behaviour into the implementation object’s singleton prototype: var Person = { fullName: function () { return this.firstName + " " + this.lastName; }, rename: function (first, last) { this.firstName = first; this.lastName = last; return this; } }; var HasCareer = { career: function () { return this.chosenCareer; }, setCareer: function (career) { this.chosenCareer = career; return this; } };
Encapsulation and Composition
184
var sam = Object.create(Object.create(null)); extend(Object.getPrototypeOf(sam), Person); extend(Object.getPrototypeOf(sam), HasCareer); var samProxy = proxy(sam);
The principle is that instead of forwarding to or mixing in part of the behaviour of our base object, weâ&#x20AC;&#x2122;re completely defining the implementation object and then creating a proxy for our implementation. This device is not perfect, for example it is not open for extension as it stands, but it forms the basis for solving more complex problems with encapsulation and coupling.
185
Encapsulation and Composition
Encapsulation for Metaobjects Encapsulation is not just a domain object concern. Lack of encapsulation also affects the relationship between metaobjects. When two or more metaobjects all have access to the same base object via open recursion⁸⁸, they become tightly coupled because they can interact via setting and reading all the base object’s properties. It is impossible to restrict their interaction to a well-defined set of methods. Encapsulation was a revolutionary idea in the 1980s when Smalltalk launched, and accepted as standard practice in the 1990s when Java rose to prominence. It is now a well-known design imperative. Paradoxically, that means that it often is not necessary to impose strict limitations on objects’ abilities to interact with each other’s internal state, because programmers are much less likely to attempt it today than they were in the 1980s when C was the popular language and manipulating structs was the popular paradigm.
. This coupling exists for all metaobject patterns that include open recursion, such as mixins, delegation, and delegation through naive prototypes. In particular, when chains of naive prototypes form class hierarchies⁸⁹, this coupling leads to the fragile base class problem⁹⁰.
A class hierarchy ⁸⁸https://en.wikipedia.org/wiki/Open_recursion#Open_recursion ⁸⁹https://en.wikipedia.org/wiki/Class_hierarchy ⁹⁰https://en.wikipedia.org/wiki/Fragile_base_class
Encapsulation and Composition
186
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. The programmer cannot determine whether a base class change is safe simply by examining in isolation the methods of the base class.– Wikipedia⁹¹
In JavaScript, prototype chains are vulnerable because changes to one prototype’s behaviour may break another prototype’s behaviour in the same chain. In this decade, we are much more worried about metaobjects becoming coupled than we are about objects. As we discussed earlier, when introducing the private mixins and forwarding, metaobjects quickly become coupled because of open recursion⁹². By default, metaobjects like prototypes are tightly coupled to each other because they all manipulate their base object’s properties. And while it is a more difficult problem to solve technically than the problem of coupling peer objects, it is more important to solve it because the problem is more widespread. Like it or not, many programmers who are perfectly aware that objects should not manipulate each other’s internal state will be surprised to learn that having “classes” manipulate an object’s internal state has exactly the same consequences. If we can find a way to manage the interface between an object and a metaobject, we can make our programs more robust and discourage others from carelessly introducing coupling.
inner proxies Our original proxy wrapped around an object, presenting an interface to the “outside world:” ⁹¹https://en.wikipedia.org/wiki/Fragile_base_class ⁹²https://en.wikipedia.org/wiki/Open_recursion#Open_recursion
187
Encapsulation and Composition
A proxy
An object or function can also create its own proxy around an object:
A proxy
Hereâ&#x20AC;&#x2122;s one way to use this. Recall our utility for creating a private mixin:
188
Encapsulation and Composition
extendPrivately
Note the highlighted expression. Every method we mix in shares the same new, empty object as its context. This separates them completely from the properties of any object they’re mixed into. Let’s start with our code for creating proxies: 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; }
But now, let’s rewrite the way we do “private” inheritance:
Encapsulation and Composition
189
function extendWithProxy (baseObject, behaviour) { var methodName, context = proxy(baseObject); for (methodName in behaviour) { if (behaviour.hasOwnProperty(methodName)) { baseObject[methodName] = behaviour[methodName].bind(context); }; }; return baseObject; }
Instead of extending an object with methods that only have access to a private object for holding their own state, weâ&#x20AC;&#x2122;re extending an object with methods that have access to their own private state and to the methods of the object. For example: var Person = { fullName: function () { return this.firstName + " " + this.lastName; }, rename: function (first, last) { this.firstName = first; this.lastName = last; return this; } }; var HasCareer = { career: function () { return this.chosenCareer; }, setCareer: function (career) { this.chosenCareer = career; return this; }, describe: function () { return this.fullName() + " is a " + this.chosenCareer; } }; var samwise = {};
Encapsulation and Composition
190
extendWithProxy(samwise, Person); extendWithProxy(samwise, HasCareer); samwise //=> { fullName: [Function], rename: [Function], career: [Function], setCareer: [Function], describe: [Function] }
Our new object has all the methods of both Person and HasCareer. Let’s try it: samwise.rename('Sam', 'Wise') samwise //=> { fullName: [Function], rename: [Function], career: [Function], setCareer: [Function], describe: [Function] } samwise.setCareer('Companion'); samwise.describe() //=> 'Sam Wise is a Companion'
Our describe method has access to HasCareer’s private internal state and to samwise’s fullName method.
an off-topic refinement If you’re “playing along at home,” you may have noticed this: samwise.setCareer('Companion'); //=> { fullName: [Function], rename: [Function], chosenCareer: 'Companion' }
The problem is that the setCareer method returns this, but when extended privately or encapsualtedly (we are making up words), this is the private state of the mixin, not the original object or its proxy. There are fixes for this. For example:
Encapsulation and Composition
function extendWithProxy (baseObject, behaviour) { var methodName, context = proxy(baseObject); for (methodName in behaviour) { if (behaviour.hasOwnProperty(methodName)) { (function (methodName) { baseObject[methodName] = function () { var result = behaviour[methodName].apply(context, arguments); return (result === context) ? baseObject : result; }; })(methodName); }; }; return baseObject; } var pepys = {}; extendWithProxy(pepys, Person); extendWithProxy(pepys, HasCareer); pepys.rename('Samuel', 'Pepys'); //=> { fullName: [Function], rename: [Function], career: [Function], setCareer: [Function], describe: [Function] } pepys.setCareer('Diarist'); //=> { fullName: [Function], rename: [Function], career: [Function], setCareer: [Function], describe: [Function] } pepys.describe(); //=> 'Samuel Pepys is a Diarist'
191
Encapsulation and Composition
192
proxies and prototypes Let’s try using our extendWithProxy function with a singleton prototype: var prototype = {}; extendWithProxy(prototype, Person); extendWithProxy(prototype, HasCareer); var michael = Object.create(prototype); michael.rename('Michael', 'Sam'); michael.fullName() //=> 'Michael Sam'
So far, so good. Now let’s use a shared prototype: var Careerist = {}; extendWithProxy(Careerist, Person); extendWithProxy(Careerist, HasCareer); var michael = Object.create(Careerist), betwitched = Object.create(Careerist); michael.rename('Michael', 'Sam'); betwitched.rename('Samantha', 'Stephens'); michael.fullName() //=> 'Samantha Stephens'
Bzzzzzzzt! This does not work because each of our “encapsulated” mixins hash its own private state, but they are mixed into the shared prototype, so therefore the private state is shared as well.
safekeeping for shared prototypes This is the same problem we solved with safekeeping for private mixins. Private mixin place the private state in an object shared by the methods in the mixin. But when the methods are mixed into a shared prototype, every object that delegates to those methods is sharing the methods, and this sharing that one shared state. So we’ll rewrite extendWithProxy to place the proxy in safekeeping:
Encapsulation and Composition
193
var number = 0; function extendWithProxy (baseObject, behaviour) { var safekeepingName = "__" + ++number + "__", methodName; for (methodName in behaviour) { if (behaviour.hasOwnProperty(methodName)) { (function (methodName) { baseObject[methodName] = function () { var context = this[safekeepingName], result; if (context == null) { context = proxy(this); Object.defineProperty(this, safekeepingName, { enumerable: false, writable: false, value: context }); } result = behaviour[methodName].apply(context, arguments); return (result === context) ? this : result; }; })(methodName); }; }; return baseObject; }
As with private mixins, we are storing the context in a hidden property within the receiver. However, instead of just being an empty object, the context is now a proxy for the receiver. Thus, every mixedin method has access to all of the receiver’s methods as well as state shared between the methods being mixed in. Let’s try it, we’ll create a shared prototype called Careerist, and we’ll mix in HasName and HasCareer. To make sure everything works as expected, we’ll use two different objects delegating to Careerist, and we’ll make sure that both HasName and HasCareer both try to modify the name property. Unlike previous examples, however, we’ll add a third mixin that relies on the methods from the other two:
Encapsulation and Composition
var HasName = { name: function () { return this.name; }, setName: function (name) { this.name = name; return this; } }; var HasCareer = { career: function () { return this.name; }, setCareer: function (name) { this.name = name; return this; } }; var IsSelfDescribing = { description: function () { return this.name() + ' is a ' + this.career(); } }; var Careerist = {}; extendWithProxy(Careerist, HasName); extendWithProxy(Careerist, HasCareer); extendWithProxy(Careerist, IsSelfDescribing); var michael = Object.create(Careerist), bewitched = Object.create(Careerist); michael.setName('Michael Sam'); bewitched.setName('Samantha Stephens'); michael.setCareer('Athlete'); bewitched.setCareer('Thaumaturge'); michael.description()
194
Encapsulation and Composition
195
//=> 'Michael Sam is a Athlete' bewitched.description() //=> 'Samantha Stephens is a Thaumaturge' IsSelfDescribing has its own private state, but it is also able to call .name and .career on the
receiver because its private state is also a proxy. This version of extendWithProxy is a superset of the behaviour of private mixins. It has more moving parts, but mixins that donâ&#x20AC;&#x2122;t want to call any methods on the receiver neednâ&#x20AC;&#x2122;t call any methods on the receiver.
Encapsulation and Composition
196
encapsulate(...) In Encapsulation for Metaobjects, we formulated extendWithProxy, a function that would extend an object with some behaviour, and simultaneously encapsulate that behaviour with an inner proxy. It works, but placing two responsibilities in one function is undesirable. The mechanism for encapsulating behaviour should be kept strictly separate from the mechanism for applying behaviour to an object. Letâ&#x20AC;&#x2122;s separate the responsibilities. We start with: 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; } var number = 0; function extendWithProxy (baseObject, behaviour) { var safekeepingName = "__" + ++number + "__", methodName; for (methodName in behaviour) { if (behaviour.hasOwnProperty(methodName)) { (function (methodName) { baseObject[methodName] = function () { var context = this[safekeepingName], result; if (context == null) { context = proxy(this); Object.defineProperty(this, safekeepingName, {
Encapsulation and Composition
enumerable: false, writable: false, value: context }); } result = methodBody.apply(context, arguments); return (result === context) ? this : result; }; })(methodName); }; }; return baseObject; }
We can partially apply this function: var encapsulate = allong.es.callLeft(extendWithProxy, Object.create(null));
Or write the whole thing out explicitly: var number = 0; function encapsulate (behaviour) { var safekeepingName = "__" + ++number + "__", encapsulatedObject = {}; Object.keys(behaviour).forEach(function (methodName) { var methodBody = behaviour[methodName]; encapsulatedObject[methodName] = function () { var context = this[safekeepingName], result; if (context == null) { context = proxy(this); Object.defineProperty(this, safekeepingName, { enumerable: false, writable: false, value: context }); } result = methodBody.apply(context, arguments); return (result === context) ? this : result;
197
Encapsulation and Composition
}; }); return encapsulatedObject; }
Now we can write: var Person = encapsulate({ fullName: function () { return this.firstName + " " + this.lastName; }, rename: function (first, last) { this.firstName = first; this.lastName = last; return this; } }); var HasCareer = encapsulate({ career: function () { return this.chosenCareer; }, setCareer: function (career) { this.chosenCareer = career; return this; }, describe: function () { return this.fullName() + " is a " + this.chosenCareer; } }); var samwise = extend({}, Person, HasCareer); samwise.rename('Sam', 'Wise') samwise //=> { fullName: [Function], rename: [Function], career: [Function], setCareer: [Function], describe: [Function] }
198
Encapsulation and Composition
199
samwise.setCareer('Companion'); samwise.describe() //=> 'Sam Wise is a Companion'
While weâ&#x20AC;&#x2122;re in the mood to refactor, letâ&#x20AC;&#x2122;s separate creating a context from getting the current context from building the forwarding methods: 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; }
Encapsulation and Composition
200
Encapsulating behaviours is a crucial building block for more sophisticated developments. As we move forward, we will refine encapsulate significantly.
Encapsulation and Composition
201
Self Our encapsulation design uses a proxy to ensure that metaobjects are decoupled from the objects and from each other. Stripping away most of our implementation, we take something like this: var hinault = Object.create(null, { _name: { value: 'Bernard Hinault', enumerable: true }, name: { value: function () { return this._name; }, enumerable: true } }); //=> { _name: 'Bernard Hinault', name: [Function] }
And turn it into this: var lemondContext = {_name: 'Greg Lemond'}; var lemond = Object.create(null, { _proxy: { value: proxy, enumerable: lemondContext }, name: { value: function () { return this._name; }.bind(lemondContext), enumerable: true } }); //=> { name: [Function] }
The second version hides its _name property inside its context object. In our full implementation, context is a proxy for the cyclist object, but this simplified code is sufficient for explaining the shortcoming of this design.
Encapsulation and Composition
context !== self Here’s a cycling team: var laVieClaire = { _riders: [], addRider: function (cyclist) { this._riders.push(cyclist); return this; }, riders: function () { return this._riders; }, includes: function (cyclist) { return this._riders.indexOf(cyclist) >= 0; } }
It’s pretty obvious what happens if we add our cyclist to a team: laVieClaire.includes(lemond) //=> false laVieClaire.addRider(lemond); laVieClaire.includes(lemond) //=> true
Now we’ll make an unencapsulated cyclist that can answer whether it belongs to a team: var bauer = Object.create(null, { _name: { value: 'Steve Bauer', enumerable: true }, name: { value: function () { return this._name; }, enumerable: true },
202
Encapsulation and Composition
belongsTo: { value: function (team) { return team.includes(this); }, enumerable: true } }); laVieClaire.addRider(bauer); bauer.belongsTo(laVieClaire) //=> true
What if we encapsulate this new kind of cyclist? var andysContext = {_name: 'Andy Hampsten'}; var hampsten = Object.create(null, { _proxy: { value: andysContext, enumerable: false }, name: { value: function () { return this._name; }.bind(andysContext), enumerable: true }, belongsTo: { value: function (team) { return team.includes(this); }.bind(andysContext), enumerable: true } }); laVieClaire.addRider(hampsten); laVieClaire.includes(hampsten) //=> true hampsten.belongsTo(laVieClaire) //=> false
203
Encapsulation and Composition
204
What happened? Well, let’s look closely at belongsTo’s function: function (team) { return team.includes(this); }.bind(andysContext)
The answer is clear: Like all encapsulated methods, we’re binding it to andysContext so that this always refers to its private data (andysContext) rather than the object (hampsten). The variable this is always bound to the current context, so when we call team.includes(this), we’re asking if the team includes the context, not the cyclist object. Bzzzt! The trouble is that this has two jobs in every function: It’s the context, and by default, it’s also the receiver of the method invocation. JavaScript does not separate these two ideas: If you bind the context of a method, you lose the ability to refer to the receiver of the method.
separating context from selfhood Other languages do not have this problem, because the concept of an object’s private state and its “selfhood” are separate. With a ruby instance method, self is a variable bound to the receiver of the message, and the state of the object is held in “instance variables,” variables whose names are prefixed with @: class Example def setFoo (value) @foo = value end end myExample = Example.new myExample.setFoo(42) myExample.foo #=> NoMethodError: undefined method `foo'
There is no way to access an instance variable through an object: In Ruby, myExample.foo is an attempt to access a method called foo, not the instance variable @foo. While Ruby does not have this, it has a similar variable, self. But you can’t use self to access instance variables within a method, calling self.foo is also attempting to invoke a method:
Encapsulation and Composition
205
class AnotherExample def setFoo (value) @foo = value end def getFoo self.foo end end secondExample = AnotherExample.new secondExample.setFoo(42) secondExample.getFoo() #=> NoMethodError: undefined method `foo'
Ruby thus cleanly separates context (in the form of private instance variables) from selfhood (the self variable). If we want to have private, hidden context and selfhood, weâ&#x20AC;&#x2122;ll also need to separate the ideas. One simple method is to copy what Ruby does, sort of. this.foo is a little like @foo,and this.self is a little like self. This implies, correctly, that you cannot have your own property named self. Hereâ&#x20AC;&#x2122;s the createContext function from encapsulate, modified to support self: function createContext (methodReceiver) { return Object.defineProperty( proxy(methodReceiver), 'self', { writable: false, enumerable: false, value: methodReceiver } ); }
We can use the revamped encapsulate: var laVieClaire = { _riders: [], addRider: function (cyclist) { this._riders.push(cyclist); return this; }, riders: function () { return this._riders; }, includes: function (cyclist) {
Encapsulation and Composition
return this._riders.indexOf(cyclist) >= 0; } } var jeanFrancoisBernard = encapsulate({ belongsTo: function (team) { return team.includes(this.self); } }); laVieClaire.addRider(jeanFrancoisBernard); jeanFrancoisBernard.belongsTo(laVieClaire) //=> true
The peril of enabling this.self in JavaScript is that encapsulated methods now have a “back door” into directly modifying the base object. When we get into composing metaobjects, we’ll work very hard to keep metaobjects from invoking each other’s methods in an unstructured way. this.self could be used to subvert such schemes.
206
Encapsulation and Composition
207
Privacy Our encapsulate function sets up a sharp demarcation: An encapsulated metaobject has one or more methods that are visible to other objects. Those “public” methods can update and query properties, but the properties are hidden from the public. The properties are “private.” This scheme is perfectly cromulent⁹³ for metaobjects that represent small, focused and simple responsibilities. But what if we have a metaobject with complex internal structure and workings? We may, for example, wish to take a code fragment and group it together, giving it a name and making it obvious that it represents a single responsibility. We could turn this:⁹⁴ var Account = encapsulate({ getOutstanding: function () { // ...elided... }, printOwing: function () { this.printBanner(); //print details console.log("name: " + this._name); console.log("amount " + this.getOutstanding()); return this; } // ...elided... });
Into this: var Account = encapsulate({ getOutstanding: function () { // ...elided... }, printOwing: function () { this.printBanner(); this._printDetails(); return this; }, ⁹³https://en.wikipedia.org/wiki/Cromulent#Embiggen_and_cromulent ⁹⁴This example of the Extract Method refactoring comes from Martin Fowler’s excellent Catalogue of Refactorings.
Encapsulation and Composition
208
_printDetails: function () { console.log("name: " + this._name); console.log("amount " + this.getOutstanding()); return this; } // ...elided... });
This works, however _printDetails is public: Even though we use a common naming convention to remind ourselves not to use it externally, we have exposed ourselves to the possibility that in a moment of haste or ignorance, another object may invoke the method. This increases the surface area of our Account metaobject by exposing another method. Furthermore, exposing a method that printOwing itself calls means that its dependents include all of the objects that have method that invoke printDetails directly or invoke it indirectly by invoking printOwning. _printDetails is, thankfully, a query. Consider this similar example featuring a helper method that
performs an update: var Account = encapsulate({ credit: function (amount) { // ...elided... }, debit: function (amount) { // ...elided... }, processCheque: function (cheque) { // ...elided... _transferTo(cheque.depositAccount, cheque.amount); return this; }, _transferTo: function (otherAccount, amount) { this.credit(amount); otherAccount.debit(amount); return this; } // ...elided... });
Encapsulation and Composition
209
_transferTo is also public, and affects the internal state of an account. Therefore, exposing it to the
world immediately couples every piece of code that invokes it with every piece of code that depends upon the state of an account’s balance. The ideal situation is to avoid making such “helper methods” public. For example, we can take advantage of JavaScript’s closures and convert the methods into helper functions: function _transferTo (fromAccount, toAccount, amount) { fromAccount.credit(amount); toAccount.debit(amount); return fromAccount; } var Account = encapsulate({ credit: function (amount) { // ...elided... }, debit: function (amount) { // ...elided... }, processCheque: function (cheque) { // ...elided... _transferTo(this, cheque.depositAccount, cheque.amount); return this; } // ...elided... });
This one works very well because it doesn’t actually use any of an account’s internal private state. But it wouldn’t work if it needed to mutate the balance directly,a s the private state is encapsulated. What if we want to make a method that has access to the encapsulated metaobject’s private state but is not visible outside of the metaobject? Let’s review the architecture. An account looks like this:
210
Encapsulation and Composition
AccountEncapulation
The encapsulate function structures each object so that there is an unenumerated, “hidden” object representing private state, and each method executes in that object’s context. Now we want to have a method that is hidden from the public, but available to each method. Well, the answer writes itself when you ask the question the right way:
Account Private Method
Encapsulation and Composition
211
If we want a private method that is available only to our public methods, we should place it in the object we already have that holds private state for our public methods.
private methods Recall our encapsulate method: 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; } var number = 0; function encapsulate (behaviour) { var safekeepingName = "__" + ++number + "__", encapsulatedObject = {}; function createContext (methodReceiver) { return Object.defineProperty( proxy(methodReceiver), 'self', { writable: false, enumerable: false, value: methodReceiver } ); } function getContext (methodReceiver) { var context = methodReceiver[safekeepingName]; if (context == null) {
Encapsulation and Composition
212
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; }
Note that our proxy function takes an optionalPrototype. Now we can use it, with a little fiddling to scan the method names for private methods: 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;
Encapsulation and Composition
} var number = 0; 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] !== '_'; }); 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 } ); } 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; }
213
Encapsulation and Composition
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; }, {}); } 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; }, constructor: function () { this._careers = []; return this; }, addCareer: function (career) { this._careers.push(career); return this; }, careers: function () { return this._englishList(this._careers); } }); var nilsson = Object.create(MultiTalented).constructor(); nilsson.addCareer('Singer').addCareer('Songwriter').careers() //=> 'Singer and Songwriter' nilsson._englishList //=> undefined
Our new architecture looks like this:
214
215
Encapsulation and Composition
Private method using a prototype for the proxy
When careers invokes this._englishList, the context is our proxy, shown as property __12345__. There is no implementation of _englishList in the proxy itself, but there is in the prototype we construct for it, so invoking it as a method works for any of the public (or other private) methods we define in this behaviour. There are many other ways to implement encapsulation with public and private methods. There are especially many other ways to distinguish public from private methods: Naming conventions are definitely a question of taste. The thing to remember is that the specific design and implementation is not the point, it’s a vehicle for thinking about access control, coupling, design, and the interaction between objects and metaobjects in a software system. “It is like a finger, pointing away to the moon. Don’t concentrate on the finger, or you will miss all that heavenly glory.”—Bruce Lee, recounting a teaching from Buddhism’s Lankavatara Sutra. https://en.wikipedia.org/wiki/Finger_pointing_to_the_moon
.
Encapsulation and Composition
216
Closing Encapsulated Objects There are two things weâ&#x20AC;&#x2122;ve ignored so far. Looking back at our introduction to Metaobjects, we discussed whether metaobjects had forwarding or delegation semantics. We also discussed whether metaobjects were open or closed for extension and we discussed whether the target for forwarding/delegation was early- or late-bound.
closed for modification and extension Our encapsulate function is closed for modification. Letâ&#x20AC;&#x2122;s take another look at it: var number = 0; function encapsulate (behaviour) { var safekeepingName = "__" + ++number + "__", properties = Object.keys(behaviour), methods = properties.filter(function (methodName) { return typeof behaviour[methodName] === 'function'; }), privateMethods = methods.filter(function (methodName) { return methodName[0] === '_'; }), publicMethods = methods.filter(function (methodName) { return methodName[0] !== '_'; }), definedMethods = methodsOfType(behaviour, publicMethods, 'function'), dependencies = methodsOfType(behaviour, properties, 'undefined'), encapsulatedObject = {}; 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 } ); }
Encapsulation and Composition
217
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; } definedMethods.forEach(function (methodName) { var methodBody = behaviour[methodName]; encapsulatedObject[methodName] = function () { var context = getContext(this), result = methodBody.apply(context, arguments); return (result === context) ? this : result; }; }); dependencies.forEach(function (methodName) { if (encapsulatedObject[methodName] == null) { encapsulatedObject[methodName] = void 0; } }); return encapsulatedObject; }
When we use encapsulate to create a metaobject, we associate each of the methods with a partial proxy for this:
218
Encapsulation and Composition
Songwriter
Once the metaobject has been created, there is (deliberately) no obvious way for any other object to access its partial proxy. For this reason, it is difficult to extend a metaobject created by encapsulate. If we try to replace an existing method, the new version will not have access to the proxy. Hereâ&#x20AC;&#x2122;s a (slightly changed) version of Songwriter that completely hides its implementation of a songlist: var Songwriter = encapsulate({ constructor: function () { this._songs = []; return this; }, addSong: function (name) { this._songs.push(name); return this; }, songlist: function () { return this._songs.join(', '); } });
We could try to modify it to produce a more readable songlist:
219
Encapsulation and Composition
function englishList (list) { var butLast = list.slice(0, list.length - 1), last = list[list.length - 1]; return butLast.length > 0 ? [butLast.join(', '), last].join(' and ') : last; } Songwriter.songlist = function () { return englishList(this._songs); }
This wonâ&#x20AC;&#x2122;t work, the replacement method will not access the partial proxy, but instead its context will be this directly:
Songwriter, extended
This is not what we want! Open recursion (unrestricted access to this) re-introduces the coupling we worked so hard to avoid. And, the method wonâ&#x20AC;&#x2122;t work because _songs is a property of the partial proxy, not of the underlying this. The same is true if we tried to add a new method. Encapsulated behaviours are closed for extension as well as being closed for extension.
Encapsulation and Composition
220
This is terrible if we like the free-wheeling “monkey-patching” style of metaprogramming popular in other language communities, but our code is actually doing its job properly by enforcing clearly defined limits on how disparate pieces of code can influence each other’s behaviour.
early warning systems “Everybody Lies”—Dr. Gregory House The one drawback of our code is that it lies: You can add or change a method, but it doesn’t really work. It would be far better to catch these incorrect practices earlier. We can prevent methods from being replaced using Object.defineProperty and prevent new methods from being added with Object.preventExtensions: var number = 0; function encapsulate (behaviour) { var safekeepingName = "__" + ++number + "__", properties = Object.keys(behaviour), methods = properties.filter(function (methodName) { return typeof behaviour[methodName] === 'function'; }), privateMethods = methods.filter(function (methodName) { return methodName[0] === '_'; }), publicMethods = methods.filter(function (methodName) { return methodName[0] !== '_'; }), definedMethods = methodsOfType(behaviour, publicMethods, 'function'), dependencies = methodsOfType(behaviour, properties, 'undefined'), encapsulatedObject = {}; 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 }
Encapsulation and Composition
); } 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; } definedMethods.forEach(function (methodName) { var methodBody = behaviour[methodName]; Object.defineProperty(encapsulatedObject, methodName, { enumerable:: true, writable: false, value: function () { var context = getContext(this), result = methodBody.apply(context, arguments); return (result === context) ? this : result; } }); }); dependencies.forEach(function (methodName) { if (encapsulatedObject[methodName] == null) { encapsulatedObject[methodName] = void 0; } }); return Object.preventExtensions(encapsulatedObject); }
Now if we try to modify Songwriter, it won’t work. If we’re in “strict mode,” we get an error:
221
Encapsulation and Composition
222
var Songwriter = encapsulate({ constructor: function () { this._songs = []; return this; }, addSong: function (name) { this._songs.push(name); return this; }, songlist: function () { return this._songs.join(', '); } }); !function () { "use strict" Songwriter.songlist = function () { return englishList(this._songs) }; }(); //=> TypeError: Cannot assign to read only property 'songlist' of #<Object>
This forces us to build new functionality with composeMetaobjects and using the tools we’ve designed to control coupling. That may seem like it is taking away our control, like we are pandering to the “lowest common denominator” of programmer while hobbling the good programmers, but this is not the case: We aren’t removing a powerful and useful feature because it is “difficult to read,” we’re removing a feature that introduces coupling as part and parcel of adding a feature (composeability for metaobjects) that provides an even-more-powerful set of capabilities.
Encapsulation and Composition
223
Decoupling with Partial Proxies Some objects have multiple responsibilities. A Person can be an Author, can HaveChildren, can HaveInvestments and so forth. Each particular responsibility can be cleanly separated into its own metaobject, and their state combined into one object with techniques like our private behaviours that work with shared prototypes using safekeeping for shared prototypes. This cleanly separates the code we write along lines of responsibility. Encapsulating the base object within a proxy reduces the surface area available for coupling by hiding all private state. But each behaviour has access to all of the object’s methods, and every responsibility we add swells this set of methods, increasing the surface area again. We saw in Encapsulation for Metaobjects that we can use proxies to prevent metaobjects from manipulating the base object’s properties. Now we’re going to explicitly limit the methods that each metaobject can access on the base object. This lowers the coupling between metaobjects. Let’s start by recalling a basic version of the encapsulate function:⁹⁵ 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) { ⁹⁵We will deliberately leave out some refinements such as self and private methods to highlight the ideas we’re exploring here.
Encapsulation and Composition
224
var methodBody = behaviour[methodName]; encapsulatedObject[methodName] = function () { var context = getContext(this), result = description[methodName].apply(context, arguments); return (result === context) ? this : result; }; }); return encapsulatedObject; }
The key line of code is context = proxy(methodReceiver);. This lazily attaches a proxy of the method receiver to a “hidden” property. Thereafter, every method of that behaviour invoked on the same receiver will use the same context. Thus, the functions of a behaviour do not have access to the receiver’s properties. In addition, since each behaviour gets its own proxy, behaviours cannot access each other’s properties either. However, proxy exposes all of the receiver’s methods to every behaviour, including methods that other behaviours add. This means that behaviours can invoke each other’s methods and become interdependent. This is almost never what we want. If we’re trying to decouple behaviours from each other, we want to strictly limit the methods exposed to each behaviour. Typically, there are two cases. First, a behaviour may provide completely independent behaviour: It does not rely on the base object’s properties or methods. In these examples, HasName and HasCareer are self-contained behaviours. They update and queries its own private state, and do not invoke any other methods. Both use a property called value, but if they re both mixed into the same base object, they’ll each get their own property and this each have their own value property: var HasName = encapsulate({ name: function () { return this.name; }, setName: function (name) { this.name = name; return this; } }); var HasCareer = encapsulate({ career: function () {
Encapsulation and Composition
225
return this.name; }, setCareer: function (name) { this.name = name; return this; } });
In contrast, IsSelfDescribing is a behaviour that depends on the receiver implementing both the name() and career() methods. We know this from inspecting the code, and fortunately it is a small and simple behaviour: var IsSelfDescribing = encapsulate({ description: function () { return this.name() + ' is a ' + this.career(); } });
If we program in this style, writing behaviours that have dependencies hidden inside the code, we will introduce a lot of coupling, with behaviours becoming dependent upon each other in an unstructured way.
managing dependencies Behavioursâ&#x20AC;&#x201C;like IsSelfDescribingâ&#x20AC;&#x201C;that depend upon the base object or another behaviour implementing methods are said to stack upon other behaviours.
Encapsulation and Composition
226
IsSelfDescribing stacked on HasName and HasCareer
We can manage these dependencies by making dependencies explicit instead of implicit. First, we’ll express the dependencies using a simple convention: If a behaviour depends upon a method, it must bind undefined to the name of the method. This is how we’ll express this for IsSelfDescribing: var IsSelfDescribing = encapsulate({ name: undefined, career: undefined, description: function () { return this.name() + ' is a ' + this.career(); } });
The way we’ll enforce this requirement is that we will no longer provide every behaviour with a proxy for all of the object’s methods. Instead, we’ll only expose the methods that the behaviour defines publicly or explicitly requires.
Encapsulation and Composition
227
To do that, weâ&#x20AC;&#x2122;ll write the function partialProxy. It works like proxy, but instead of iterating over the base objectâ&#x20AC;&#x2122;s properties, it iterates over a subset we provide. We will also rewrite extendsWithProxy to use partialProxy and the declared dependencies: function partialProxy (baseObject, methods) { var proxyObject = Object.create(null); methods.forEach(function (methodName) { proxyObject[methodName] = function () { var result = baseObject[methodName].apply(baseObject, arguments); return (result === baseObject) ? proxyObject : result; } }); return proxyObject; } var number = 0; function methodsOfType (behaviour, type) { var methods = [], methodName; for (methodName in behaviour) { if (typeof(behaviour[methodName]) === type) { methods.push(methodName); } }; return methods; } function encapsulate (behaviour) { var safekeepingName = "__" + ++number + "__", definedMethods = methodsOfType(behaviour, 'function'), dependencies = methodsOfType(behaviour, 'undefined'), encapsulatedObject = {}; function createContext (methodReceiver) { return partialProxy(methodReceiver, dependencies); } function getContext (methodReceiver) {
Encapsulation and Composition
228
var context = methodReceiver[safekeepingName]; if (context == null) { context = createContext(methodReceiver); Object.defineProperty(methodReceiver, safekeepingName, { enumerable: false, writable: false, value: context }); } return context; } definedMethods.forEach(function (methodName) { var methodBody = behaviour[methodName]; encapsulatedObject[methodName] = function () { var context = getContext(this), result = methodBody.apply(context, arguments); return (result === context) ? this : result; }; }); return encapsulatedObject; }
Our createContext function now calls partialProxy instead of proxy. We can try it with HasName, HasCareer, and IsSelfDescribing: var Careerist = extend({}, HasName, HasCareer, IsSelfDescribing); var michael = Object.create(Careerist), bewitched = Object.create(Careerist); michael.setName('Michael Sam'); bewitched.setName('Samantha Stephens'); michael.setCareer('Athlete'); bewitched.setCareer('Thaumaturge'); michael.description() //=> 'Michael Sam is a Athlete' bewitched.description() //=> 'Samantha Stephens is a Thaumaturge'
Encapsulation and Composition
229
We have the same behaviour as before, but we’ve required that behaviours explicitly declare the methods the depend on. This prevents coupling from sneaking up on us accidentally, and it makes it easier to see the coupling when we’re reviewing the code.
why this matters To summarize, instead of every behaviour having access to all of the receiver’s methods, each behaviour only has access to methods it explicitly depends on. This helps by documenting each behaviour’s dependencies and by eliminating accidental coupling between behaviours.
Encapsulation and Composition
230
Composing Metaobjects Up to now, weâ&#x20AC;&#x2122;ve combined behaviours in an object (such as to create a prototype) using extend to forcibly copy the methods from our behaviours into an empty object. The core of extend is this block: for (key in provider) { if (Object.prototype.hasOwnProperty.call(provider, key)) { consumer[key] = provider[key]; }; };
We evaluate consumer[key] = provider[key]; for each method in our behaviour. This has the desired effect when weâ&#x20AC;&#x2122;re composing two behaviours that have a disjoint set of methods. But if they both have a method with the same name, one will overwrite the other. This is almost never the right thing to do. If you think about composition, what we want is that if we have a behaviour A and a behaviour B, we want the behaviour AB to be both A and B. If we have some expectation about the behaviour of A, we should have the same expectation of AB. If A and B both define a method, m(), then calling AB.m() should be equivalent to calling A.m(). This is not the case if B.m overwrites A.m. Likewise AB.m() should also be equivalent to calling B.m(). We want AB.m() to be equivalent to both A.m() and B.m(). For example: var SingsSongs = encapsulate({ _songs: null, constructor: function () { this._songs = []; return this; }, addSong: function (name) { this._songs.push(name); return this; }, songs: function () { return this._songs; } }); var HasAwards = encapsulate({
Encapsulation and Composition
231
_awards: null, constructor: function () { this._awards = []; return this; }, addAward: function (name) { this._awards.push(name); return this; }, awards: function () { return this._awards; } }); var AwardWinningSongwriter = extend(Object.create(null), SingsSongs, HasAwards), tracy = Object.create(AwardWinningSongwriter).constructor(); tracy.songs() //=> undefined HasAwards.constructor has overwritten SingsSongs.constructor, exactly what we said it should
not do.
232
Encapsulation and Composition
Composeable Espresso Cups
composing methods We know a little something about composing methods from Method Objects. One way to compose two methods is to evaluate one and then the other. Hereâ&#x20AC;&#x2122;s what a very simple version looks like for functions. Itâ&#x20AC;&#x2122;s a protocol:
Encapsulation and Composition
233
function simpleProtocol (fn1, fn2) { return function composed () { fn1.apply(this, arguments); return fn2.apply(this, arguments); } }
Using this formulation, we can say that given A.m and B.m, then AB.m = simpleProtocol(A.m, B.m). We can promote this idea to composing metaobjects. First, we generalize simpleProtocol to handle an number of functions and the degenerate case of one function. Weâ&#x20AC;&#x2122;ll call our generalization orderProtocol. We can then write composeMetaobjects to apply this to encapsulated methods: var __slice = [].slice; function isUndefined (value) { return typeof value === 'undefined'; } function isntUndefined (value) { return typeof value !== 'undefined'; } function isFunction (value) { return typeof value === 'function'; } function orderProtocol () { if (arguments.length === 1){ return arguments[0]; } else { var fns = arguments; return function composed () { for (var i = 0; i < (fns.length - 1); ++i) { fns[i].apply(this, arguments); } return fns[fns.length - 1].apply(this, arguments); } } }
Encapsulation and Composition
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; }, {}) } 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; }, {}); } function applyProtocol(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; }, {});
234
Encapsulation and Composition
235
} function composeMetaobjects { var metaobjects = __slice.call(arguments, 0), arrays = propertiesToArrays(metaobjects), resolved = resolveUndefineds(arrays), composed = applyProtocol(resolved, orderProtocol); return composed; }
Letâ&#x20AC;&#x2122;s revisit our example from above, but this time weâ&#x20AC;&#x2122;ll composeMetaobjects our metaobjects instead of extending an object: var SingsSongs = encapsulate({ _songs: null, constructor: function () { this._songs = []; return this; }, addSong: function (name) { this._songs.push(name); return this; }, songs: function () { return this._songs; } }); var HasAwards = encapsulate({ _awards: null, constructor: function () { this._awards = []; return this; }, addAward: function (name) { this._awards.push(name); return this; }, awards: function () {
Encapsulation and Composition
236
return this._awards; } }); var AwardWinningSongwriter = composeMetaobjects(SingsSongs, HasAwards), tracy = Object.create(AwardWinningSongwriter).constructor(); tracy.addSong('Fast Car'); tracy.songs() //=> [ 'Fast Car' ]
Now AwardWinningSongwriter.constructor does exactly what we want it to do.
return value protocols Our orderProtocol is very simple. But there are some cases it doesn’t handle well. Sometimes we want to decorate a method with some behaviour, but not take over the return value. This often happens when we have two responsibilities, but they are not “peers:” One is a primary responsibility, while the other is a secondary and lesser responsibility. For example, a Songwriter’s responsibility is to manage its songs: var Songwriter = encapsulate({ constructor: function () { this._songs = []; return this.self; }, addSong: function (name) { this._songs.push(name); return this.self; }, songs: function () { return this._songs; } });
Secondary responsibilities are often orthogonal concerns. Waving aside ECMAScript-6’s Object.observe⁹⁶ for a moment, we could design a program that has views that are notified when models that delegate to Songwriter are changed. We would start with the basic idea that something like a view could “subscribe” to a model that is a Subscribable: ⁹⁶http://wiki.ecmascript.org/doku.php?id=harmony:observe
Encapsulation and Composition
237
var Subscribable = encapsulate({ constructor: 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); }); } });
A SubscribableSongwriter would obviously be composeMetaobjects(Songwriter, Subscribable). But in order for subscribing to be useful, we want notify() to be called whenever we call addSong(). We can do that by adding more behaviour to addSong: var SubscribableSongwriter = composeMetaobjects( Songwriter, Subscribable, encapsulate({ notify: undefined, addSong: function () { this.notify(); } }) ); var sweetBabyJames = Object.create(SubscribableSongwriter).constructor();
The third metaobject, encapsulate({notify: undefined, addSong: function () { this.notify(); }}), appends a behaviour to addSong that calls notify() when a song is added. We can see that it works using this primitive “view:”
Encapsulation and Composition
238
var SongwriterView = { constructor: 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; } }; var jamesView = Object.create(SongwriterView).constructor(sweetBabyJames, 'James \ Taylor'); sweetBabyJames.addSong('Fire and Rain') //=> James Taylor has written 'Fire and Rain' undefined
Or did it work? Letâ&#x20AC;&#x2122;s look at Songwriter.addSong: function (name) { this._songs.push(name); return this.self; }
It is supposed to return this.self, but it actually returns undefined, because it is returning whatever the last addSong behavior returns. The last behaviour is function () { this.notify(); }, and that doesnâ&#x20AC;&#x2122;t return anything at all, so we get undefined.
Encapsulation and Composition
239
This is almost never what we want. We could “fix” function () { this.notify(); } to return this.self, but the larger issue is that function () { this.notify(); } is a secondary responsibility. It shouldn’t know what is to be returned. What we want is to be able to compose behaviour, but make responsibility for the return value optional. We’ll do that with a simple convention: Not returning a value, or returning undefined, advertises that a function does not have responsibility for the return value. We can implement this with a simple change to our ordering strategy: var __slice = [].slice; function isntUndefined (value) { return typeof value !== 'undefined'; } function orderProtocol2 () { 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]; } } } } function composeMetaobjects { var metaobjects = __slice.call(arguments, 0), = propertiesToArrays(metaobjects), arrays resolved = resolveUndefineds(arrays), composed = applyProtocol(resolved, orderProtocol2); return composed;
Encapsulation and Composition
240
} var CorrectSubscribableSongwriter = composeMetaobjects( Songwriter, Subscribable, encapsulate({ notify: undefined, addSong: function () { this.notify(); } }) ); var sweetBabyJames = Object.create(CorrectSubscribableSongwriter).constructor(); var jamesView = Object.create(SongwriterView).constructor(sweetBabyJames, 'James \ Taylor'); sweetBabyJames.addSong('Fire and Rain') === sweetBabyJames //=> James Taylor has written 'Fire and Rain' true
Success! It now returns the original value, the receiver, as we intended. One consequence of returning the last non-undefined value is that it is possible to want to return undefined but to have that value usurped by another behaviour that returns something other than undefined. In practice, this is rare, but if it does arise it will be necessary to work around this limitation or to change strategies.
specialization and prototype chains Our scheme for decorating methods with additional behaviour like addSong: function () { this.notify(); } works perfectly when we’re composing behaviour. This works in concert with prototypes when we’re building a single prototype. What this scheme can’t do is compose behaviour across prototypes. Although we prefer to avoid creating elaborate prototype chains, let’s presume that we have an excellent reason for wanting to create an Songwriter prototype, and also create a SubscribableSongwriter prototype that delegates to Songwriter. In other words:
241
Encapsulation and Composition
A small prototype chain
Now we want to make a SubscribableSongwriter that delegates to Songwriter. There is a trick for making behaviour that delegates to a prototype: We compose the behaviour we want with an object that delegates to the desired prototype. In effect, weâ&#x20AC;&#x2122;re composing the delegation with the behaviour we want. Weâ&#x20AC;&#x2122;ll need to adjust applyProtocol to start with a seed that can be passed in with a prototype correctly set: 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;
Encapsulation and Composition
242
return applied; }, seed); }
Weâ&#x20AC;&#x2122;ll also have to change composeMetaobjects to generate the correct seed (and while weâ&#x20AC;&#x2122;re at it, check that everything being composed have compatible prototypes): 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); } // shim if allong.es.callLeft not available 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); }; }; } })(); function seedFor (objectList) { var seed = objectList[0] == null ? Object.create(null) : Object.create(Object.getPrototypeOf(objectList[0])), isCompatibleWithSeed = callLeft2(canBeMergedInto, seed);
Encapsulation and Composition
if (!objectList.every(isCompatibleWithSeed)) throw 'incompatible prototypes'; return seed; } function composeMetaobjects { var metaobjects = __slice.call(arguments, 0), arrays = propertiesToArrays(metaobjects), resolved = resolveUndefineds(arrays), seed = seedFor(metaobjects), = applyProtocol(seed, resolved, orderProtocol2); composed return composed; }
We have what we need to build our object: var Songwriter = encapsulate({ constructor: function () { this._songs = []; return this.self; }, addSong: function (name) { this._songs.push(name); return this.self; }, songs: function () { return this._songs; } }); var Subscribable = encapsulate({ constructor: 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;
243
Encapsulation and Composition
244
}); }, subscribers: function () { return this._subscribers; }, notify: function () { receiver = this; this._subscribers.forEach( function (subscriber) { subscriber.apply(receiver.self, arguments); }); } }); var SubscribableSongwriter = composeMetaobjects( Object.create(Songwriter), Subscribable, encapsulate({ notify: undefined, addSong: function () { this.notify(); } }) ); var SongwriterView = { constructor: 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 + "'"; }))] : [];
Encapsulation and Composition
245
console.log(this.name + songList); return this; } }; var paulSimon = Object.create(SubscribableSongwriter).constructor(), paulView = Object.create(SongwriterView).constructor(paulSimon, 'Paul Simon'\ ); paulSimon.addSong('Cecilia') //=> Paul Simon has written 'Cecilia' {} paulSimon.songs() //=> [ 'Cecilia' ]
Why would we do this? Well, for certain styles of programming, we care very much what prototypes are in an object’s prototype chain. Note: Songwriter.isPrototypeOf(sweetBabyJames) //=> false Songwriter.isPrototypeOf(paulSimon) //=> true
When we compose behaviours directly, we lose the ability to track their inclusion in the prototype chain. There is no baked-in way to ask whether an object includes the behaviour from AwardWinningSongwriter. So why don’t we always wire behaviours up with prototype chains? As we discussed earlier, prototype chains can only model trees, while composing behaviours provides much more freedom to mix and match compact, focused responsibilities. When we choose model our behaviour with prototypes, it is still advantageous to use our model for composing behaviour. A naïve prototype chain with methods embedded directly in the prototypes has open recursion and suffers from coupling. Using encapsulated behaviours with prototypes decouples prototypes from their objects and from each other.
Encapsulation and Composition
246
Transforming and Decorating Metaobjects Going back to the Big Idea, there is value in having an algebra of metaobjects. We’ve just seen a protocol for encapsulating and composing metaobjects: Encapsulation works with composition to decouple metaobjects from each other, which in turn makes it easy to combine and recombine them as needed to form new metaobjects. Composition is not the only mechanism for building metaobjects from other metaobjects. We can also transform metaobjects into new metaobjects. We’ve already seen one such transformation: encapsulate transformed an open metaobject into an encapsulated metaobject.
decorating methods Here’s another function that transforms a metaobject into another metaobject: function fluentByDefault (metaobject) { var metaproto = Object.getPrototypeOf(metaobject), fluentMetaobject = Object.create(metaproto), key; for (key in metaobject) { if (typeof metaobject[key] === 'function') { !function (key) { fluentMetaobject[key] = function () { var returnValue = metaobject[key].apply(this, arguments); return typeof returnValue === 'undefined' ? this : returnValue; } }(key); } } return fluentMetaobject; }
This takes a metaobject and returns another metaobject. The new metaobject delegates all of its methods to the original, but its return value protocol is decorated: any method that returns undefined will return this instead. Note that we are not modifying a metaobject in place. Consider:
Encapsulation and Composition
247
var Songwriter = encapsulate({ constructor: function () { this._songs = []; }, addSong: function (name) { this._songs.push(name); }, songs: function () { return this._songs; } }); var FluentSongwriter = fluentByDefault(Songwriter); var hall = Object.create(Songwriter), oates = Object.create(FluentSongwriter); hall.constructor(); //=> undefined oates.constructor(); //=> {} hall.addSong("your kiss is on my list"); //=> undefined oates.addSong("your kiss is on my list"); //=> {}
Creating FluentSongwriter by transforming Songwriter does not modify Songwriter, it retains its behaviour and meaning. This differs from practice in some language communities, where it is commonplace to modify metaobjects in place. As we discussed in Closing Encapsulated Objects, encapsulated objects like Songwriter cannot be modified in place, but a naĂŻve metaobject could easily be mutated: var NaiveSongwriter = { constructor: function () { this._songs = []; }, addSong: function (name) { this._songs.push(name); }, songs: function () { return this._songs;
Encapsulation and Composition
248
} }; // ... var oldInitialize = NaiveSongwriter.constructor, oldAddSong = NaiveSongwriter.addSong; NaiveSongwriter.constructor = function () { oldInitialize.call(this); return this; } NaiveSongwriter.addSong = function (song) { oldAddSong.call(this, song); return this; }
This goes completely against the idea of an “algebra:” Modifying a metaobject couples the code that modifies the metaobject to every object that might use it. On a small scale this is manageable. But people have adopted this practice in languages like Ruby, and their experience with large-scale programs has produced decidedly mixed reviews. On the whole, it is decidedly better to transform metaobjects by creating new metaobjects from them that may delegate to the original.
selective transformations Our fluentByDefault transformer attempts to make all methods fluent. We can be more selective. Here’s a version that takes a metaobject and one or more “selectors” and then makes the methods matching teh selectors into methods that are fluent by default: function selectorize (selectors) { var fns = selectors.map(function (selector) { var regex; if (typeof selector === 'string' && selector[0] === '/') { regex = new RegExp(selector.substring(1, selector.length-1)); return function (name) { return !!name.match(regex); }; } else if (typeof selector === 'string') { return function (name) { return name === selector; };
Encapsulation and Composition
249
} else if (typeof selector === 'function') { return selector } }); return function select (name) { return fns.some(function (test) { return test.call(this, name) }, this); } } function fluentByDefault (metaobject) { var selectors = [].slice.call(arguments, 1), selectorFn = selectorize(selectors), metaproto = Object.getPrototypeOf(metaobject), fluentMetaobject = Object.create(metaproto), key; for (key in metaobject) { if (typeof metaobject[key] === 'function' && selectorFn.call(metaobject, key)\ ) { !function (key) { fluentMetaobject[key] = function () { var returnValue = metaobject[key].apply(this, arguments); return typeof returnValue === 'undefined' ? this : returnValue; } }(key); } else fluentMetaobject[key] = metaobject[key]; } return fluentMetaobject; } var Songwriter = encapsulate({ constructor: function () { this._songs = []; }, addSong: function (name) { this._songs.push(name);
Encapsulation and Composition
250
}, songs: function () { return this._songs; } }); var FluentSongwriter = fluentByDefault(Songwriter, "initialize"); var hall = Object.create(Songwriter), oates = Object.create(FluentSongwriter); hall.constructor(); //=> undefined oates.constructor(); //=> {} hall.addSong("your kiss is on my list"); //=> undefined oates.addSong("your kiss is on my list"); //=> undefined
This is a lot of work just to make things fluent. Letâ&#x20AC;&#x2122;s extract an inner helper function: function fluentByDefault (metaobject) { var selectors = [].slice.call(arguments, 1), selectorFn = selectorize(selectors), metaproto = Object.getPrototypeOf(metaobject), decoratedMetaobject = Object.create(metaproto), decorator = function (methodBody) { return function () { var returnValue = methodBody.apply(this, arguments); return typeof returnValue === 'undefined' ? this : returnValue; } }, key; for (key in metaobject) { if (typeof metaobject[key] === 'function' && selectorFn.call(metaobject, key)\ ) {
Encapsulation and Composition
251
decoratedMetaobject[key] = decorator(metaobject[key]); } else decoratedMetaobject[key] = metaobject[key]; } return decoratedMetaobject; }
And now we can extract the decorator parameter: function decorate (decorator, metaobject) { var selectors = [].slice.call(arguments, 2), selectorFn = selectorize(selectors), metaproto = Object.getPrototypeOf(metaobject), decoratedMetaobject = Object.create(metaproto), key; for (key in metaobject) { if (typeof metaobject[key] === 'function' && selectorFn.call(metaobject, key)\ ) { decoratedMetaobject[key] = decorator(metaobject[key]); } else decoratedMetaobject[key] = metaobject[key]; } return decoratedMetaobject; }
Our new decorate transformer can perform all kinds of decorations. We can borrow an idea from our method resolvers (it makes a great method decorator): function after (decoration) { return function (methodBody) { return function () { var methodBodyResult = methodBody.apply(this, arguments), decorationResult = decoration.apply(this, arguments); return typeof methodBodyResult === 'undefined' ? decorationResult : methodBodyResult; }; } }
Encapsulation and Composition
252
var logAfter = after(function () { console.log('after!'); }); var LoggingSongwriter = decorate(logAfter, Songwriter, 'addSong'); var taylor = Object.create(LoggingSongwriter); taylor.constructor(); taylor.addSong("fire and rain"); //=> after! undefined
It’s easy to extend this idea to include before, and around decorations: function before (decoration) { return function (methodBody) { return function () { var decorationResult = decoration.apply(this, arguments), methodBodyResult = methodBody.apply(this, arguments) return typeof methodBodyResult === 'undefined' ? decorationResult : methodBodyResult; }; } } function around (decoration) { return function (methodBody) { return function () { return decoration.apply(this, [methodBody].concat([].slice.call(arguments, \ 0))); }; } }
We can thus make new metaobjects out of old ones with functions that selectively decorate one or more of the metaobject’s methods.
the elements of decoupling style This simple idea of writing a metaobject transformer is a direct analogue to writing a functional decorator. We are creating a new metaobject that is semantically similar to the old one, but without “monkey-patching” or mutating the existing metaobject.
Encapsulation and Composition
253
The monkey-patching style works fine on many small projects, but as we scale up, we run into problems. Code will depend upon the original metaobject, and thus a change to the new code may break the old code.
inheritance and decoration
Encapsulation and Composition
254
Playing Well with Classes Throughout our examples of encapsulating and composing metaobjects, we have shown that they are still objects and can be used as prototypes. To recap, all of these idioms work: // an unencapsulated metaobject var PlainSongwriterMetaobject = { constructor: function () { this._songs = []; return this.self; }, addSong: function (name) { this._songs.push(name); return this.self; }, songs: function () { return this._songs; } }; var tracy = Object.create(PlainSongwriterMetaobject) tracy.constructor(); // an encapsulated metaobject var SongwriterMetaobject = encapsulate({ constructor: function () { this._songs = []; return this.self; }, addSong: function (name) { this._songs.push(name); return this.self; }, songs: function () { return this._songs; } }); var tracy = Object.create(SongwriterMetaobject) tracy.constructor();
Encapsulation and Composition
255
// composed metaobjects var HasAwardsMetaobject = encapsulate({ _awards: null, constructor: function () { this._awards = []; return this; }, addAward: function (name) { this._awards.push(name); return this; }, awards: function () { return this._awards; } }); var AwardWinningSongwriterMetaobject = composeMetaobjects(SongwriterMetaobject, H\ asAwardsMetaobject), tracy = Object.create(AwardWinningSongwriterMetaobject); tracy.constructor();
metaobjects and constructors As we’ve discussed, many JavaScript programs use the following idiom to implement “classes:” function Songwriter () { this._songs = []; } Songwriter.prototype = { addSong: function (name) { this._songs.push(name); return this.self; }, songs: function () { return this._songs; } }
256
Encapsulation and Composition
var tracy = new Songwriter();
Its defining features are: 1. You call it like a function with the new keyword; 2. Behaviour is kept in its prototype property. We can manually make such a “class” by wrapping a function around a metaobject: function AwardWinningSongwriter () { AwardWinningSongwriterMetaobject.constructor.apply(this, arguments); } AwardWinningSongwriter.prototype = AwardWinningSongwriterMetaobject;
We can automate this idiom. This Newable function takes an optional name and a metaobject, and returns a “class” that works with the new keyword: function Newable (optionalName, metaobject) { var name = typeof(optionalName) === 'string' ? optionalName : '', metaobject
= arguments.length > 1 ? metaobject : optionalName,
constructor = (metaobject.constructor || function () {}), source = "(function " + name + " () { " + "var r = constructor.apply(this, arguments); " + "return r === undefined ? this : r; " + "})", clazz = eval(source); clazz.prototype = extend({}, metaobject); delete clazz.prototype.constructor; return clazz; } var AwardWinningSongwriter = Newable(AwardWinningSongwriterMetaobject); // or Newable("AwardWinningSongwriter", AwardWinningSongwriterMetaobject); var tracy = new AwardWinningSongwriter();
257
Encapsulation and Composition
We can chain these classes to create a “class hierarchy” if we so desire. Most of the work is concerned with chaining constructor functions: function Newable (optionalName, metaobject, optionalSuper) { var name = typeof(optionalName) === 'string' ? optionalName : '', metaobject
= typeof(optionalName) === 'string' ? metaobject : optionalName,
superClazz
= typeof(optionalName) === 'string' ? optionalSuper : metaobject,
source = "(function " + name + " () { " + "var r = constructor.apply(this, arguments); " + "return r === undefined ? this : r; " + "})", clazz = eval(source), constructor; if (typeof(metaobject.constructor) === 'function' && typeof(optionalSuper) === \ 'function') { constructor = function () { optionalSuper.apply(this, arguments); return metaobject.constructor.apply(this, arguments); } } else if (typeof(metaobject.constructor) === 'function') { constructor = metaobject.constructor; } else if (typeof(optionalSuper) === 'function') { constructor = optionalSuper; } else constructor = function () {} clazz.prototype = extend(Object.create(optionalSuper.prototype), metaobject); delete clazz.prototype.constructor; return clazz; }
Now we can formulate a different kind of AwardWinningSongwriter, one that wires up the behaviour from HasAwardsMetaobject and “extends” the constructor Songwriter:
Encapsulation and Composition
258
var Songwriter = Newable('Songwriter', SongwriterMetaobject); var AwardWinningSongwriter = Newable('AwardWinningSongwriter', HasAwardsMetaobjec\ t, Songwriter); var tracy = new AwardWinningSongwriter(); tracy.addSong('Fast Car'); tracy.addAward('Grammy'); tracy.songs(); //=> [ 'Fast Car' ] tracy.awards(); //=> [ 'Grammy' ]
Of course, we can have Newable extend any constructor function it likes, including those created with ES6’s class keyword. And other “classes” can extend the functions created by Newable. Thus, we see that the metaobjects we create are fully compatible with the existing idioms we may encounter is a code base.
Inheritance, Ontologies, and Semantic Types
⁹⁷ ⁹⁷Royal Java Distributed by the Boston Public Library, no known rights.
Inheritance, Ontologies, and Semantic Types
260
Class Hierarchies In theory, JavaScript does not have classes. It has “prototypes,” and it is always entertaining to lurk in a programming forum and watch peopel debate the exact definition of the word “class” in computer science, and then debate whether JavaScript has them. We’ve been careful to use the word “metaobject” in this book, mostly because “metaobject” isn’t encumbered with baggage such as the instanceof keyword or specific rules about inheritance and creation of instances. In practice, the following snippet of code is widely considered to be an example of a “class” in JavaScript: function Account () { this._currentBalance = 0; } Account.prototype.balance = function () { return this._currentBalance; } Account.prototype.deposit = function (howMuch) { this._currentBalance = this._currentBalance + howMuch; return this; } // ... var account = new Account();
The pattern can be extended to provide the notion of subclassing: function ChequingAccount () { Account.call(this); } ChequingAccount.prototype = Object.create(Account.prototype); ChequingAccount.prototype.sufficientFunds = function (cheque) { return this._currentBalance >= cheque.amount(); } ChequingAccount.prototype.process = function (cheque) { this._currentBalance = this._currentBalance - cheque.amount();
261
Inheritance, Ontologies, and Semantic Types
return this; }
We’ve been calling this pattern “Delegating to Prototypes,” but let’s call them classes and subclasses so that we can align our investiagtion with popular literature. These “classes and subclasses” provide most of the features of classes we find in languages like Smalltalk⁹⁸: • Classes are responsible for creating objects and initializing them with properties (like the current balance); • Classes are responsible for and “own” methods, Objects delegate method handling to their classes (and superclasses); • Methods directly manipulate an object’s properties. Smalltalk was, of course, invented forty years ago⁹⁹. In those forty years, we’ve learned a lot about what works and what doesn’t work in object-oriented programming. Unfortunately, this pattern celebrates the things that don’t work, and glosses over or omits the things that work.
the semantic problem with hierarchies At a semantic level, classes are the building blocks of an ontology¹⁰⁰. This is often formalized in a diagram:
An ontology of accounts
The idea behind class-based OO is to classify (note the word) our knowledge about objects into a tree. At the top is the most general knowledge about all objects, and as we travel down the tree, we get more and more specific knowledge about specific classes of objects, e.g. objects representing Visa Debit accounts. Only, the real world doesn’t work that way. It really doesn’t work that way. In morphology¹⁰¹, for example, we have penguins, birds that swim. And the bat, a mammal that flies. And monotremes¹⁰² like the platypus, an animal that lays eggs but nurses its young with milk. ⁹⁸http://www.squeak.org ⁹⁹http://worrydream.com/EarlyHistoryOfSmalltalk ¹⁰⁰https://en.wikipedia.org/wiki/Ontology_ ¹⁰¹https://en.wikipedia.org/wiki/Morphology_ ¹⁰²https://en.wikipedia.org/wiki/Monotreme
Inheritance, Ontologies, and Semantic Types
262
It turns out that our knowledge of the behaviour of non-trivial domains (like morphology or banking) does not classify into a nice tree, it forms a directed acyclic graph. Or if we are to stay in the metaphor, it’s a thicket. Furthermore, the idea of building software on top of a tree-shaped ontology would be suspect even if our knowledge fit neatly into a tree. Ontologies are not used to build the real world, they are used to describe it from observation. As we learn more, we are constantly updating our ontology, sometimes moving everything around. In software, this is incredibly destructive: Moving everything around breaks everything. In the real world, the humble Platypus does not care if we rearrange the ontology, because we didn’t use the ontology to build Australia, just to describe what we found there. If we started by thinking that Platypus must be a flightless Avian, then later decide that it is a monotreme¹⁰³, we aren’t distrurbing any platypi at all with our rearranging the ontology. Whereas with classes and subclasses, if we rearrange our hierarchy, we are going to have to break and then mend our existing code. It’s sensible to build an ontology from observation of things like bank accounts. That kind of ontology is useful for writing requirements, use cases, tests, and so on. But that doesn’t mean that it’s useful for writing the code that implements bank accounts. Class Hierarchies are often the wrong semantic model, and the wisdom of forty years of experience with them is that there are better ways to compose programs.
how does encapsulation help? As we’ve discussed repeatedly, naïve classes suffer from open recursion¹⁰⁴. An enrmous amount of coupling takes place between classes in a class hierarchy when each class has full access to an object’s properties through the this keyword. In Encapsulation for MetaObjects, we looked at one possible way to isolate classes from each other, reducing the amount of coupling. If coupling is dramatically reduced throgh the use of techniques like encapsulation, we can look at a class hierarchy strictly as an excercise in getting the semantics correct. ¹⁰³https://en.wikipedia.org/wiki/Monotreme ¹⁰⁴https://en.wikipedia.org/wiki/Open_recursion#Open_recursion
Inheritance, Ontologies, and Semantic Types
263
Getting the Semantics Right Let’s talk about modelling students at a university. Here are a few things we might need to model: • Students pursue a degree program such as “Pre-Med,” “Law,” “Pharmacy,” or “Nursing.” • Students attend on a part-time or full-time schedule. Should there be a Student class? Certainly. What about subclassing Student with PreMedStudent, LawStudent, PharamcyStudent, and NursingStudent? We use thes eterms in everyday speech, and there are certain behaviours we expect to differ between them. Furthermore, we can expect that we do not have a “many-to-many” relationship between students and programs. And yet, we could also set up the program as a “has-a” relationship. Maybe each Student has a program property, and delegates behaviour to its program. Likewise… PartTime and FullTime are disjoint, a student is one or the other but not both. So we could have a PartTimeStudent and a FullTimeStudent. But what if we decide that we also want to have PreMedStudent, LawStudent, PharamcyStudent, and NursingStudent? Are we supposed to set up a two-level hierarchy, with classes like PartTimeNursingStudent and a FullTimeNursingStudent both delegating to NursingStudent, and NursingStudent delegating to Student? And why not the other way around, e.g. PharmacyPartTimeStudent and NursingPartTimeStudent both delegating to PartTimeStudent that in turn delegates to Student? or likewise, why not make schedule a property, and delegate behaviour to it so that each Student has-a Schedule? This a strong hint that the program and schedule behaviours should not be placed in a hierarchy with each other: They seem to be peers. They could be implemented by composing metaobjects into prototypes or with delegation to properties. Now let’s consider this entirely made-up case, salespeople in a company: • • • • •
There are inside and outside salespeople. Inside salespeople work on Major Accounts or Small/Medium Business (“SMB”) Outside salespeople sell to all customers. Inside salespeople sell the full range of products. Outside salespeople sell electronic components, computer peripherals, or test equipment.
Now there is a natural hierarchy: • Salesperson – InsideSalesperson * MajorAccountsInsideSalesperson * SmallMediumBusinessInsideSalesperson
Inheritance, Ontologies, and Semantic Types
264
– OutsideSalesperson * ElectronicComponentsOutsideSalesperson * ComputerPeripheralsOutsideSalesperson * TestEquipmentOutsideSalesperson This follows from the fact that the variations on an inside salesperson are disjoint from the variations on an outside salesperson. Does this mean that we must organize prototypes into a “class hierarchy?” Not necessarily. There are two major factors affecting our choice: First, ease of rearrangement vs. likelihood of needing to rearrange: Even with encapsulation, class hierarchies are inflexible relative to composing metaobjects or delegating to properties. That can be a good thing, in that it acts as a road-bump against someone accidentally implementing a MajorAccountsOutsideSalesperson, and it does make our understanding of the constraints of the domain very discoverable in the code. Second, invariance of class over the lifetime of an entity. Do salespeople move between inside and out? From SMB to Major Accounts? Changing classes is relatively rare in OOP style. In the current release of JavaScript, changing an object’s prototype is unevenly supported, unexpected by programmers, and incurs a heavy performance penalty. It’s usually easier to make a new entity with a new prototype chain that is otherwise a copy of the old entity, or to model something we expect to change as a state machine¹⁰⁵: If people tend to remain as inside or outside salespeople, but change between Major Accounts and SMB more often, we could model the accountType as a property and delegate or forward behaviour to it. Ultimately, class hierarchies do make semantic sense for some portions of the behaviour we need to model, but we need to consider the semantics carefully and not try to force everything to fit into a hierarchy. Those things that naturally fit can benefit from being modelled as classes, those that do not should be modelled in another way. Circling back to our fundamental idea, this is why it is important to (a) Have multiple ways to model behaviour with metaobjects, and (b) design our metaobject protocols (encapsulate, composition, delegation, and so forth) to be compatible with each other. The flexibility of mixing and matching state machines, delegation to properties, composing mixins, and prototype chains permits us to design software that is simpler to read, write, and refactor as needed. ¹⁰⁵https://en.wikipedia.org/wiki/Finite-state_machine
Inheritance, Ontologies, and Semantic Types
265
Structural vs. Semantic Typing A long-cherished principle of dynamic languages is that programs employ “Duck” or “Structural” typing. So if we write: function deposit (account, instrument) { account.dollars += instrument.dollars; account.cents += instrument.cents; account.dollars += Math.floor(account.cents / 100); account.cents = account.cents % 100; return account; }
This works for things that look like cheques, and for things that look like money orders:¹⁰⁶ cheque = { dollars: 100, cents: 0, number: 6 } deposit(currentAccount, cheque); moneyOrder = { dollars: 100, cents: 0, fee: 1.50 } deposit(currentAccount, moneyOrder);
The general idea here is that as long as we pass deposit an instrument that has dollars and cents properties, the function will work. We can think about hasDollarsAndCents as a “type,” and we can say that programming in a dynamic language like JavaScript is programming in a world where there is a many-many relationship between types and entities. Every single entity that has dollars and cents has the imaginary type hasDollarsAndCents, and every single function that takes a parameter and uses only its dollars and cents properties is a function that requires a parameter of type hasDollarsAndCents. ¹⁰⁶There’re good things we can say about why we should consider making an amount property, and/or encapsulate these structs so they behave like objects, but this gives the general idea of structural typing.
Inheritance, Ontologies, and Semantic Types
266
There is no checking of this in advance, like some other languages, but there also isnâ&#x20AC;&#x2122;t any explicit declaration of these types. They exist logically in the running system, but not manifestly in the code we write. This maximizes flexibility, in that it encourages the creation of small, independent pieces work seamlessly together. It also makes it easy to refactor to small, independent pieces. The code above could easily be changed to something like this: cheque = { amount: { dollars: 100, cents: 0 }, number: 6 } deposit(currentAccount, cheque.amount); moneyOrder = { amount: { dollars: 100, cents: 0 }, fee: 1.50 } deposit(currentAccount, moneyOrder.amount);
drawbacks This flexibility has a cost. With our ridiculously simple example above, we can easy deposit new kinds of instruments. But we can also do things like this: var backTaxesOwed = { dollars: 10,874, cents: 06 } var rentReceipt = { dollars: 420, cents: 0, unit: 504,
Inheritance, Ontologies, and Semantic Types
267
month: 6, year: 1962 } deposit(backTaxesOwed, rentReceipt);
Structurally, deposit is compatible with any two things that haveDollarsAndCents. But not all things that haveDollarsAndCents are semantically appropriate for deposits. This is why some OO language communities work very hard developing and using type systems that incorporate semantics. This is not just a theoretical concern. Numbers and strings are the ultimate in semantic-free data types. Confusing metric with imperial measures is thought to have caused the loss of the Mars Climate Orbiter¹⁰⁷. To prevent mistakes like this in software, forcing values to have compatible semantics–and not just superficially compatible structure–is thought to help create selfdocumenting code and to surface bugs.
semantic structs We’ve already seen Structs: function Struct (template) { if (Struct.prototype.isPrototypeOf(this)) { var struct = this; Object.keys(template).forEach(function (key) { Object.defineProperty(struct, key, { enumerable: true, writable: true, value: template[key] }); }); return Object.preventExtensions(struct); } else return new Struct(template); } Struct is a structural type, not a semantic type. But it can be extended to incorporate the notion of
semantic types by turning it from and object factory into a “factory-factory.” Here’s a completely new version of Struct, we give it a name and the keys we want, and it gives us a JavaScript constructor function: ¹⁰⁷https://en.wikipedia.org/wiki/Mars_Climate_Orbiter
Inheritance, Ontologies, and Semantic Types
268
function Struct () { var name = arguments[0], keys = [].slice.call(arguments, 1), constructor = eval("(function "+name+"(argument) { return initialize.call(t\ his, argument); })"); function initialize (argument) { if (constructor.prototype.isPrototypeOf(this)) { var struct = this; keys.forEach(function (key) { Object.defineProperty(struct, key, { enumerable: true, writable: true, value: argument[key] }); }); return Object.preventExtensions(struct); } else return new constructor(argument); }; return constructor; } var Depositable = Struct('Depositiable', 'dollars', 'cents'), RecordOfPayment = Struct('RecordOfPayment', 'dollars', 'cents'); var cheque = new Depositable({dollars: 420, cents: 0}); cheque.constructor; //=> [Function: Depositiable] cheque //=> cheque //=>
instanceof Depositable; true instanceof RecordOfPayment; false
Although Depositable and RecordOfPayment have the same structural type, they are different semantic types, and we can detect the difference with instanceof (and Object.isPrototypeOf).
Inheritance, Ontologies, and Semantic Types
269
We can also bake this test into our constructors. The code above uses a pattern borrowed from Effective JavaScript¹⁰⁸ so that you can write either new Depositable(...) or Depositable(...) and always get a new instance of Depositable. This version abandons that convention in favour of making Depositable a prototype check, and adds an explicit assertion method: function Struct () { var name = arguments[0], keys = [].slice.call(arguments, 1), constructor = eval("(function "+name+"(argument) { return initialize.call(t\ his, argument); })"); function initialize (argument) { if (constructor.prototype.isPrototypeOf(this)) { var argument = argument, struct = this; keys.forEach(function (key) { Object.defineProperty(struct, key, { enumerable: true, writable: true, value: argument[key] }); }); return Object.preventExtensions(struct); } else return constructor.prototype.isPrototypeOf(argument); }; constructor.assertIsPrototypeOf = function (argument) { if (!constructor.prototype.isPrototypeOf(argument)) { var name = constructor.name === '' ? "Struct(" + keys.join(", ") + ")" : constructor.name; throw "Type Error: " + argument + " is not a " + name; } else return argument; } return constructor; } ¹⁰⁸http://effectivejs.com
Inheritance, Ontologies, and Semantic Types
270
var Depositable = Struct('Depositable', 'dollars', 'cents'), RecordOfPayment = Struct('RecordOfPayment', 'dollars', 'cents'); var cheque = new Depositable({dollars: 420, cents: 0}); Depositable(cheque); //=> true RecordOfPayment.assertIsPrototypeOf(cheque); //=> Type Error: [object Object] is not a RecordOfPayment
We can use these “semantic” structs by adding assertions to critical functions: function deposit (account, instrument) { Depositable.assertIsPrototypeOf(instrument); account.dollars += instrument.dollars; account.cents += instrument.cents; account.dollars += Math.floor(account.cents / 100); account.cents = account.cents % 100; return account; }
This prevents us from accidentally trying to deposit a rent receipt.
is semantic typing worthwhile? The value of semantic typing is an open question. There are its proponents, and its detractors. One thing to consider is the proposition that with few exceptions, a programming system cannot be improved solely by removing features that can be subject to abuse. Instead, a system is improved by removing harmful features in such a way that they enable the addition of other, more powerful features that were “blocked” by the existence of harmful features. For example, proper or “pure” functional programming languages do not allow the mutation of values. This does remove a number of harmful possibilities, but in and of itself removing mutation is not a win. However, once you remove mutation you enable a number of optimization techniques such as lazy evaluation. This frees programmers to do things like write code for data structures that would not be possible in languages (like JavaScript) that have an eager evaluation strategy. Likewise, in our discussion of encapsulating metaobjects, we introduced private state and private methods. These features take some flexibility away from programmers. But we didn’t just rest on
Inheritance, Ontologies, and Semantic Types
271
the promise of decreasing coupling: We took advantage of this to introduce a more powerful way to compose metaobjects. Reducing the “surface area” of metaobjects increases their composeability, so we ended up with more flexibility, not less. If we subscribe to this philosophy about only reducing flexibility when it enables us to make an even greater increase in flexibility, then structural typing cannot be not be a win solely because we can “catch errors earlier” with things like Depositable.assertIsPrototypeOf(instrument). To be of benefit, it must enable some completely new kind of programming paradigm or feature that increases overall flexibility. We’ll revisit structural typing when we look at The Expression Problem, and in particular we’ll look at using structural typing to implement new dispatching protocols.
Inheritance, Ontologies, and Semantic Types
272
Interlude: “is-a” and “was-a” Words like “delegate” are carefully chosen to say nothing about semantic relationships. Given: function C () {} C.prototype.iAm = function () { return "a C"; }; var a = new C(); a.iAm(); //=> 'a C' a instanceof C //=> true
We can say that the entity a delegates handling of the method iAm to its prototype. But what is the relationship between a and C? What is this “instance of?” In programming, there are two major kinds relationships between entities and metaobjects (be those metaobjects mixins, classes, prototypes, or anything else). First, there is a semantic relationship. We call this is-a. If we say that a is-a C, we are saying that there is some valuable meaning to the idea that there is a set of all Cs and that a belonging to that set matters. The second is an implementation relationship. We have various names for this depending on how we implement things. We can say a delegates-to C. We can say a uses C. We sometimes arrange things so that a is-composed-of mixin M. When we have an implementation relationship, we are saying that there is no meaningful set of entities, the relationship is purely one of convenience. Sometimes, we use physical inheritance, like a prototype, but we don’t care about a semantic relationship, it’s simply a mechanism for sharing some behaviour. When we use inheritance but don’t care about semantics, we call the relationship was-a. a was-a C means that we are not asserting that it is meaningful to say that a is a member of the set of all Cs. Semantic typing is all about “is-a,” whereas structural typing is all about “was-a.”
273
Inheritance, Ontologies, and Semantic Types
was-a in the physical world “was-a” happens in the physical world all the time. Here’s an example: I buy a vintage Volkswagen Beetle. I customize it with racks:
My cargo bug is-a beetle. It’s recognizable as a beetle. And everywhere I could use a beetle, I can use my cargo bug. This is the is-a relationship of programming, there is some set of functions, and they we are saying that they are valid for all “b” where “b is-a beetle.” My cargo bug is a beetle, so we know that our functions are valid when they operate on it. Now I buy another beetle, but I rip off the bodywork and build a completely different car out of it, a Dune Buggy:
A dune buggy is not a beetle. It doesn’t look like a beetle. It was a beetle, but now it’s a dune buggy. “was-a” is a relationship that describes one object using another object for its implementation, but there is no equivalence implied. “was-a” is a relationship of convenience. image (c) 2010 Christina Spicuzza. Some rights reserved image (c) 2011. Some rights reserved
.
Inheritance, Ontologies, and Semantic Types
274
The Expression Problem The Expression Problem¹⁰⁹ is a programming design challenge: Given two orthogonal concerns of equal importance, how do we express our programming solution in such a way that neither concern becomes secondary? An example given in the c2 wiki¹¹⁰ concerns a set of shapes (circle, square) and a set of calculations on those shapes (circumference, area). We could write this using metaobjects: var Square = encapsulate({ constructor: function (length) { this.length = length; }, circumference: function () { return this.length * 4; }, area: function () { return this.length * this.length; } }); var Circle = encapsulate({ constructor: function (radius) { this.radius = radius; }, circumference: function () { return Math.PI * 2.0 * this.radius; }, area: function () { return Math.PI * this.radius * this.radius; } });
Or functions on structs:
¹⁰⁹https://en.wikipedia.org/wiki/Expression_problem ¹¹⁰http://c2.com/cgi/wiki?ExpressionProblem
Inheritance, Ontologies, and Semantic Types
275
var Square = Struct('Square', 'length'); var Circle = Struct('Circle', 'radius'); function circumference(shape) { if (Square(shape)) { return shape.length * 4; } else if (Circle(shape)) { return Math.PI * 2.0 * this.radius; } } function area (shape) { if (Square(shape)) { return this.length * this.length; } else if (Circle(shape)) { return Math.PI * this.radius * this.radius; } }
Both of these operations make one thing a first-class citizen and the the other a second-class citizen. The object solution makes shapes first-class, and operations second-class. The function solution makes operations first-class, and shapes second-class. We can see this by adding new functionality: 1. If we add a new shape (e.f. Triangle), it’s easy with the object solution: Everything you need to know about a triangle goes in one place. But it’s hard with the function solution: We have to carefully add a case to each function covering triangles. 2. If we add a new operation, (e.g. boundingBox returns the smallest square that encloses the shape), it’s easy with the function solution: we add a new function and make sure it has a case for each kind of shape. But it’s hard with the object solution: We have to make sure that we add a new method to each object. The expression problem originated as follows: Given a set of entities and a set of operations on those entities, how do we add new entities and new operations, without recompiling, without unsafe operations like casts, and while maintaining type safety? The general form of the problem does not concern type safety, but does concern the elegance of the design.
In a simple (two objects and two methods) example, the expression problem does not seem like much of a stumbling block. But imagine we are operating at scale, with a hierarchy of classes that
Inheritance, Ontologies, and Semantic Types
276
have methods at every level of the ontology. Adding new operations can be messy, especially in a language that does not have type checking to make sure we cover all of the appropriate cases. And the functions-first approach is equally messy in contemporary software. It’s a very sensible technique when we program with a handful of canonical data structures and want to make many operations on those data structures. This is why, despite decades of attempts to write ObjectRelational Mapping libraries, PL/SQL¹¹¹ is not going away. Given a slowly-changing database schema, it’s far easier to write a new procedure that operates across tables, than to try to write methods on objects representing a single entity in a table.
dispatches from space There’s a related problem. Consider some kind of game involving meteors that fall from the sky towards the Earth. You have fighters of some kind that fly around and try to shoot the meteors. We have an established way of handling a meteors hitting the Earth or a fighter flying into the ground and crashing: We write a .hitsGround() method for meteors and for fighters. WHenever something hits the ground, we invoke its .hitsGround() method, and it handles the rest. A fighter hitting the ground will cost so many victory points and trigger a certain animation. A meteor hitting the ground will cost a different number of victory points and trigger a different animation. And it’s easy to add new kinds of things that can hit the ground. As long as they implement .hitsGround(), we’re good. Each object knows what to do. This resembles encapsulation, but it’s actually called ad hoc polymorphism¹¹². It’s not an object hiding its state from tampering, it’s an object hiding its semantic type from the code that uses it. Fighters and meteors both have the same structural type, but different semantic types and different behaviour. “Standard” OO, as practiced by Smalltalk and its descendants on down to JavaScript, makes heavy use of polymorphism. The mechanism is known as single dispatch because given an expression like a.b(c,d), The choice of method to invoke given the method b is made based on a single receiver, a. The identities of c and d are irrelevant to choosing the code to handle the method invocation. Single-dispatch handles crashing into the ground brilliantly. It also handles things like adjusting the balance of a bank account brilliantly. But not everything fits the single dispatch model. Consider a fighter crashing into a meteor. Or another fighter. Or a meteor crashing into a fighter. Or a meteor crashing into another meteor. If we write a method like .crashInto(otherObject), then right away we have an anti-pattern, there are things that ought to be symmetrical, but we’re forcing an asymmetry on them. This is vaguely like forcing class A to extend B because we don’t have a convenient way to compose metaobjects. ¹¹¹https://en.wikipedia.org/wiki/PL/SQL ¹¹²https://en.wikipedia.org/wiki/Polymorphism_
Inheritance, Ontologies, and Semantic Types
277
In languages with no other option, we’re forced to do things like have one object’s method know an extraordinary amount of information about another object. For example, if a fighter’s .crashInto(otherObject) method can handle crashing into meteors, we’re imbuing fighters with knowledge about meteors.
double dispatch Over time, various ways to handle this problem with single dispatch have arisen. One way is to have a polymorphic method invoke another object’s polymorphic methods. For example: var FighterPrototype = { crashInto: function (otherObject) { this.collide(); otherObject.collide(); this.destroyYourself(); otherObject.destroyYourself(); }, collide: function () { // ... }, destroyYourself: function () { // ... } }
In this scheme, each object knows how to collide and how to destroy itself. So a fighter doesn’t have to know about meteors, just to trust that they implement .collide() and .destroyYourself(). Of course, this presupposes that a collisions between objects can be subdivided into independant behaviour. What if, for example, we have special scoring for ramming a meteor, or perhaps a sarcastic message to display? What if meteors are unharmed if they hit a fighter but shatter into fragments if they hit each other? A pattern for handling this is called double-dispatch¹¹³. It is a little more elegant in manifestly typed languages than in dynamically typed languages, but such superficial elegance is simply masking some underlyin issues. Here’s how we could implement collisions with special cases:
¹¹³https://en.wikipedia.org/wiki/Double_dispatch
Inheritance, Ontologies, and Semantic Types
278
var FighterPrototype = { crashInto: function (objectThatCrashesIntoFighters) { return objectThatCrashesIntoFighters.isStruckByAFighter(this) }, isStruckByAFighter: function (fighter) { // handle fighter-fighter collisions }, isStruckByAMeteor: function (meteor) { // handle fighter-meteor collisions } } var MeteorPrototype = { crashInto: function (objectThatCrashesIntoMeteors) { return objectThatCrashesIntoMeteors.isStruckByAMeteor(this) }, isStruckByAFighter: function (fighter) { // handle meteor-fighter collisions }, isStruckByAMeteor: function (meteor) { // handle meteor-meteor collisions } } var someFighter = Object.create(FighterPrototype), someMeteor = Object.create(MeteorPrototype); someFighter.crashInto(someMeteor);
In this scheme, when we call someFighter.crashInto(someMeteor), FighterPrototype.crashInto invokes someMeteor.isStruckByAFighter(someFighter), and that handles the specific case of a meteor being struck by a fighter. To make this work, both fighters and meteors need to know about each other. They are coupled. And as we add more types of objects (observation balloons? missiles? clouds? bolts of lightning?), our changes must be spread across our prototypes. It is obvious that this system is highly inflexible. The principle of messages and encapsulation is ignored, we are simply using JavaScript’s method dispatch system to achieve a result, rather than modeling entities. Generally speaking, double dispatch is considered a red flag. Sometimes it’s the best technique to use, but often it’s a sign that we have chosen the wrong abstractions.
Inheritance, Ontologies, and Semantic Types
279
Multiple Dispatch In The Expression Problem, we saw that JavaScript’s single-dispatch system made it difficult to model interactions that varied on two (or more) semantic types. Our example was modeling collisions between fighters and meteors, where we want to have different outcomes depending upon whether a fighter or a meteor collided with another fighter or a meteor. Languages such as Common Lisp¹¹⁴ bake support for this problem right in, by supporting multiple dispatch. With multiple dispatch, generic functions can be specialized depending upon any of their arguments. In this example, we’re defining forms of collide to work with a meteors and fighters: (defmethod collide ((object-1 meteor) (object-2 fighter)) (format t "meteor ~a collides with fighter ~a" object-1 object-2)) (defmethod collide ((object-1 meteor) (object-2 meteor)) (format t "meteor ~a collides with another meteor ~a" object-1 object-2))
Common Lisp’s generic functions use dynamic dispatch on both object-1 and object-2 to determine which body of collide to evaluate. Meaning, both types are checked at run time, at the time when the function is invoked. Since more than one argument is checked dynamically, we say that Common Lisp has multiple dispatch. Manifestly typed OO languages like Java appear to support multiple dispatch. You can create one method with several signatures, something like this: interface Collidable { public void crashInto(Meteor meteor); public void crashInto(Fighter fighter); } class Meteor implements Collidable { public void crashInto(Meteor meteor); public void crashInto(Fighter fighter); } class Fighter implements Collidable { public void crashInto(Meteor meteor); public void crashInto(Fighter fighter); }
Alas this won’t work. Although we can specialize crashInto by the type of its argument, the Java compiler resolves this specialization at compile time, not run time. It’s early bound. Thus, if we write something like this pseudo-Java: ¹¹⁴https://en.wikipedia.org/wiki/Common_Lisp
Inheritance, Ontologies, and Semantic Types
280
Collidable thingOne = Math.random() < 0.5 ? new Meteor() : new Fighter(), Collidable thingTwo = Math.random() < 0.5 ? new Meteor() : new Fighter(); thingOne.crashInto(thingTwo);
It won’t even compile! The compiler can figure out that thingOne is a Collidable and that it has two different signatures for the crashInto method, but all it knows about thingTwo is that it’s a Collidable, the compiler doesn’t know if it should be compiling an invocation of crashInto(Meteor meteor) or crashInto(Fighter fighter), so it refuses to compile this code. Java’s system uses dynamic dispatch for the receiver of a method: The class of the receiver is determined at run time and the appropriate method is determined based on that class. But it uses static dispatch for the specialization based on arguments: The compiler sorts out which specialization to invoke based on the declared type of the argument at compile time. If it can’t sort that out, the code does not compile. Java may have type signatures to specialize methods, but it is still single dispatch, just like JavaScript.
emulating multiple dispatch Javascript cannot do true multiple dispatch without some ridiculous greenspunning of method invocations. But we can fake it pretty reasonably using the same technique we used for emulating predicate dispatch. We start with the same convention: Methods and functions must return something if they successfully hand a method invocation, or raise an exception if they catastrophically fail. They cannot return undefined (which in JavaScript, also includes not explicitly returning something). Recall that this allowed us to write the Match function that took a serious of guards, functions that checked to see if the value of arguments was correct for each case. Our general-purpose guard, when, took all of the arguments as parameters. function nameAndLength(name, length, body) { var abcs = [ 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'z', 'x', 'c', 'v', 'b', 'n', 'm' ], pars = abcs.slice(0, length), src = "(function " + name + " (" + pars.join(',') + ") { return body.apply\ (this, arguments); })"; return eval(src); } function imitate(exemplar, body) {
Inheritance, Ontologies, and Semantic Types
281
return nameAndLength(exemplar.name, exemplar.length, body); } function getWith (prop, obj) { function gets (obj) { return obj[prop]; } return obj === undefined ? gets : gets(obj); } function mapWith (fn, mappable) { function maps (collection) { return collection.map(fn); } return mappable === undefined ? maps : maps(collection); } function pluckWith (prop, collection) { var plucker = mapWith(getWith(prop)); return collection === undefined ? plucker : plucker(collection); } function Match () { var fns = [].slice.call(arguments, 0), lengths = pluckWith('length', fns), length = Math.min.apply(null, lengths), = pluckWith('name', fns).filter(function (name) { return name !== '\ names '; }), name = names.length === 0 ? '' : names[0]; return nameAndLength(name, length, function () {
Inheritance, Ontologies, and Semantic Types
282
var i, value; for (i in fns) { value = fns[i].apply(this, arguments); if (value !== undefined) return value; } }); }
What we want is to write guards for each argument. So weâ&#x20AC;&#x2122;ll write whenArgsAre, a guard that takes predicates for each argument as well as the body of the function case: function isType (type) { return function (arg) { return typeof(arg) === type; }; } function instanceOf (clazz) { return function (arg) { return arg instanceof clazz; }; } function isPrototypeOf (proto) { return Object.prototype.isPrototypeOf.bind(proto); } function whenArgsAre () { var matchers = [].slice.call(arguments, 0, arguments.length - 1), body = arguments[arguments.length - 1]; function typeChecked () { var i, arg, value; if (arguments.length != matchers.length) return; for (i in arguments) { arg = arguments[i];
283
Inheritance, Ontologies, and Semantic Types
if (!matchers[i].call(this, arg)) return; } value = body.apply(this, arguments); return value === undefined ? null : value; } return imitate(body, typeChecked); } function Fighter () {}; function Meteor () {}; var handlesManyCases = Match( whenArgsAre( instanceOf(Fighter), instanceOf(Meteor), function (fighter, meteor) { return "a fighter has hit a meteor"; } ), whenArgsAre( instanceOf(Fighter), instanceOf(Fighter), function (fighter, fighter) { return "a fighter has hit another fighter"; } ), whenArgsAre( instanceOf(Meteor), instanceOf(Fighter), function (meteor, fighters) { return "a meteor has hit a fighter"; } ), whenArgsAre( instanceOf(Meteor), instanceOf(Meteor), function (meteor, meteor) { return "a meteor has hit another meteor"; } ) ); handlesManyCases(new Meteor(),
new Meteor());
Inheritance, Ontologies, and Semantic Types
284
//=> 'a meteor has hit another meteor' handlesManyCases(new Fighter(), new Meteor()); //=> 'a fighter has hit a meteor'
Our MultipleDispatch function now allows us to build generic functions that dynamically dispatch on all of their arguments. They work just fine for creating multiply dispatched methods: var FighterPrototype = {}, MeteorPrototype = {}; FighterPrototype.crashInto = Match( whenArgsAre( isPrototypeOf(FighterPrototype), function (fighter) { return "fighter(fighter)"; } ), whenArgsAre( isPrototypeOf(MeteorPrototype), function (fighter) { return "fighter(meteor)"; } ) ); MeteorPrototype.crashInto = Match( whenArgsAre( isPrototypeOf(FighterPrototype), function (fighter) { return "meteor(fighter)"; } ), whenArgsAre( isPrototypeOf(MeteorPrototype), function (meteor) { return "meteor(meteor)"; } ) ); var someFighter = Object.create(FighterPrototype), someMeteor = Object.create(MeteorPrototype);
Inheritance, Ontologies, and Semantic Types
285
someFighter.crashInto(someMeteor); //=> 'fighter(meteor)'
We now have usable dynamic multiple dispatch for generic functions and for methods. It’s built on predicate dispatch, so it can be extended to handle other forms of guarding.
the opacity of functional composition Consider the following problem: We wish to create a specialized entity, an ArmoredFighter that behaves just like a regular fighter, only when it strikes another fighter it has some special behaviour. var ArmoredFighterPrototype = Object.create(FighterPrototype); ArmoredFighterPrototype.crashInto = Match( whenArgsAre( isPrototypeOf(FighterPrototype), function (fighter) { return "armored-fighter(fighter)"; } ) );
Our thought is that we are “overriding” the behaviour of crashInto when an armored fighter crashes into any other kind of fighter. But we wish to retain the behaviour we have already designed when an armored fighter crashes into a meteor. This is not going to work. Although we have written our code such that the various cases and predicates are laid out separately, at run time they are composed opaquely into functions. As far as JavaScript is concerned, we’ve written: var FighterPrototype = {}; FighterPrototype.crashInto = function (q) { // black box }; var ArmoredFighterPrototype = Object.create(FighterPrototype); ArmoredFighterPrototype.crashInto = function (q) { // black box };
Inheritance, Ontologies, and Semantic Types
286
We’ve written code that composes, but it doesn’t decompose. We’ve made it easy to manually take the code for these functions apart, inspect their contents, and put them back together in new ways, but it’s impossible for us to write code that inspects and decomposes the code. A better design might incorporate reflection and decomposition at run time.
Metaobject Protocols
¹¹⁵¹¹⁶
¹¹⁵https://www.flickr.com/photos/baristahoon/8664290273 ¹¹⁶Cime Espresso Machine Factory, Copyright 2012, Jong Hoon Lee Some Rights Reserved
Metaobject Protocols
In This Chapter In this chapter, weâ&#x20AC;&#x2122;ll explore making metaobjects that encapsulate their own metaprogramming behaviour. In other words, weâ&#x20AC;&#x2122;ll program OO, using OO.
.
288
Metaobject Protocols
289
Genesis In this section, we’re going to build a Metaobject Protocol¹¹⁷. Or in less buzzword-compatible terms, we’re going to build classes that can be programmed using methods. We can already make class-like things with functions, prototypes, and the new keyword, provided we directly manipulate our “classes.” This makes it difficult to make changes after the fact, because all of our code is tightly coupled to the structure of our “classes.” We can decouple our code from the structure of our classes by doing all of our manipulation with methods instead of with direct manipulation of functions and prototypes. Let’s start with a simple example, a quadtree¹¹⁸ class. Normally, we would start with something like this: function QuadTree (nw, ne, se, sw) { this.nw = nw; this.ne = ne; this.se = se; this.sw = sw; }
And we would write methods using code like this: QuadTree.prototype.population = function () { return this.nw.population() + this.ne.population() + this.se.population() + this.sw.population(); } var account = new QuadTree(...);
As discussed before, we are directly manipulating QuadTree’s internal representation. Let’s abstract method definition away. We’d like to write: QuadTree.defineMethod( 'population', function () { return this.nw.population() + this.ne.population() + this.se.population() + this.sw.population(); });
We’ll see why in a moment. How do we make this work? Let’s start with: ¹¹⁷https://en.wikipedia.org/wiki/Metaobject#Metaobject_protocol ¹¹⁸https://en.wikipedia.org/wiki/Quadtree
Metaobject Protocols
290
QuadTree.defineMethod = function (name, body) { this.prototype[name] = body; return this; };
That works, and now we can introduce new semantics at will. For example, what does this do? QuadTree.defineMethod = function (name, body) { Object.defineProperty( this.prototype, name, { writable: false, enumerable: false, value: body }); return this; };
factory factories We can now write: QuadTree.defineMethod( 'population', function () { return this.nw.population() + this.ne.population() + this.se.population() + this.sw.population(); });
We accomplished this by writing: QuadTree.defineMethod = function (name, body) { this.prototype[name] = body; return this; };
Fine. But presuming we carry on to create other classes like Cell, how do we give them the same defineMethod method? Hmmm, itâ&#x20AC;&#x2122;s almost like we want to have objects that delegate to a common prototype. We know how to do that:
Metaobject Protocols
291
var ClassMethods = { defineMethod: function (name, body) { this.prototype[name] = body; return this; } }; var QuadTree = Object.create(ClassMethods);
Alas, this doesn’t work for functions we use with new, because JavaScript functions are a special kind of object that we can’t decorate with our own prototypes. We could fool around with mixing behaviour in, but let’s abstract new away and use a .create‘ method to create instances.
.create This is what QuadTree would look like with its own .create method. We’ve moved initialization into an instance method instead of being part of the factory method: var QuadTree = { create: function () { var acct = Object.create(this.prototype); if (acct.constructor) { acct.constructor.apply(acct, arguments); } return acct; }, defineMethod: function (name, body) { this.prototype[name] = body; return this; }, prototype: { constructor: function (nw, ne, se, sw) { this.nw = nw; this.ne = ne; this.se = se; this.sw = sw; } } };
Let’s extract a few things. Step one:
Metaobject Protocols
function Class () { return { create: function () { var instance = Object.create(this.prototype); Object.defineProperty(instance, 'constructor', { value: this }); if (instance.constructor) { instance.constructor.apply(instance, arguments); } return instance; }, defineMethod: function (name, body) { this.prototype[name] = body; return this; }, prototype: {} }; } var QuadTree = Class(); QuadTree.defineMethod( 'initialize', function (nw, ne, se, sw) { this.nw = nw; this.ne = ne; this.se = se; this.sw = sw; } ).defineMethod( 'population', function () { return this.nw.population() + this.ne.population() + this.se.population() + this.sw.population(); } );
Step two:
292
Metaobject Protocols
var BasicObjectClass = { prototype: {} } function Class (superclass) { return { create: function () { var instance = Object.create(this.prototype); Object.defineProperty(instance, 'constructor', { value: this }); if (instance.constructor) { instance.constructor.apply(instance, arguments); } return instance; }, defineMethod: function (name, body) { this.prototype[name] = body; return this; }, prototype: Object.create(superclass.prototype) }; } var QuadTree = Class(BasicObjectClass);
Step three: var BasicObjectClass = { prototype: {} } var MetaMetaObjectPrototype = { create: function () { var instance = Object.create(this.prototype); Object.defineProperty(instance, 'constructor', { value: this }); if (instance.constructor) { instance.constructor.apply(instance, arguments); } return instance;
293
Metaobject Protocols
}, defineMethod: function (name, body) { this.prototype[name] = body; return this; } } function Class (superclass) { return Object.create(MetaMetaObjectPrototype, { prototype: { value: Object.create(superclass.prototype) } }) } var QuadTree = Class(BasicObjectClass); // QuadTree.defineMethod...
Step four, now we change the shape: var Class = { create: function (superclass) { return Object.create(MetaMetaObjectPrototype, { prototype: { value: Object.create(superclass.prototype) } }); } } var QuadTree = Class.create(BasicObjectClass); // QuadTree.defineMethod...
Step five:
294
Metaobject Protocols
var MetaObjectPrototype = Object.create(MetaMetaObjectPrototype, { constructor: { value: function (superclass) { this.prototype = Object.create(superclass.prototype) } } }); var Class = { create: function (superclass) { var klass = Object.create(this.prototype); Object.defineProperty(klass, 'constructor', { value: this }); if (klass.constructor) { klass.constructor.apply(klass, arguments); } return klass; }, prototype: MetaObjectPrototype }; var QuadTree = Class.create(BasicObjectClass); // QuadTree.defineMethod...
ouroboros Now we are in an interesting place. Letâ&#x20AC;&#x2122;s consolidate and rename things: var MetaObjectPrototype = { create: function () { var instance = Object.create(this.prototype); Object.defineProperty(instance, 'constructor', { value: this }); if (instance.constructor) { instance.constructor.apply(instance, arguments); } return instance; }, defineMethod: function (name, body) {
295
Metaobject Protocols
this.prototype[name] = body; return this; }, constructor: function (superclass) { if (superclass != null && superclass.prototype != null) { this.prototype = Object.create(superclass.prototype); } else this.prototype = Object.create(null); } }; var MetaClass = { create: function () { var klass = Object.create(this.prototype); Object.defineProperty(klass, 'constructor', { value: this }); if (klass.constructor) { klass.constructor.apply(klass, arguments); } return klass; }, prototype: MetaObjectPrototype };
And now we do something interesting, we use MetaClass to make Class: var Class = MetaClass.create(MetaClass); Class.constructor === MetaClass //=> true
And we use Class to make QuadTree:
296
Metaobject Protocols
297
var BasicObjectClass = Class.create(null); var QuadTree = Class.create(BasicObjectClass); QuadTree.constructor === Class //=> true QuadTree.defineMethod( 'initialize', function (nw, ne, se, sw) { this.nw = nw; this.ne = ne; this.se = se; this.sw = sw; } ).defineMethod( 'population', function () { return this.nw.population() + this.ne.population() + this.se.population() + this.sw.population(); } );
As well as Cell: var Cell = Class.create(BasicObjectClass); Cell.defineMethod( 'initialize', function (population) { this._population = population; }).defineMethod( 'population', function () { return this._population; });
What have we achieved? Well, QuadTree and Cell are both classes, and theyâ&#x20AC;&#x2122;re also instances of class Class. How does this help us? We now have the ability to modify every aspect of the creation of classes and instances by modifying methods and classes. For example, if all classes need a new method, we can call Class.defineMethod. Letâ&#x20AC;&#x2122;s try creating a new method on all classes:
Metaobject Protocols
Class.defineMethod( 'instanceMethods', function () { var prototype = this.prototype; return Object.getOwnPropertyNames(prototype).filter( function (propertyName) { return typeof(prototype[propertyName]) === 'function'; }) }); Cell.instanceMethods() //=> [ 'initialize', 'population' ]
It works!
298
Metaobject Protocols
299
The Class Class In The “I” Word, we discussed how the word “inheritance” is used, loosely, to refer to either or both of membership in a formal class (“is-a”), and delegation of implementation and expectations (“was-a”). Given our code for initializing metaobjects: constructor: function (superclass) { if (superclass != null && superclass.prototype != null) { this.prototype = Object.create(superclass.prototype); } else this.prototype = Object.create(null); }
This sets up the prototype chain for instances. For example, a special subclass of QuadTree for trees of trees that supports interpolating n, e, s, w, and c: var TreeOfTrees = Class.create(QuadTree); TreeOfTrees.defineMethod('n', function () { return this.ne.constructor.create(this.nw.ne, this.ne.nw, this.ne.sw, this.nw.s\ e); }).defineMethod('e', function () { return this.ne.constructor.create(this.ne.sw, this.ne.se, this.se.ne, this.se.n\ w); }) // ...
Instances of TreeOfTrees inherit the methods defined in TreeOfTrees as well as those in QuadTree. Although this technique can be overused, it is a useful option for sharing behaviour. Obviously, implementation inheritance allows two classes to share a common superclass, and thus share behaviour between them. What about classes themselves? As described, they are all instances of Class, but that is not a hard and fast requirement.
fluent interfaces Consider fluent interfaces¹¹⁹. In software engineering, a fluent interface (as first coined by Eric Evans and Martin Fowler) is an implementation of an object oriented API that aims to provide for more readable code. Object and instance methods can be bifurcated into two classes: Those that query something, and those that update something. Most design philosophies arrange things such that update methods return the value being updated. However, the fluent¹²⁰ style presumes that most of the time when ¹¹⁹https://en.wikipedia.org/wiki/Fluent_interface ¹²⁰https://en.wikipedia.org/wiki/Fluent_interface
300
Metaobject Protocols
you perform an update, you are more interested in doing other things with the receiver then the values being passed as argument(s), so the rule is to return the receiver unless the method is a query. So, given: var MutableQuadTree = Class.create(BasicObjectClass);
We might write: MutableQuadTree .defineMethod( 'setNW', this.nw = value; return this; }) .defineMethod( 'setNE', this.ne = value; return this; }) .defineMethod( 'setSE', this.se = value; return this; }) .defineMethod( 'setSW', this.sw = value; return this; });
function (value) {
function (value) {
function (value) {
function (value) {
This would be a fluent interface, allowing us to write things like: var tree = MutableQuadTree .create() .setNW(Cell.create(1)) .setNE(Cell.create(0)) .setSE(Cell.create(0)) .setSW(Cell.create(01);
However, what work it is! Another possibility would be to subclass Class, and override defineMethod, like so:
301
Metaobject Protocols
var allong = require('allong.es'); var unvariadic = allong.es.unvariadic; var FluentClass = Class.create(Class); FluentClass.defineMethod( 'defineMethod', function (name, body) { this.prototype[name] = unvariadic(body.length, function () { var returnValue = body.apply(this, arguments); if (typeof(returnValue) === 'undefined') { return this; } else return returnValue; }); return this; });
A normal method semantics are that if it doesn’t explicitly return a value, it returns undefined. A FluentClass method’s semantics are that if it doesn’t explicitly return a value, it returns the receiver. So now, we can write: var MutableQuadTree = FluentClass.create(BasicObjectClass); MutableQuadTree .defineMethod( 'setNW', this.nw = value; }) .defineMethod( 'setNE', this.ne = value; }) .defineMethod( 'setSE', this.se = value; }) .defineMethod( 'setSW', this.sw = value; });
function (value) {
function (value) {
function (value) {
function (value) {
var tree = MutableQuadTree .create() .setNW(Cell.create(1)) .setNE(Cell.create(0)) .setSE(Cell.create(0)) .setSW(Cell.create(01);
Metaobject Protocols
302
And our trees are fluent by default: Any method that doesn’t explicitly return a value with the return keyword will return the receiver. By creating classes that share their own implementation of defineMethod, we now have a way to create collections of objects that have their own special semantics. There are many ways to do this, of course, but the key idea here is that we’re using polymorphism: Fluent classes look just like regular classes, so there is no need to write any special code when defining a fluent method or creating an object with fluent semantics. We’re using the exact same techniques for programming with metaclasses that we use for programming with domain objects.
303
Metaobject Protocols
Class Mixins In The Class Class, we saw how we could use implementation inheritance to create FluentClass, a class where methods are fluent by default. Implementation inheritance is not the only way to customize class behaviour, and it’s rarely the best choice. Long experience with hierarchies of domain objects has shown that excessive reliance on implementation inheritance leads to fragile software that ends up being excessively coupled. Contemporary thought suggests that traits or mixins are a better choice when there isn’t a strong need to model an “is-a” relationship. Considering that JavaScript is a language that emphasizes ad hoc sets rather than formal classes, it makes sense to look at other ways to create classes with custom semantics.
singleton prototypes The typical arrangement for “classes” is that objects of the same class share a common prototype associated with the class:
Standard class prototype delegation
This is true whether we are using functions as our “classes” or objects that are themselves instances of Class. In Singleton Prototypes, we looked at how we can associate a singleton prototype with each object:
304
Metaobject Protocols
Singleton prototype delegation
Each object delegates its behaviour to its own singleton prototype, and that prototype delegates in turn to the class prototype. As discussed earlier, the motivation for this arrangement is so that methods can be added to individual objects while preserving a separation of domain attributes from methods. For example, you can add a method to an objectâ&#x20AC;&#x2122;s singleton prototype and still use .hasOwnProperty() to distinguish attributes from methods. To create objects with singleton prototypes, we could use classes that have their own .create method:
Metaobject Protocols
305
var SingletonPrototypicalClass = Class.create(Class); SingletonPrototypicalClass.defineMethod('create', function () { var singletonPrototype = Object.create(this.prototype); var instance = Object.create(singletonPrototype); Object.defineProperty(instance, 'constructor', { value: this }); if (instance.constructor) { instance.constructor.apply(instance, arguments); } return instance; });
This “injects” the singleton prototype in between the newly created instance and the class’s prototype. So if we create a new class: var SingletonPrototypeCell = SingletonPrototypicalClass.create(); var SingletonPrototypeQuadTree = SingletonPrototypicalClass.create();
We know that every instance of SingletonPrototypeCell and SingletonPrototypeQuadTree will have its own singleton prototype.
mixins This seems very nice, but what do we do if we want a class that has both fluent interface semantics and singleton prototype semantics, like our MutableQuadTree example? Do we decide that all classes should encompass both behaviours by adding the functionality to Class? That works, but it leads to a very heavyweight class system. This is the philosophy of languages like Ruby, where all classes have a lot of functionality. That is consistent with their approach to basic objects as well: The default Object class also has a lot of functionality that every object inherits by default. Do we make SingletonPrototypicalClass a subclass of FluentClass? That makes it impossible to have a class with singleton prototype semantics that doesn’t also have fluent semantics, and there’s the little matter that fluency and prototype semantics really aren’t related in any kind of “isa” semantic sense. Making FluentClass a subclass of SingletonPrototypicalClass has the same problems. Forty years of experience modeling domain entities has led OO thinking to the place where representing independent semantics with inheritance is now considered an anti-pattern. Instead, we look for ways to select traits, behaviours, or semantics on an a’la carte basis using template inheritance, also called mixing in behaviour. Mixing behaviour in is as simple as extending the class object:
Metaobject Protocols
306
var MutableQuadTree = Class.create(); extend(MutableQuadTree, { defineMethod: function (name, body) { this.prototype[name] = unvariadic(body.length, function () { var returnValue = body.apply(this, arguments); if (typeof(returnValue) === 'undefined') { return this; } else return returnValue; }); return this; } }); extend(MutableQuadTree, { create: function () { var singletonPrototype = Object.create(this.prototype); var instance = Object.create(singletonPrototype); Object.defineProperty(instance, 'constructor', { value: this }); if (instance.constructor) { instance.constructor.apply(instance, arguments); } return instance; } });
We eschew monkeying around explicitly with internals, so letâ&#x20AC;&#x2122;s create some functions we can call: function Fluentize (klass) { extend(klass, { defineMethod: function (name, body) { this.prototype[name] = unvariadic(body.length, function () { var returnValue = body.apply(this, arguments); if (typeof(returnValue) === 'undefined') { return this; } else return returnValue; }); return this;
Metaobject Protocols
307
} }); } function Singletonize (klass) { extend(klass, { create: function () { var singletonPrototype = Object.create(this.prototype); var instance = Object.create(singletonPrototype); Object.defineProperty(instance, 'constructor', { value: this }); if (instance.constructor) { instance.constructor.apply(instance, arguments); } return instance; } }); }
Now we can write: MutableQuadTree = Class.create(); Fluentize(MutableQuadTree); Singletonize(MutableQuadTree);
This cleanly separates the notion of subclassing classes from the notion of some classes having traits or behaviours they share with other classes. Note that although the mixin pattern is completely different from the subclassing Class pattern, both are made possible by using objects as classes rather than using functions, the new keyword, and directly manipulating prototypes to define methods.
Metaobject Protocols
308
Well, Actually… Both inheritance and mixins have their place for both our domain classes and the metaobjects that define them. However, if not designed with care, object-oriented code bases evolve over time to become brittle and coupled whether they’re built with inheritance or mixins. Let’s take another look at class mixins and see why. In Class Mixins, we saw how we could build two mixins, Fluentize and Singletonize. Implementing these two semantics as mixins allows us to create classes that have neither semantic, fluent semantics, singleton prototype semantics, or both. This is far less brittle than trying to do the same thing by cramming both sets of functionality into every class (the “heavyweight base” approach) or trying to set up an artificial inheritance hierarchy. This sounds great for classes of classes and for our domains as well: Build classes for domain objects, and write mixins for various bits of shared functionality. Close the book, we’re done! Alas, we’re not done. The flexibility of mixins is an illusion, and by examining the hidden problem, we’ll gain a deeper understanding of how to design object-oriented software.
self-binding-ization In our next example, Fluentize overrides the defineMethod method, such that fluent classes use its implementation and not the implementation built into MetaClass. Likewise, Singletonize overrides the create method, such that singleton prototype classes use its implementation and not the implementation built into MetaClass. These two mixins affect completely different methods, and furthermore the changes they make do not affect each other in any way. But this isn’t always the case, consider this counter: var Counter = Class.create(); Counter .defineMethod('initialize', function () { this._count = 0; }) .defineMethod('increment', function () { ++this._count; }) .defineMethod('count', function () { return this.count; }); var c = Counter.create();
And we have some function written in continuation-passing-style:
Metaobject Protocols
309
function log (message, callback) { console.log(message); return callback(); }
Alas, we can’t use our counter: log("doesn't work", c.increment);
The trouble is that the expression c.increment returns the body of the method, but when it is invoked using callback(), the original context of c has been lost. The usual solution is to write: log("works", c.increment.bind(c));
The .bind method binds the context permanently. Another solution is to write (or use a function¹²¹ to write): c.increment = c.increment.bind(c);
Then you can write: log("works without thinking about it", c.increment);
It seems like a lot of trouble to be writing this out everywhere, especially when the desired behaviour is nearly always that methods be bound. Let’s write another mixin: function SelfBindingize (klass) { klass.defineMethod = function (name, body) { Object.defineProperty(this.prototype, name, { get: function () { return body.bind(this); } }); return this; } } var SelfBindingCounter = Class.create(); SelfBindingize(SelfBindingCounter); ¹²¹http://underscorejs.org/#bind
310
Metaobject Protocols
SelfBindingCounter.defineMethod('initialize', function () { this._count = 0; }) SelfBindingCounter.defineMethod('increment', function () { return ++this._count; \ }) SelfBindingCounter.defineMethod('count', function () { return this.count; }); var cc = SelfBindingCounter.create(); function log (message, callback) { console.log(message); return callback(); } log("works without thinking about it", cc.increment);
Classes that mix SelfBindingize in are self-binding. Weâ&#x20AC;&#x2122;ve encapsulated the internal representation and implementation, and hidden it behind a method that we mix into the class.
Composition with Red, Blue and Yellowâ&#x20AC;&#x201C;Piet Mondrian (1930)
composition As we might expect, SelfBindingize plays nicely with Singletonize, just as Fluentize did. But speaking of Fluentize, Singletonize does not play nicely with Fluentize. They both create new versions of defineMethod, and if you try to use both, only the last one executed will take effect.
Metaobject Protocols
311
The problem is that SelfBindingize and Fluentize don’t compose. We can make up lots more similar examples too. In fact, metaprogramming tools generally don’t compose when you write them individually. And this lesson is not restricted to “advanced” ideas like writing class mixins: Mixins in the problem domain have the same problem, they tend not to compose by default, and that makes using them a finicky and fragile process. The naïve way to solve this problem is to write our mixins, and then when conflicts are discovered, rewrite them to play well together. This is not a bad strategy, in fact it might be considered the “optimistic” strategy. As long as we are aware of the possibility of conflicts and know to look for them when the code does not do what we expect, it is not unreasonable to try to avoid conflicts rather than investing in another layer of abstraction up front. On the other hand, if we expect to be doing a lot of mixing in, it is a good investment to design around composition up front rather than deal with the uncertainty of not knowing when what appears to be a small change might lead to a lot of rewriting later on. When dealing with semantics “in the large,” such as when writing class mixins, there generally aren’t a lot of “custom” semantics needed in one software application, and they generally come up early in the design and development process. Thus, many developers take the optimistic approach: They build classes and incorporate custom semantics as they go, resolving any conflicts where needed. However, in the real of the problem domain, there are many more requirements for mixing functionality in, and requirements change more often. Thus, experienced programmers will sometimes build (or “borrow” from a library) infrastructure to make it easy to compose mixed-in functionality for domain objects. For example, in the Ruby on Rails¹²² framework, there is extensive support for binding behaviour to controller methods and to model object lifecycles, and the library is carefully written so that you can bind more than one quantum of behaviour, and the bindings compose neatly without overwriting each others’ effects. This support is implemented with metaprogramming of the semantics of controller and model base classes. If we accept the notion that we are more likely to need to compose model behaviour than metaobject behaviour, one way forward is to use our tools for metaobject programming to implement a protocol for composing model behaviour.
method decorators When looking at writing classes with classes, we used the example of making methods fluent by default. Fluent APIs are a wide-ranging, cross-cutting concern the can be applied to many designs. But for every wide-ranging design technique, there are thousands of application-specific domain concerns that can be applied to method semantics. Depending on the problem domain, programmers might be concerned about things like: ¹²²https://github.com/gititboards/RubyOnRails
Metaobject Protocols
312
• Checking for permissions before executing a method • Logging the invocation and/or result of a method • Checking entities for validity before persisting them to storage. Each domain concern can be written as a mixin of sorts. In javaScript Allongé¹²³, we looked at techniques for using combinators to apply special semantics to methods. For example, here is a simple combinator that “validates” the arguments to a method: function validates (methodBody) { return function () { var i; for (i = 0; i < arguments.length; ++i) { if (typeof(arguments[i].validate) === 'function' && !arguments[i].validate(\ )) throw "validation failure"; } return fn.apply(this, arguments); } }
Amongst other improvements, a more robust implementation would mimic the method body’s arity. But this is close enough for discussion: Applied to a function, it returns a function that first checks all of its arguments. If any of them implement a .validate method, that method is called and an error thrown if it doesn’t return true. And here’s a combinator that implements “always fluent” method semantics (this differs slightly from the “fluent-by-default” that we saw earlier): function fluent (methodBody) { return function () { methodBody.apply(this, arguments); return this } }
These two combinators compose nicely if you write something like this:
¹²³https://leanpub.com/javascript-allonge
Metaobject Protocols
313
function Account () { // ... } Account.prototype.validate = function () { // ... }; function AccountList () { this.accounts = []; } AccountList.prototype.add = fluent( validates( function (account) { this.accounts.push(account); }));
You can even explicitly compose the combinators: var compose = require('allong').es.compose; var fluentlyValidates = compose(fluent, validates);
Or apply them after assigning the method: AccountList.prototype.add = function (account) { this.accounts.push(account); }; // ... AccountList.prototype.add = fluent( validates( AccountList.prototype.add ) );
Everything seems wonderful with this approach! But this has limitations as well. The limitation is that combinators operate on functions. The hidden assumption is that when we apply the combinator, we already have a function we want to decorate. This wonâ&#x20AC;&#x2122;t work, for example:
Metaobject Protocols
314
function AccountList () { this.accounts = []; } AccountList.prototype.add = fluent( validates( AccountList.prototype.add ) ); // ... AccountList.prototype.add = function (account) { this.accounts.push(account); };
You can’t decorate the method’s function before you assign it! Here’s another variation of the same problem: function AccountList () { this.accounts = []; } AccountList.prototype.add = fluent( validates( function (account) { this.accounts.push(account); })); // ...
So far so good, but: function IndexedAccountList () { this.accounts = {}; } IndexedAccountList.prototype = Object.create(AccountList.prototype); IndexedAccountList.prototype.add = function (account) { this.accounts[account.id] = account; } IndexedAccountList.prototype.at = function (id) { return this.accounts[id]; }
Metaobject Protocols
315
Our IndexedAccountList.prototype.add overrides AccountList.prototype.add… And in the process, overrides the modifications made by fluent and validates. This makes perfect sense if you are using prototypical inheritance purely as a convenience for sharing some behaviour. But if you are actually trying to model a formal class, then perhaps you always want the .add method to validate its arguments and fluently return the receiver. Perhaps you only want to override how the method adds things internally without modifying the “contract” you are expressing with the outside world? Regardless of your exact interpretation of the semantics of overriding a method that you’ve decorated, the larger issue remains that there is some brittleness involved in using functions to decorate methods in classes. They don’t always compose neatly and cleanly.
Appendix: Source Code
¹²⁴ ¹²⁴Coffee Chart (c) 2012 Michael Coghlan, some rights reserved
Appendix: Source Code
Encapsulation and Composition var __slice = [].slice; function isUndefined (value) { return typeof value === 'undefined'; } function isntUndefined (value) { return typeof value !== 'undefined'; } function isFunction (value) { return typeof value === 'function'; } //////////////////////////////////////////////////////////// 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 (Object.prototype.hasOwnProperty.call(provider, key)) { consumer[key] = provider[key]; }; }; }; return consumer; }; var policies = { overwrite: function overwrite (fn1, fn2) { return fn1; }, discard: function discard (fn1, fn2) { return fn2;
317
Appendix: Source Code
}, before: function before (fn1, fn2) { return function () { var fn1value = fn1.apply(this, arguments), fn2value = fn2.apply(this, arguments); return fn2value !== void 0 ? fn2value : fn1value; } }, after: function after (fn1, fn2) { return function () { var fn2value = fn2.apply(this, arguments), fn1value = fn1.apply(this, arguments); return fn2value !== void 0 ? fn2value : fn1value; } }, around: function around (fn1, fn2) { return function () { var argArray = [fn2.bind(this)].concat(__slice.call(arguments, 0)); return fn1.apply(this, argArray); } } }; var __slice = [].slice; 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 (Object.prototype.hasOwnProperty.call(provider, key)) { consumer[key] = provider[key]; };
318
Appendix: Source Code
}; }; return consumer; }; 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; } function methodsOfType (behaviour, list, type) { return list.filter(function (methodName) { return typeof(behaviour[methodName]) === type; }); } function propertyFlags (behaviour) { var properties = [], propertyName; for (propertyName in behaviour) { if (behaviour[propertyName] === null) { properties.push(propertyName); } } return properties; } var number = 0; function encapsulate (behaviour) {
319
Appendix: Source Code
var safekeepingName = "__" + ++number + "__", properties = Object.keys(behaviour), methods = properties.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 = properties.filter(function (methodName) { return isUndefined(behaviour[methodName]); }), methodsToProxy = publicMethods.concat(dependencies), encapsulatedObject = {}; function createContext (methodReceiver) { var innerProxy = partialProxy(methodReceiver, methodsToProxy); privateMethods.forEach(function (methodName) { innerProxy[methodName] = behaviour[methodName]; }); return Object.defineProperty( innerProxy, 'self', { writable: false, enumerable: false, value: 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;
320
Appendix: Source Code
} publicMethods.forEach(function (methodName) { var methodBody = behaviour[methodName]; Object.defineProperty(encapsulatedObject, methodName, { enumerable: true, writable: false, value: function () { var context = getContext(this), result = methodBody.apply(context, arguments); return (result === context) ? this : result; } }); }); dependencies.forEach(function (methodName) { if (encapsulatedObject[methodName] == null) { encapsulatedObject[methodName] = void 0; } }); return Object.preventExtensions(encapsulatedObject); } ///////////////////////////////////////////////////////// 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) {
321
Appendix: Source Code
return values[values.length - 1]; } } } } 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; }, Object.create(null)) } 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; }, {}); } 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);
322
Appendix: Source Code
323
} else throw "Don't know what to do with " + value; return applied; }, seed); } 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); } // shim if allong.es.callLeft not available 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); }; }; } })(); function seedFor (objectList) { var seed = objectList[0] == null ? Object.create(null) : Object.create(Object.getPrototypeOf(objectList[0])), isCompatibleWithSeed = callLeft2(canBeMergedInto, seed);
324
Appendix: Source Code
if (!objectList.every(isCompatibleWithSeed)) throw 'incompatible prototypes'; return seed; } function composeMetaobjects () { var metaobjects = __slice.call(arguments, 0), arrays = propertiesToArrays(metaobjects), resolved = resolveUndefineds(arrays), seed = seedFor(metaobjects), = applyProtocol(seed, resolved, orderStrategy2); composed return composed; } function Newable (optionalName, metaobject, optionalSuper) { var name = typeof(optionalName) === 'string' ? optionalName : '', metaobject
= typeof(optionalName) === 'string' ? metaobject
superClazz
= typeof(optionalName) === 'string' ? optionalSuper
: optionalName,
: metaobject, source = "(function " + name + " () { " + "var r = constructor.apply(this, arguments); " + "return r === undefined ? this : r; " + "})", clazz = eval(source), constructor; if (typeof(metaobject.constructor) === 'function' && typeof(optionalSuper) === \ 'function') { constructor = function () { optionalSuper.apply(this, arguments); return metaobject.constructor.apply(this, arguments); } } else if (typeof(metaobject.constructor) === 'function') { constructor = metaobject.constructor; }
Appendix: Source Code
else if (typeof(optionalSuper) === 'function') { constructor = optionalSuper; } else constructor = function () {} clazz.prototype = extend({}, metaobject); delete clazz.prototype.constructor; return clazz; }
325
Appendix: Source Code
Methods function equals (x) { return function eq (y) { return (x === y); }; } function not (fn) { var name = fn.name === '' ? "not" : "not_" + fn.name; return nameAndLength(name, fn.length, function () { return !fn.apply(this, arguments) }); } function getWith (prop, obj) { function gets (obj) { return obj[prop]; } return obj === undefined ? gets : gets(obj); } function mapWith (fn, mappable) { function maps (collection) { return collection.map(fn); } return mappable === undefined ? maps : maps(collection); } function pluckWith (prop, collection) { var plucker = mapWith(getWith(prop)); return collection === undefined ? plucker : plucker(collection);
326
Appendix: Source Code
327
} function nameAndLength(name, length, body) { var abcs = [ 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'z', 'x', 'c', 'v', 'b', 'n', 'm' ], pars = abcs.slice(0, length), src = "(function " + name + " (" + pars.join(',') + ") { return body.apply\ (this, arguments); })"; return eval(src); } function imitate(exemplar, body) { return nameAndLength(exemplar.name, exemplar.length, body); } function when (guardFn, optionalFn) { function guarded (fn) { return imitate(fn, function () { if (guardFn.apply(this, arguments)) return fn.apply(this, arguments); }); } return optionalFn == null ? guarded : guarded(optionalFn); } function Match () { var fns = [].slice.call(arguments, 0), lengths = pluckWith('length', fns), length = Math.min.apply(null, lengths), names = pluckWith('name', fns).filter(function (name) { return name !== '\ '; }), = names.length === 0 name ? '' : names[0]; return nameAndLength(name, length, function () { var i, value;
Appendix: Source Code
for (i in fns) { value = fns[i].apply(this, arguments); if (value !== undefined) return value; } }); } function isType (type) { return function (arg) { return typeof(arg) === type; }; } function instanceOf (clazz) { return function (arg) { return arg instanceof clazz; }; } function isPrototypeOf (proto) { return Object.prototype.isPrototypeOf.bind(proto); } function MatchTypes () { var matchers = [].slice.call(arguments, 0, arguments.length - 1), body = arguments[arguments.length - 1]; function typeChecked () { var i, arg, value; if (arguments.length != matchers.length) return; for (i in arguments) { arg = arguments[i]; if (!matchers[i].call(this, arg)) return; } value = body.apply(this, arguments); return value === undefined ? null
328
Appendix: Source Code
: value; } return imitate(body, typeChecked); }
329
Appendix: Source Code
330
Utility Functions Throughout this book, we have used utility functions in our code snippets and examples. Here is a list of functions we’ve borrowed from other sources. Most are from underscore¹²⁵ or allong.es¹²⁶. var allong = require('allong.es'); var _ = require('underscore');
extend extend can be found in the underscore¹²⁷ library: var extend = _.extend;
Or you can use this formulation: var __slice = [].slice; 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; };
pipeline pipeline composes functions in the order they are applied: ¹²⁵http://underscorejs.org ¹²⁶http://allong.es ¹²⁷http://underscorejs.org
Appendix: Source Code
331
var pipeline = allong.es.pipeline; function square (n) { return n * n; } function increment (n) { return n + 1; } pipeline(square, increment)(6) //=> 37
tap tap passes a value to a function, discards the result, and returns the original value: var tap = allong.es.tap; tap(6, function (n) { console.log("Hello there, " + n); }); //=> Hello there, 6 6
map map applies function to an array, returning an array of the results: var map = allong.es.map; map([1, 2, 3], function (n) { return n * n; }); //=> [1, 4, 9]
variadic variadic takes a function and returns a â&#x20AC;&#x153;variadicâ&#x20AC;? function, one that collects arguments in an array
and passes them to the last parameter:
Appendix: Source Code
var variadic = allong.es.variadic; var a = variadic(function (args) { return { args: args }; }); a(1, 2, 3, 4, 5) //=> { args: [1, 2, 3, 4, 5] } var b = variadic(function (first, second, rest) { return { first: first, second: second, rest: rest }; }); b(1, 2, 3, 4, 5) //=> { first: 1, second: 2, rest: [3, 4, 5] }
unvariadic unvariadic takes a variadic function and turns it into a function with a fixed arity: var unvariadic = allong.es.unvariadic; function ensuresArgumentsAreNumbers (fn) { return unvariadic(fn.length, function () { for (var i in arguments) { if (typeof(arguments[i]) !== 'number') { throw "Ow! NaN!!" } } return fn.apply(this, arguments); }); } function myAdd (a, b) { return a + b; } var myCheckedAdd = ensuresArgumentsAreNumbers(myAdd); myCheckedAdd.length //=> 2
332