extensible v3.1.0
extensible
Create highly extensible software components.
Installation
npm install --save extensible
Introduction
This library simplifies modularization of cross-cutting concerns in libraries/applications. It's a simple framework for doing aspect-oriented programming in javascript software.
Objects created by the exported function can be extended with methods(empty prototypes) and middlewares that implement aspects of the object's methods.
Usage
For maximum reuse, middlewares should be very small and keep their knowledge of other installed middlewares to a minimum. In practice, they will know something about middlewares installed in deeper layers.
The best way to understand is through an example that shows its features. We will build a tiny database library based on leveldb(leveldown) which can be extended via plugins. The following code defines the core API and the innermost middleware/layer:
// levelup.js
var leveldown = require('leveldown');
var extensible = require('extensible');
var levelup = extensible();
// Add basic methods:
levelup.$defineMethod('open', 'location, cb');
levelup.$defineMethod('get', 'key, cb');
levelup.$defineMethod('put', 'key, value, cb');
levelup.$defineMethod('del', 'key, cb');
// Now add the innermost layer, which implements the core database methods:
levelup.$use({
open: function(location, cb) {
this.db = leveldown(location);
this.db.open(cb);
},
get: function(key, cb) {
this.db.get(key, cb);
},
put: function(key, value, cb) {
this.db.put(key, value, cb);
},
del: function(key, cb) {
this.db.del(key, cb);
}
});
// Export a constructor
module.exports = function(options) {
return levelup.$fork();
};
This will result in a very simple but working database API:
var levelup = require('./levelup');
var db = levelup();
var k = new Buffer([1, 2, 3]);
var v = new Buffer([1, 2, 3, 4]);
db.open('./db-example', function(err) {
db.put(k, v, function(err) {
db.get(k, function(err, val) {
console.error(val); // <SlowBuffer 01 02 03 04>
});
});
});
The created module only supports buffers/strings as keys/values. Lets build a plugin which adds support to using arbitrary objects. We will use 'msgpack-js' for serializing values and 'bytewise' for serializing keys:
// levelup-pack.js
var bytewise = require('bytewise');
var msgpack = require('msgpack-js');
// Since this is not the innermost layer, we will use the last argument, 'next'
// to invoke the next layer.
//
// All methods receive keys so they must be serialized with bytewise for a
// couchdb-like ordering of records.
//
// Methods that return values(get) must also override the callback to convert
// the buffer back to javascript objects.
module.exports = {
get: function(key, cb, next) {
next(bytewise.encode(key), function(err, value) {
if (err) return cb(err);
cb(null, msgpack.decode(value));
});
},
put: function(key, value, cb, next) {
next(bytewise.encode(key), msgpack.encode(value), cb);
},
del: function(key, cb, next) {
next(bytewise.encode(key), cb);
}
};
Note that we havent altered the 'open' method. When a middleware doesn't implement a method, it will automatically invoke next layer.
To use the new feature, install the middleware into the db object, which will wrap it into another layer:
var levelup = require('./levelup');
var levelupPack = require('./levelup-pack');
var db = levelup();
// wrap into the serialization layer
db.$use(levelupPack);
var k = [1, 2, 3];
var v = {name: 'john doe'};
db.open('./db-example', function(err) {
db.put(k, v, function(err) {
db.get(k, function(err, val) {
console.error(val); // { name: 'john doe' }
});
});
});
Middlewares can also be functions, which are called with the context set to the object being extended. To illustrate lets build a plugin which converts our API to return objects implementing the Promises/A+ spec through the 'rsvp' promise library.
This example will also show how to perform instrospection and modify a method signature while maintaining compatibility with previous layers:
// levelup-promise.js
var rsvp = require('rsvp');
module.exports = function() {
var _this = this; // reference to the object
var rv = {};
// this assumes all methods follow node convention of callback as last arg
this.$eachMethodDescriptor(function(method) {
// redefine the method signature by removing the last 'cb' parameter
var newArgs = method.args.slice();
var lastArg = newArgs.pop();
// Only wrap if the last argument is named 'cb'
if (lastArg !== 'cb')
return;
_this.$defineMethod(method.name, newArgs.join(','));
rv[method.name] = function() {
var next = arguments[arguments.length - 4];
// get all args up to and excluding 'next'
var args = Array.prototype.slice.call(arguments, 0, arguments.length - 4);
return new rsvp.Promise(function(resolve, reject) {
// push the callback for the next layer, which still has the old
// method signature
args.push(function(err, result) {
if (err) return reject(err);
// resolve passing all values returned
resolve(result);
});
next.apply(this, args);
});
};
});
return rv;
};
Now install on the db object to wrap it into another layer:
var levelup = require('./levelup');
var levelupPack = require('./levelup-pack');
var levelupPromise = require('./levelup-promise');
var db = levelup();
// serialization
db.$use(levelupPack);
// promises
db.$use(levelupPromise);
var k1 = [1, 2, 3], k2 = [4, 5, 6];
var v1 = {name: 'foo'}, v2 = {name: 'bar'};
db.open('./db-example').then(function(err) {
return db.put(k1, v1);
}).then(function() {
return db.put(k2, v2);
}).then(function() {
return db.get(k2);
}).then(function(val) {
console.log(val); // {name: 'bar'}
return db.get(k1);
}).then(function(val) {
console.log(val); // {name: 'foo'}
}).catch(function(err) {
console.error(err);
});
API
- extensible()
- extensible#$defineMethod()
- extensible#$use()
- extensible#$getMethodDescriptor()
- extensible#$eachMethodDescriptor()
- extensible#$eachLayer()
- extensible#$fork()
- extensible#$instance()
- extensible#$instanceOf()
extensible()
The extensible
constructor function returns a new, empty object which can
be the base of a new extensible component.
extensible#$defineMethod(name[, args, descriptor])
Defines a new empty method on the object. The object has no behavior until a
middleware implementing some aspect is installed with use()
. Its possible to
redefine an existing method with a different number of arguments/signature, in
which case the middleware must take care of adapting the arguments for the next
layer(which still uses the old signature).
name
is the method name and may be any valid javascript property name.
args
is a string with comma-separated parameter names which are used to
generate the middleware wrapper functions, so it must match the middleware's
method signature.
descriptor
is an object containing metadata that can be discovered and
introspected later, possibly by other middlewares/plugins. The object passed to
descriptor
is merged with an object with the {name(string), args(array)}
schema.
extensible#$use(middleware, opts)
Extend object with middleware
, which will become the new top layer.
middleware
may implement any of the methods already defined with
defineMethod()
. Other methods are simply ignored, even if they are added
later.
The implemented methods must have the same number of arguments passed to the
last defineMethod()
call. It may can optionally use the next
,
layer
, state
and self
arguments described as follows:
next
: Helper function to call the next middleware layer. This function has the same parameters declared withdefineMethod()
and may also accept an extrastate
described below. This must not be called if the middleware was the first added to the object(it is the bottom layer). If the method was upgraded,next
will have the signature of the method defined in the next middleware layer.layer
: Object that wraps the current middleware and has a reference to the next middleware through a 'next' property. This can be used to call another method in a lower layer without passing through the whole middleware pipeline.state
: Argument which is passed implicitly through the middleware pipeline and may be modified by any of the invoked middlewares. One use case for this is to pass options to a non-adjacent lower middleware separated by middlewares with 'incompatible signatures'.self
: Reference to the extensible object. This is only used by the special$call
method(See the '$fork' method below) for when the callable object is called like a method(this
no longer points to the extensible object).
If middleware
is a function it will be treated as a factory and called with
the object being extended as context(this
) and opts
as argument.
extensible#$getMethodDescriptor(name)
Returns the descriptor
object for a previously defined method.
extensible#$eachMethodDescriptor(cb)
Invokes cb
for each method descriptor defined in the object. The iteration
order is not predictable.
extensible#$eachLayer(cb)
Invokes cb
for each layer(object wrapping a middleware) in the object. The
iteration order is bottom->top (middlewares installed first are visited first).
extensible#$fork([asCallable, inheritProperties])
Forks by creating a new object with all methods and layers from the current object. It may be called with two optional arguments:
asCallable
: The forked object is callable, meaning that a function is returned. To implement the function behavior, implement the $call method (Read below). For most purposes, it may be used as a normalextensible()
that can be called like a function.inheritProperties
: If true, the parent properties will be inherited through the prototype chain instead of simply copied. If a falsy value is passed(the default) the forked object will contain separate properties for the layers and descriptors, so it may be extended independently of the original object.
extensible#$instance()
For normal objects, this creates a child object linked through the prototype
chain(Unlike $fork, the child object cannot be extended independently).
If the object is callable, the properties/methods are simply copied(normal
javascript inheritance becomes broken for this object and its children).
This will call a $constructor
method if defined, forwarding any passed
arguments.
extensible#$instanceOf()
Replacement for instanceof
that works with callable objects.
Special methods
Extensible objects can implement the following special methods(in the same middleware-based architecture of normal methods):
$call
: This implements the behavior of calling callable objects.$constructor
: Initializer called with arguments passed to$instance