Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Improving the implementation of a plugin architecture

Tags:

javascript

I have an extend method included in my library, making it possible for methods to be added to the core library:

library.prototype.extend = function(name,plugin) {

    library.prototype[name] = plugin.init;

    for (var method in plugin) {

        if(method !== 'init') {

            library.prototype[name].prototype[method] = plugin[method]; 

        }

    }

};

In use it looks like so:

library.prototype.extend('aReallyInterestingPlugin', {

    //the init method gets added to the base libraries prototype
    init: function() {

        this.shouldBePrivate;

    },

    //another other methods are created as a prototype of the newly added Plugin
    anotherMethod : function() {
        //this should have access to the shouldBePrivate var
    }
});

Users are then able to call the plugin like so:

var test = new library();
test.aReallyInterestingPlugin();

This works but I'm not exactly happy with the approach and have been trying to find an alternative pattern to make this work.

The problem with it, is that the init and the anotherMethod are added directly to the libraries prototype chain so their scope is also the global libraries scope which is messy because if any instance variables are declared (like shouldBePrivate above) they are also added to the libraries prototype chain.

How can I enable the plugin to be added and have it's own private scope? One way I've thought of is that a plugin could always be called as a constructor (and will therefore have it's own scope and this context) but then I'm not sure how clean that is...for instance for that to work the user would have to do something like this when calling the plugin:

var test = new library();
test.aPlugin = new aReallyInterestingPlugin();
like image 212
Mike Rifgin Avatar asked Nov 01 '22 09:11

Mike Rifgin


1 Answers

What you could do is to have the plugin method bound to a new object, so that they don't 'pollute' the Library.
But instead of having the plugin as a method of a Library instance, have it rather as a lazy getter, so the syntax is more fluid, and you can build a new instance only if required.
The plugin syntax could then be simplified : just use a javascript class => a function that has methods defined on its prototype.
I think also that it makes sense to have 'extend' as a property of Library, not a method set on its prototype, since no instance should make use of it.

With this you can add a plugin with

library.extend('aReallyInterestingPlugin',AReallyInterestingPluginClass);

to use you can write

var myLibrary = new Library();

myLibrary.somePlugin.someMethod(arg1, arg2, ...);

The code would look like :

library.extend = function(name,plugin) {    
    var pluginInstance = null; // lazy evaluation to avoid useless memory consumption

    var pluginGetter = function() {
       if (pluginInstance == null) pluginInstance = new plugin();
       return pluginInstance;                                       };

    Object.defineProperty( Library.prototype, name, 
                                       { get: pluginGetter, enumerable : true } ); 
} ;

A plugin is just standard javascript class:

 function MyPlugin() {
      this.pluginProperty1 = 'some value';
 }

 MyPlugin.prototype = {
      method1 : function() { /* do things */} ,
      method2 : function() { /* do other things */ }
 };

Notice that with this scheme the plugin is a singleton, i.e. every instances of Library will return the same object when asked for the same plugin.

If you prefer once plugin instance per Library, just have the Library constructor hold the plugins instances. (maybe in a hidden property).

function Library() {
    // the code allready here...        
    var pluginInstances = {};
    Object.defineProperty(this, 'pluginInstances', 
                  { get : function() { return pluginInstances }, enumerable : false });
}

library.extend = function(name,plugin) {        
    var pluginGetter = function() {
       if (! this.pluginInstances[name] ) this.pluginInstances[name] = new plugin();
       return this.pluginInstances[name];
    };

    Object.defineProperty( Library.prototype, name, 
                                       { get: pluginGetter, enumerable : true } );    
} ;

syntax for the plugin and for the use remains the same.

Edit : for older Browser support, you can still use a function instead of a getter :

function Library() {
    // the code allready here...        
    this.pluginInstances= {} ;
}

library.extend = function(name,plugin) {        
    Library.prototype[name] = function() {
       if (! this.pluginInstances[name] ) this.pluginInstances[name] = new plugin();
       return this.pluginInstances[name];
    };    
} ;

to use it you would do :

var myLibrary = new Library();

myLibrary.somePlugin().someMethod(arg1, arg2, ...);

Edit 2 : version with singleton plugins and with no getters is :

 function Library() {        /* same code */    }

library.extend = function(name,plugin)    {     
    var pluginInstance = null; // lazy evaluation to avoid useless memory consumption    
    Library.prototype[name] = function() {
       if (pluginInstance == null) pluginInstance = new plugin();
       return pluginInstance;                                       };
}
like image 193
GameAlchemist Avatar answered Nov 14 '22 22:11

GameAlchemist