Whether or not Design Patterns in oriented programming language are a language smell is another story (even if I could not personally agree more). I wanted to write about the following affirmations in Mastering JavaScript Design Patterns:
« [...] we don't have that ability in JavaScript: constructors cannot be private. Thus, we do the best we can and return the current instance from the constructor.» MJDP
« [...] JavaScript makes the problem even worse. It isn't possible to create a clean implementation of the Singleton pattern due to the restrictions on the constructor.» MJDP
Challenge Accepted.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function Wall() { | |
this.height = 0; | |
if (Wall._instance) | |
return Wall._instance; | |
Wall._instance = this; | |
} | |
Wall.getInstance = function () { | |
if (!Wall._instance) { | |
Wall._instance = new Wall(); | |
} | |
return Wall._instance; | |
}; | |
Wall._instance = null; | |
Wall.prototype.setHeight = function (height) { | |
this.height = height; | |
}; | |
Wall.prototype.getStatus = function () { | |
console.log("Wall is " + this.height + " meters tall"); | |
}; |
The second check inside the Wall constructor is hideous and we can definitely hide the constructor thanks to a closure, leveraging JavaScript functional nature. I thus gave this affirmation as an exercise for my students and here is a possible solution:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var Wall = (function () { | |
var _instance = null; | |
function Wall() { | |
this.height = 0; | |
} | |
Wall.prototype.getStatus = function () { | |
console.log("Wall is " + this.height + " meters tall"); | |
}; | |
return { | |
getInstance: function () { | |
if (!_instance) { | |
_instance = new Wall(); | |
_instance.__proto__.constructor = function(){}; // hide constructor | |
} | |
return _instance; | |
} | |
}; | |
})(); |
But we can go even further, why bother duplicating singleton logic when you can make a Singleton factory:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Singleton Factory | |
var Singleton = { | |
create: function (Constructor) { | |
var _instance; | |
function privateConstructor() { | |
throw new ReferenceError('Private constructor'); | |
} | |
return { | |
getInstance: function ( /*args*/ ) { | |
if (!_instance) { | |
_instance = Object.create(Constructor.prototype); // create object | |
Constructor.apply(_instance, arguments); // apply constructor | |
_instance.__proto__.constructor = privateConstructor; // hide constructor | |
} | |
return _instance; | |
} | |
}; | |
} | |
}; | |
// Usage | |
var Wall = (function (Singleton) { // We should use a DI there | |
function Wall(height) { | |
this.height = height; | |
} | |
Wall.prototype.getStatus = function () { | |
console.log("Wall is " + this.height + " meters tall"); | |
}; | |
// The implementation and the singleton creation still needs to be encapsulated | |
return Singleton.create(Wall); | |
})(Singleton); |
Bonus: a lot of Singleton implementations available on Internet forgot to hide the constructor, even Addy Osmani books on JavaScript Design Patterns forgot this:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//This "SingletonTester" is not a valid Singleton because: | |
var InnerConstructor = SingletonTester.getInstance({ | |
pointX: 5 | |
}).constructor; | |
console.log(new InnerConstructor() !== new InnerConstructor()); // true |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// From http://addyosmani.com/resources/essentialjsdesignpatterns/book/#singletonpatternjavascript | |
var SingletonTester = (function () { | |
// options: an object containing configuration options for the singleton | |
// e.g var options = { name: "test", pointX: 5}; | |
function Singleton( options ) { | |
// set options to the options supplied | |
// or an empty object if none are provided | |
options = options || {}; | |
// set some properties for our singleton | |
this.name = "SingletonTester"; | |
this.pointX = options.pointX || 6; | |
this.pointY = options.pointY || 10; | |
} | |
// our instance holder | |
var instance; | |
// an emulation of static variables and methods | |
var _static = { | |
name: "SingletonTester", | |
// Method for getting an instance. It returns | |
// a singleton instance of a singleton object | |
getInstance: function( options ) { | |
if( instance === undefined ) { | |
instance = new Singleton( options ); | |
} | |
return instance; | |
} | |
}; | |
return _static; | |
})(); | |
var singletonTest = SingletonTester.getInstance({ | |
pointX: 5 | |
}); | |
// Log the output of pointX just to verify it is correct | |
// Outputs: 5 | |
console.log( singletonTest.pointX ); |
[Update] Supporting static methods and leveraging Object.create features instead of redefining __proto__.constructor:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Singleton Factory | |
var Singleton = { | |
create: function (Constructor) { | |
var _instance; | |
function privateConstructor() { | |
throw new ReferenceError('Private constructor'); | |
} | |
// using lodash extend #lazy | |
return _.extend({}, Constructor, { | |
getInstance: function ( /*args*/ ) { | |
if (!_instance) { | |
_instance = Object.create(Constructor.prototype, { | |
constructor: { | |
get: privateConstructor | |
} | |
}); // create object with private constructor | |
Constructor.apply(_instance, arguments); // apply constructor | |
} | |
return _instance; | |
} | |
}); | |
} | |
}; | |
var Wall = (function (Singleton) { // We should use a DI there | |
function Wall(height) { | |
this.height = height; | |
} | |
Wall.prototype.getStatus = function () { | |
console.log("Wall is " + this.height + " meters tall"); | |
}; | |
Wall.staticMethod = function () { | |
return true; | |
}; | |
// The implementation and the singleton creation still needs to be encapsulated | |
return Singleton.create(Wall); | |
})(Singleton); | |
console.log(Wall.staticMethod()); // true | |
console.log(Wall.getInstance() === Wall.getInstance()); // true |