Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge pull request #263 from Gozala/bug/compose-702835

fix Bug 702835 - module for doing simple inheritance f=@ochameau r=@ochameau r=@warner
  • Loading branch information...
commit 5cbb4ce4a1a6d42596bf607c766c0ea2629d8fc2 2 parents 3f45f1c + d9154f3
@Gozala Gozala authored
View
203 packages/api-utils/docs/base.md
@@ -0,0 +1,203 @@
+### Inheritance ###
+
+Doing [inheritance in JavaScript](https://developer.mozilla.org/en/Introduction_to_Object-Oriented_JavaScript)
+is both verbose and painful. Reading or writing such code requires requires
+sharp eye and lot's of discipline, mainly due to code fragmentation and lots of
+machinery exposed:
+
+ // Defining a simple Class
+ function Dog(name) {
+ // Classes are for creating instances, calling them without `new` changes
+ // behavior, which in majority cases you need to handle, so you end up
+ // with additional boilerplate.
+ if (!(this instanceof Dog)) return new Dog(name);
+
+ this.name = name;
+ };
+ // To define methods you need to make a dance with a special 'prototype'
+ // property of the constructor function. This is too much machinery exposed.
+ Dog.prototype.type = 'dog';
+ Dog.prototype.bark = function bark() {
+ return 'Ruff! Ruff!'
+ };
+
+ // Subclassing a `Dog`
+ function Pet(name, breed) {
+ // Once again we do our little dance
+ if (!(this instanceof Pet)) return new Pet(name, breed);
+
+ Dog.call(this, name);
+ this.breed = breed;
+ }
+ // To subclass, you need to make another special dance with special
+ // 'prototype' properties.
+ Pet.prototype = Object.create(Dog.prototype);
+ // If you want correct instanceof behavior you need to make a dance with
+ // another special `constructor` property of the `prototype` object.
+ Object.defineProperty(Pet.prototype, 'contsructor', { value: Pet });
+ // Finally you can define some properties.
+ Pet.prototype.call = function(name) {
+ return this.name === name ? this.bark() : '';
+ };
+
+An "exemplar" is a factory for instances. Usually exemplars are defined as
+(constructor) functions as in examples above. But that does not necessary has
+to be the case. Prototype (object) can form far more simpler exemplars. After
+all what could be more object oriented than objects that inherit from objects.
+
+ var Dog = {
+ new: function(name) {
+ var instance = Object.create(this);
+ this.initialize.apply(instance, arguments);
+ return instance;
+ },
+ initialize: function initialize(name) {
+ this.name = name;
+ },
+ type: 'dog',
+ bark: function bark() {
+ return 'Ruff! Ruff!'
+ }
+ };
+ var fluffy = Dog.new('fluffy');
+
+
+ var Pet = Object.create(Dog);
+ Pet.initialize = function initialize(name, breed) {
+ Dog.initialize.call(this, name);
+ this.breed = breed;
+ };
+ Pet.call = function call(name) {
+ return this.name === name ? this.bark() : '';
+ };
+
+While this small trick solves some readability issues, there are still more. To
+address them this module exports `Base` exemplar with few methods predefined:
+
+ var Dog = Base.extend({
+ initialize: function initialize(name) {
+ this.name = name;
+ },
+ type: 'dog',
+ bark: function bark() {
+ return 'Ruff! Ruff!'
+ }
+ });
+
+ var Pet = Dog.extend({
+ initialize: function initialize(name, breed) {
+ Dog.initialize.call(this, name);
+ this.breed = breed;
+ },
+ function call(name) {
+ return this.name === name ? this.bark() : '';
+ }
+ });
+
+ var fluffy = Dog.new('fluffy');
+ dog.bark(); // 'Ruff! Ruff!'
+ Dog.isPrototypeOf(fluffy); // true
+ Pet.isPrototypeOf(fluffy); // true
+
+### Composition ###
+
+Even though (single) inheritance is very powerful it's not always enough.
+Sometimes it's more useful suitable to define reusable pieces of functionality
+and then compose bigger pieces out of them:
+
+ var HEX = Base.extend({
+ hex: function hex() {
+ return '#' + this.color
+ }
+ })
+
+ var RGB = Base.extend({
+ red: function red() {
+ return parseInt(this.color.substr(0, 2), 16)
+ },
+ green: function green() {
+ return parseInt(this.color.substr(2, 2), 16)
+ },
+ blue: function blue() {
+ return parseInt(this.color.substr(4, 2), 16)
+ }
+ })
+
+ var CMYK = Base.extend(RGB, {
+ black: function black() {
+ var color = Math.max(Math.max(this.red(), this.green()), this.blue())
+ return (1 - color / 255).toFixed(4)
+ },
+ magenta: function magenta() {
+ var K = this.black();
+ return (((1 - this.green() / 255).toFixed(4) - K) / (1 - K)).toFixed(4)
+ },
+ yellow: function yellow() {
+ var K = this.black();
+ return (((1 - this.blue() / 255).toFixed(4) - K) / (1 - K)).toFixed(4)
+ },
+ cyan: function cyan() {
+ var K = this.black();
+ return (((1 - this.red() / 255).toFixed(4) - K) / (1 - K)).toFixed(4)
+ }
+ })
+
+ // Composing `Color` prototype out of reusable components:
+ var Color = Base.extend(HEX, RGB, CMYK, {
+ initialize: function initialize(color) {
+ this.color = color
+ }
+ })
+
+ var pink = Color.new('FFC0CB')
+ // RGB
+ pink.red() // 255
+ pink.green() // 192
+ pink.blue() // 203
+
+ // CMYK
+ pink.magenta() // 0.2471
+ pink.yellow() // 0.2039
+ pink.cyan() // 0.0000
+
+### Combining composition & inheritance ###
+
+Also it's easy to mix composition with inheritance:
+
+ var Pixel = Color.extend({
+ initialize: function initialize(x, y, color) {
+ Color.initialize.call(this, color)
+ this.x = x
+ this.y = y
+ },
+ toString: function toString() {
+ return this.x + ':' + this.y + '@' + this.hex()
+ }
+ });
+
+ var pixel = Pixel.new(11, 23, 'CC3399');
+ pixel.toString() // 11:23@#CC3399
+ Pixel.isPrototypeOf(pixel) // true
+
+ // Pixel instances inhertis from `Color`
+ Color.isPrototypeOf(pixel); // true
+
+ // In fact `Pixel` itself inherits from `Color`, remember just simple and
+ // pure prototypal inheritance where object inherit from objects.
+ Color.isPrototypeOf(Pixel); // true
+
+### Classes ###
+
+Module exports `Class` function. `Class` takes argument of exemplar object
+extending `Base` and returns `constructor` function that can be used for
+simulating classes defined by given exemplar.
+
+ var CPixel = Class(Pixel);
+ var pixel = CPixel(11, 12, '000000');
+ pixel instanceof CPixel // true
+ Pixel.prototypeOf(pixel); // true
+
+ // Use of `new` is optional, but possible.
+ var p2 = CPixel(17, 2, 'cccccc');
+ p2 instanceof CPixel // true
+ p2.prototypeOf(pixel); // true
View
209 packages/api-utils/lib/base.js
@@ -0,0 +1,209 @@
+/* vim:set ts=2 sw=2 sts=2 expandtab */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Jetpack.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Irakli Gozalishvili <gozala@mozilla.com> (Original Author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+"use strict";
+
+// Instead of inheriting from `Object.prototype` we copy all interesting
+// properties from it and then freeze. This way we can guarantee integrity
+// of components build on top.
+exports.Base = Object.freeze(Object.create(null, {
+ toString: { value: Object.prototype.toString },
+ toLocaleString: { value: Object.prototype.toLocaleString },
+ toSource: { value: Object.prototype.toSource },
+ valueOf: { value: Object.prototype.valueOf },
+ isPrototypeOf: { value: Object.prototype.isPrototypeOf },
+ /**
+ * Creates an object that inherits from `this` object (Analog of
+ * `new Object()`).
+ * @examples
+ *
+ * var Dog = Base.extend({
+ * bark: function bark() {
+ * return 'Ruff! Ruff!'
+ * }
+ * });
+ * var dog = Dog.new();
+ */
+ new: { value: function create() {
+ var object = Object.create(this);
+ object.initialize.apply(object, arguments);
+ return object;
+ }},
+ /**
+ * When new instance of the this prototype is created it's `initialize`
+ * method is called with all the arguments passed to the `new`. You can
+ * override `initialize` to set up an instance.
+ */
+ initialize: { value: function initialize() {
+ }},
+ /**
+ * Merges all the properties of the passed objects into `this` instance (This
+ * method can be used on instances only as prototype objects are frozen).
+ *
+ * If two or more argument objects have own properties with the same name,
+ * the property is overridden, with precedence from right to left, implying,
+ * that properties of the object on the left are overridden by a same named
+ * property of the object on the right.
+ *
+ * @examples
+ *
+ * var Pet = Dog.extend({
+ * initialize: function initialize(options) {
+ * // this.name = options.name -> would have thrown (frozen prototype)
+ * this.merge(options) // will override all properties.
+ * },
+ * call: function(name) {
+ * return this.name === name ? this.bark() : ''
+ * },
+ * name: null
+ * })
+ * var pet = Pet.new({ name: 'Benzy', breed: 'Labrador' })
+ * pet.call('Benzy') // 'Ruff! Ruff!'
+ */
+ merge: { value: function merge() {
+ var descriptor = {};
+ Array.prototype.forEach.call(arguments, function (properties) {
+ Object.getOwnPropertyNames(properties).forEach(function(name) {
+ descriptor[name] = Object.getOwnPropertyDescriptor(properties, name);
+ });
+ });
+ Object.defineProperties(this, descriptor);
+ return this;
+ }},
+ /**
+ * Takes any number of argument objects and returns frozen, composite object
+ * that inherits from `this` object and combines all of the own properties of
+ * the argument objects. (Objects returned by this function are frozen as
+ * they are intended to be used as types).
+ *
+ * If two or more argument objects have own properties with the same name,
+ * the property is overridden, with precedence from right to left, implying,
+ * that properties of the object on the left are overridden by a same named
+ * property of the object on the right.
+ * @examples
+ *
+ * // ## Object composition ##
+ *
+ * var HEX = Base.extend({
+ * hex: function hex() {
+ * return '#' + this.color;
+ * }
+ * })
+ *
+ * var RGB = Base.extend({
+ * red: function red() {
+ * return parseInt(this.color.substr(0, 2), 16);
+ * },
+ * green: function green() {
+ * return parseInt(this.color.substr(2, 2), 16);
+ * },
+ * blue: function blue() {
+ * return parseInt(this.color.substr(4, 2), 16);
+ * }
+ * })
+ *
+ * var CMYK = Base.extend(RGB, {
+ * black: function black() {
+ * var color = Math.max(Math.max(this.red(), this.green()), this.blue());
+ * return (1 - color / 255).toFixed(4);
+ * },
+ * cyan: function cyan() {
+ * var K = this.black();
+ * return (((1 - this.red() / 255).toFixed(4) - K) / (1 - K)).toFixed(4);
+ * },
+ * magenta: function magenta() {
+ * var K = this.black();
+ * return (((1 - this.green() / 255).toFixed(4) - K) / (1 - K)).toFixed(4);
+ * },
+ * yellow: function yellow() {
+ * var K = this.black();
+ * return (((1 - this.blue() / 255).toFixed(4) - K) / (1 - K)).toFixed(4);
+ * }
+ * })
+ *
+ * var Color = Base.extend(HEX, RGB, CMYK, {
+ * initialize: function Color(color) {
+ * this.color = color;
+ * }
+ * });
+ *
+ * // ## Prototypal inheritance ##
+ *
+ * var Pixel = Color.extend({
+ * initialize: function Pixel(x, y, hex) {
+ * Color.initialize.call(this, hex);
+ * this.x = x;
+ * this.y = y;
+ * },
+ * toString: function toString() {
+ * return this.x + ':' + this.y + '@' + this.hex();
+ * }
+ * });
+ *
+ * var pixel = Pixel.new(11, 23, 'CC3399')
+ * pixel.toString(); // 11:23@#CC3399
+ *
+ * pixel.red(); // 204
+ * pixel.green(); // 51
+ * pixel.blue(); // 153
+ *
+ * pixel.cyan(); // 0.0000
+ * pixel.magenta(); // 0.7500
+ * pixel.yellow(); // 0.2500
+ *
+ */
+ extend: { value: function extend() {
+ return Object.freeze(this.merge.apply(Object.create(this), arguments));
+ }}
+}));
+
+/**
+ * Function takes prototype object that implements `initialize` method, and
+ * returns `constructor` function (with correct prototype property), that can
+ * be used for simulating classes for given prototypes.
+ */
+exports.Class = Object.freeze(function Class(prototype) {
+ function constructor() {
+ var instance = Object.create(prototype);
+ prototype.initialize.apply(instance, arguments);
+ return instance;
+ }
+ return Object.freeze(Object.defineProperties(constructor, {
+ prototype: { value: prototype },
+ new: { value: constructor }
+ }));
+});
View
236 packages/api-utils/tests/test-base.js
@@ -0,0 +1,236 @@
+"use strict";
+
+var { Base, Class } = require("api-utils/base");
+
+exports["test .isPrototypeOf"] = function(assert) {
+ assert.ok(Base.isPrototypeOf(Base.new()),
+ "Base is a prototype of Base.new()");
+ assert.ok(Base.isPrototypeOf(Base.extend()),
+ "Base is a prototype of Base.extned()");
+ assert.ok(Base.isPrototypeOf(Base.extend().new()),
+ "Base is a prototoype of Base.extend().new()");
+ assert.ok(!Base.extend().isPrototypeOf(Base.extend()),
+ "Base.extend() in not prototype of Base.extend()");
+ assert.ok(!Base.extend().isPrototypeOf(Base.new()),
+ "Base.extend() is not prototype of Base.new()");
+ assert.ok(!Base.new().isPrototypeOf(Base.extend()),
+ "Base.new() is not prototype of Base.extend()");
+ assert.ok(!Base.new().isPrototypeOf(Base.new()),
+ "Base.new() is not prototype of Base.new()");
+};
+
+exports["test inheritance"] = function(assert) {
+ var Parent = Base.extend({
+ name: "parent",
+ method: function () {
+ return "hello " + this.name;
+ }
+ });
+
+ assert.equal(Parent.name, "parent", "Parent name is parent");
+ assert.equal(Parent.method(), "hello parent", "method works on prototype");
+ assert.equal(Parent.new().name, Parent.name, "Parent instance inherits name");
+ assert.equal(Parent.new().method(), Parent.method(),
+ "method behaves same on the prototype");
+ assert.equal(Parent.extend({}).name, Parent.name,
+ "Parent decedent inherits name");
+
+ var Child = Parent.extend({ name: "child" });
+ assert.notEqual(Child.name, Parent.name, "Child overides name");
+ assert.equal(Child.new().name, Child.name, "Child intsances inherit name");
+ assert.equal(Child.extend().name, Child.name,
+ "Child decedents inherit name");
+
+ assert.equal(Child.method, Parent.method, "Child inherits method");
+ assert.equal(Child.extend().method, Parent.method,
+ "Child decedent inherit method");
+ assert.equal(Child.new().method, Parent.method,
+ "Child instances inherit method");
+
+ assert.equal(Child.method(), "hello child",
+ "method refers to instance proprety");
+ assert.equal(Child.extend({ name: "decedent" }).new().method(),
+ "hello decedent", "method may be overrided");
+};
+
+exports["test prototype immutability"] = function(assert) {
+
+ assert.throws(function() {
+ var override = function() {};
+ Base.extend = override;
+ if (Base.extend !== override)
+ throw Error("Property was not set");
+ }, "Base prototype is imutable");
+
+ assert.throws(function() {
+ Base.foo = "bar";
+ if (Base.foo !== "bar")
+ throw Error("Property was not set");
+ }, "Base prototype is non-configurabel");
+
+ assert.throws(function() {
+ delete Base.new;
+ if ('new' in Base)
+ throw Error('Property was not deleted');
+ }, "Can't delete properties on prototype");
+
+ var Foo = Base.extend({
+ name: 'hello',
+ rename: function rename(name) {
+ this.name = name;
+ }
+ });
+
+ assert.throws(function() {
+ var override = function() {};
+ Foo.extend = override;
+ if (Foo.extend !== override)
+ throw Error("Property was not set");
+ }, "Can't change prototype properties");
+
+ assert.throws(function() {
+ Foo.foo = "bar";
+ if (Foo.foo !== "bar")
+ throw Error("Property was not set");
+ }, "Can't add prototype properties");
+
+ assert.throws(function() {
+ delete Foo.name;
+ if ('new' in Foo)
+ throw Error('Property was not deleted');
+ }, "Can't remove prototype properties");
+
+ assert.throws(function() {
+ Foo.rename("new name");
+ if (Foo.name !== "new name")
+ throw Error("Property was not modified");
+ }, "Method's can't mutate prototypes");
+
+ var Bar = Foo.extend({
+ rename: function rename() {
+ return this.name;
+ }
+ });
+
+ assert.equal(Bar.rename(), Foo.name,
+ "properties may be overided on decedents");
+};
+
+exports['test instance mutability'] = function(assert) {
+ var Foo = Base.extend({
+ name: "foo",
+ init: function init(number) {
+ this.number = number;
+ }
+ });
+ var f1 = Foo.new();
+ /* V8 does not supports this yet!
+ assert.throws(function() {
+ f1.name = "f1";
+ }, "can't change prototype properties");
+ */
+ f1.alias = "f1";
+ assert.equal(f1.alias, "f1", "instance is mutable");
+ delete f1.alias;
+ assert.ok(!('alias' in f1), "own properties are deletable");
+ f1.init(1);
+ assert.equal(f1.number, 1, "method can mutate instance's own properties");
+};
+
+exports['test super'] = function(assert) {
+ var Foo = Base.extend({
+ initialize: function Foo(options) {
+ this.name = options.name;
+ }
+ });
+
+ var Bar = Foo.extend({
+ initialize: function Bar(options) {
+ Foo.initialize.call(this, options);
+ this.type = 'bar';
+ }
+ });
+
+ var bar = Bar.new({ name: 'test' });
+
+ assert.ok(Bar.isPrototypeOf(bar), 'Bar is prototype of Bar.new');
+ assert.ok(Foo.isPrototypeOf(bar), 'Foo is prototype of Bar.new');
+ assert.ok(Base.isPrototypeOf(bar), 'Base is prototype of Bar.new');
+ assert.equal(bar.type, 'bar', 'bar initializer was called');
+ assert.equal(bar.name, 'test', 'bar initializer called Foo initializer');
+};
+
+exports['test class'] = function(assert) {
+ var Foo = Base.extend({
+ type: 'Foo',
+ initialize: function(options) {
+ this.name = options.name;
+ },
+ serialize: function serialize() {
+ return '<' + this.name + ':' + this.type + '>';
+ }
+ });
+ var CFoo = Class(Foo);
+ var f1 = CFoo({ name: 'f1' });
+ var f2 = new CFoo({ name: 'f2' });
+ var f3 = CFoo.new({ name: 'f3' });
+ var f4 = Foo.new({ name: 'f4' });
+
+ assert.ok(f1 instanceof CFoo, 'correct instanceof');
+ assert.equal(f1.name, 'f1', 'property initialized');
+ assert.equal(f1.serialize(), '<f1:Foo>', 'method works');
+
+ assert.ok(f2 instanceof CFoo, 'correct instanceof when created with new')
+ assert.equal(f2.name, 'f2', 'property initialized');
+ assert.equal(f2.serialize(), '<f2:Foo>', 'method works');
+
+ assert.ok(f3 instanceof CFoo, 'correct instanceof when created with .new')
+ assert.equal(f3.name, 'f3', 'property initialized');
+ assert.equal(f3.serialize(), '<f3:Foo>', 'method works');
+
+ assert.ok(f4 instanceof CFoo, 'correct instanceof when created from prototype')
+ assert.equal(f4.name, 'f4', 'property initialized');
+ assert.equal(f4.serialize(), '<f4:Foo>', 'method works');
+
+ var Bar = Foo.extend({
+ type: 'Bar',
+ initialize: function(options) {
+ this.size = options.size;
+ Foo.initialize.call(this, options);
+ }
+ });
+ var CBar = Class(Bar);
+
+
+ var b1 = CBar({ name: 'b1', size: 1 });
+ var b2 = new CBar({ name: 'b2', size: 2 });
+ var b3 = CBar.new({ name: 'b3', size: 3 });
+ var b4 = Bar.new({ name: 'b4', size: 4 });
+
+ assert.ok(b1 instanceof CFoo, 'correct instanceof');
+ assert.ok(b1 instanceof CBar, 'correct instanceof');
+ assert.equal(b1.name, 'b1', 'property initialized');
+ assert.equal(b1.size, 1, 'property initialized');
+ assert.equal(b1.serialize(), '<b1:Bar>', 'method works');
+
+ assert.ok(b2 instanceof CFoo, 'correct instanceof when created with new');
+ assert.ok(b2 instanceof CBar, 'correct instanceof when created with new');
+ assert.equal(b2.name, 'b2', 'property initialized');
+ assert.equal(b2.size, 2, 'property initialized');
+ assert.equal(b2.serialize(), '<b2:Bar>', 'method works');
+
+ assert.ok(b3 instanceof CFoo, 'correct instanceof when created with .new');
+ assert.ok(b3 instanceof CBar, 'correct instanceof when created with .new');
+ assert.equal(b3.name, 'b3', 'property initialized');
+ assert.equal(b3.size, 3, 'property initialized');
+ assert.equal(b3.serialize(), '<b3:Bar>', 'method works');
+
+ assert.ok(b4 instanceof CFoo, 'correct instanceof when created from prototype');
+ assert.ok(b4 instanceof CBar, 'correct instanceof when created from prototype');
+ assert.equal(b4.name, 'b4', 'property initialized');
+ assert.equal(b4.size, 4, 'property initialized');
+ assert.equal(b4.serialize(), '<b4:Bar>', 'method works');
+};
+
+require("test").run(exports);
+
Please sign in to comment.
Something went wrong with that request. Please try again.