Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Singletonable mixin #3

Merged
merged 4 commits into from
Apr 24, 2016
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
1.0.7 - Add singletonable mixin (also added to BaseObject)
1.0.6 - Add soakCall corresponding to soakApply
1.0.5 - Add soakApply and allow soak strings to be passed as function builder arguments
1.0.4 - Add applyMethod to underscore
Expand Down
2 changes: 2 additions & 0 deletions lib/baseObject.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
require('./objectUnderscore');
var superExtendable = require('./superExtendable');
var gettable = require('./gettable');
var singletonable = require('./singletonable');

// Check that the constructor was called with `new`
function badConstructorCall(obj) {
Expand Down Expand Up @@ -74,6 +75,7 @@ BaseObject.addProperties = function (varArgs) {

superExtendable(BaseObject);
gettable(BaseObject);
singletonable(BaseObject);

var originalExtend = BaseObject.extend;

Expand Down
25 changes: 25 additions & 0 deletions lib/singletonable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Add lazy singletons to Constructors. This adds a makeSingleton class
// method, which will create a singleton property of the given name, passing
// the remaining arguments to the constructor.
//
// The singleton is constructed on first access, through
// Object.defineProperty. This defers initialization, simplifying dependency
// management.

module.exports = function (Constructor) {
Constructor.makeSingleton = function (name, varargs) {
// Singleton should be of whatever makeSingleton was called on, not
// necessarily what was passed to singletonable()
var Child = this;
varargs = arguments._.tail();

// Storage for the the singleton object, once it is created
var singleton;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally I think this would be Constructor._singleton or something like that so that people could mock/override it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm... I guess that won't really work because of the name you're using, but you could provide a setter method

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a previous iteration, I had set configurable: true, thinking that would make it so subclasses could override it. But then I realized that that's not necessary for the subclass case, and took it off. Mocking is a good point though, so I think you're right.

I have a trick for this kind of situation where you define a property as a configurable property, but then set it when it's accessed, so it acts exactly like normal property. That'll be good because then we can use autoMock too.

Object.defineProperty(Child, name, {
get: function () {
if (!singleton) singleton = _.applyConstructor(Child, varargs)
return singleton;
},
})
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "intrusive",
"version": "1.0.6",
"version": "1.0.7",
"description": "a set of intrusive javascript modules, not for use in libraries",
"main": "index.js",
"scripts": {
Expand Down
77 changes: 77 additions & 0 deletions test/baseObject.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,81 @@ describe('Base object', function () {
}).to.throw;
})
})

describe('when you create a singleton', function () {
var S = BaseObject.extend({
x: 1,
y: 2,
});
S.makeSingleton('first');
S.makeSingleton('second');

it('should have created the first singleton', function () {
return expect(S.first.x).equal(1)
})

it('should have created the second singleton', function () {
return expect(S.second.x).equal(1)
})

describe('when you modify a property on one singleton', function () {
before(function () {
S.first.x = 2;
})

it('should modify the first singleton', function () {
return expect(S.first.x).equal(2);
})

it('should not modify the second singleton', function () {
return expect(S.second.x).equal(1);
})
})

describe('when you have a subclass', function () {
var T;
before(function () {
T = S.extend({
y: 3,
z: 4,
})
})

describe('when you make a new singleton', function () {
before(function () {
T.makeSingleton('third');
})

it('should not create the singleton on the parent constructor', function () {
return expect(S.third).undefined;
})

it('should create a singleton on the child constructor', function () {
return expect(T.third).not.undefined;
})

it('should create a singleton that is an instance of the child constructor', function () {
return expect(T.third.z).equal(4);
})
})

describe('when you override a singleton', function () {
before(function () {
T.makeSingleton('first');
})

it('should not change parent constructor singleton', function () {
return expect(S.first.y).equal(2);
})

it('should create a singleton on the child constructor', function () {
return expect(T.first).not.undefined;
})

it('should create a singleton that is an instance of the child constructor', function () {
return expect(T.first.z).equal(4);
})
})
})
})
})
44 changes: 44 additions & 0 deletions test/singletonable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
var init = require('./init');
var singletonable = require('../lib/singletonable');

describe('Singletonable', function () {
var constructorCalls = 0;
var Klass = function (a, b, c) {
var self = this;
constructorCalls++;
self.a = a;
self.b = b;
self.c = c;
}

singletonable(Klass);

describe('when you create a singleton', function () {
before(function () {
Klass.makeSingleton('singleton', 1, 2, 3);
})

it('should not call the constructor', function () {
return expect(constructorCalls).equal(0);
})

it('should have a property for the singleton', function () {
return expect(Klass.hasOwnProperty('singleton')).true;
})

describe('when you access the constructor', function () {
before(function () {
Klass.singleton;
Klass.singleton;
})

it('should have called the constructor once', function () {
return expect(constructorCalls).equal(1);
})

it('should have the correct properties', function () {
return expect(Klass.singleton._.pick('a', 'b', 'c')).deep.equal({a: 1, b: 2, c: 3});
})
})
})
})