Hi,
Apologies in advance, this turned out to be a really long post.
Found a reasonable attempt by Jim Higgson at
http://jimhigson.blogspot.com/2009/01/prototype-singleton-classes.html
but didn't like the fact that if you called new again it threw an
exception.
I think the point of Jim's article there is that *all* calls to `new`
will throw an exception except for the one call that's performed
*internally* as part of the definition of the class. So users of the
class will never be able to use `new`, not even once. That's as close
to making `new` private as you're going to get, a `new` that always
fails and tells you the right thing to do. :-) (There are some issues
with the actual code in that article, but the principle is sound.)
To make it work like that I have used the following code. Comments on
anything that you don't like, or that might cause an issue would be
appreciated.
What you've created there isn't really a singleton so much as multiple
instance facades sharing the same underlying state. It works, I'm not
criticising, but in a true singleton, there is *one* instance.
Stepping back: There's very little (if any) call for singleton
*classes* in JavaScript. (This is probably why you're not seeing more
discussion of them as relates to Prototype.) Singleton *classes* are
largely a way to work around the fact that in most class-based OOP
languages, the only way to create objects with behaviors (methods) is
to define a class and then create an instance of it. This isn't true
in JavaScript; objects are just objects, and objects can have
behaviors all of their own:
var bar = {
foo: 42,
inc: function() {
++this.foo;
}
};
// Usage:
alert(bar.foo); // alerts 42
bar.inc();
alert(bar.foo); // alerts 43
But you *can* create singleton "classes" with JavaScript if you like.
In fact, JavaScript has a very handy feature to make it easier: You
can override the usual behavior of `new`. `new` calls a constructor
function with `this` set as a newly-created blank object. If the
constructor function doesn't return a value, `new` returns the
reference to the object it created. But if the constructor function
*does* return a value and that value is an object, `new` returns that
instead! So making `new` "return" a singleton in straight JavaScript
is dead easy: Just have your constructor function return the single
instance reference.
Unfortunately, Prototype's Class stuff doesn't support doing that. The
actual constructor function is generated by Prototype and has no
return value. Any value you return from your `initialize` function is
discarded.
So your choices if you want singleton classes are
1. Disable `new`, throwing an exception like Jim does telling the user
what to do
2. Don't use Prototype's Class stuff to define your singleton classes
and use `new`
or
3. Create multiple instance facades on top of a single shared state
(as you did)
If you'll pardon a digression: Using scoping functions (a key aspect
of the module pattern) makes all three of those easier. I always use
scoping functions to define things. For instance (no pun!), here's how
I define a boring old class with Prototype's Class.create:
var PlainOldClass = Class.create((function() {
// Our instance initializer
function initialize(name) {
this.setName(name);
}
// An instance function
function speak() {
alert("Hi, I'm " + this.name);
}
// Export our public functions
return {
initialize: initialize,
speak: speak
};
})());
Note how I've defined and called a function to create the object to
pass into Class.create, rather than creating it directly using literal
notation. This gives us a private class-wide scope (truly private
class variables), which has several advantages -- not least that our
instance methods can be *named* methods (not anonymous ones), which
means debuggers and other tools can help us more effectively. This is
pretty much the only way right now you can have named instance methods
in current browser implementations of JavaScript. The usual way (to
date):
var Foo = Class.create({
bar: function() {
// ...
}
});
or non-Prototype:
function Foo() {
// ...
}
Foo.prototype.bar = function() {
// ...
};
...just creates an anonymous function and binds it to a property.
Tools can't help us much because the function has no name; in call
stacks and such you see a lot of "(anonymous)" entries.
Sadly, you can't do what Jim did in his article, although you *should*
be able to:
var Foo = Class.create({
initialize: function initialize() {
// ...
}
});
That creates a named function called `initialize`, and then assigns it
to a property called `initialize`. You should be able to do that, it's
valid according to the spec, but bugs in both IE and Safari (and
possibly others) mean it doesn't work correctly. There's a great
article by kangax exploring this in various browsers:
http://yura.thinkweb2.com/named-function-expressions/
So why the digression into scoping functions? Because it makes all
three of your choices easier.
1. Disable `new`:
var Singleton = (function() {
// Our singleton instance
var instance;
// The class
var Singleton = Class.create((function() {
// Our singleton initializer.
function initialize() {
// Does our instance exist?
if (!instance) {
// No, this is it -- initialize it
instance = this;
prepInstance.call(this);
}
else {
// Yes, fail
throw "Invalid use of `new`, use
`Singleton.getInstance` to get the singleton";
}
}
// Our instance prep function.
// Private, not exported to the prototype.
function prepInstance() {
this.foo = 42;
}
// Set 'foo'.
// Public; this gets exported to the prototype.
function setFoo(foo) {
this.foo = foo;
}
// Get 'foo'.
// Public; this gets exported to the prototype.
function getFoo() {
return this.foo;
}
// Export our public functions
return {
initialize: initialize,
setFoo: setFoo,
getFoo: getFoo
};
})());
// Our "getInstance" function, which we put on the class
Singleton.getInstance = getInstance;
function getInstance() {
return instance;
}
// Create the single instance
new Singleton();
// Return the class object for the global symbol
return Singleton;
})();
// WRONG usage:
var s = new Singleton(); // throws useful explanatory exception
// Correct usage:
var s1 = Singleton.getInstance();
var s2 = Singleton.getInstance();
alert("s1 === s2? " + (s1 === s2)); // alerts true
That actually uses two scoping functions, one for the class stuff and
one for the singleton stuff. The `instance` reference is truly
private, not a property of the constructor function or anything where
it can be tampered with. Also note that the `prepInstance` function is
also truly private; there's no reason for it to be a public function,
of the class or the instance.
2. Don't use Prototype's Class stuff for the singleton, and use `new`
var Singleton = (function() {
// Our singleton instance
var instance;
// Our singleton constructor, which we export by returning
// from the anonymous scoping function
function Singleton() {
// Does our instance exist?
if (!instance) {
// No, create and initialize it
instance = this;
prepInstance.call(this);
}
// Return the instance; this is the magic bit JavaScript
enables
return instance;
}
// Our instance prep function.
// Private, not exported to the prototype.
function prepInstance() {
this.foo = 42;
}
// Set 'foo'.
// Public; this gets exported to the prototype.
function setFoo(foo) {
this.foo = foo;
}
// Get 'foo'.
// Public; this gets exported to the prototype.
function getFoo() {
return this.foo;
}
// Export our public functions
Object.extend(Singleton.prototype, {
setFoo: setFoo,
getFoo: getFoo
});
// Return the constructor from the anonymous builder function
return Singleton;
})();
// Usage:
var s1 = new Singleton();
var s2 = new Singleton();
alert("s1 === s2? " + (s1 === s2)); // alerts true
s1.setFoo(17);
alert(s2.getFoo()); // alerts 17, of course, it's the same instance
As a side note, I have to say that although it's really cool, I don't
like this for real-world use. "New" means "new", to me. If I'm using
the keyword "new", I expect to be getting new instances -- s1 should
not === s2.
3. Create multiple instance facades on top of a single shared state
If you're going to have instance facades over shared state, then it's
essential that you don't store any mutable properties on the instances
themselves (obviously). You solved that in your implementation by
always referring back to STUser._instance_.xyz to get to things. A
scoping function makes it a lot easier: Just use variables in the
scoping function:
var FacadeSingleton = Class.create((function() {
var fooProp; // Our 'foo' property
// Single instance initialization can be inline
fooProp = 42;
// Set 'foo'.
// Public; this gets exported to the prototype.
function setFoo(foo) {
fooProp = foo;
}
// Get 'foo'.
// Public; this gets exported to the prototype.
function getFoo() {
return fooProp;
}
// Export our public functions
return {
setFoo: setFoo,
getFoo: getFoo
};
})());
// Usage:
var s1 = new FacadeSingleton();
var s2 = new FacadeSingleton();
alert("s1 === s2? " + (s1 === s2)); // alerts false
s1.setFoo(17);
alert(s2.getFoo()); // alerts 17, not 42
`fooProp` exists in our scoping function. There's only one, so the
"instance" methods that interact with it are sharing the same
underlying data -- singleton behavior behind multiple instance
facades, or to use classed-based terminology, the instance methods
interact with the class member (static) variables.
Note that we didn't need an `initialize` function, since we can just
initialize our props right there in the scoping function.
If naming conflicts worry you (note I had to use `fooProp` rather than
`foo` to avoid conflicting with the argument name in `getFoo`), you
can put all the properties on a data object instead:
var FacadeSingleton = Class.create((function() {
var data; // Our shared property data
// Single instance initialization can be inline
data = {
foo: 42
};
// Set 'foo'.
// Public; this gets exported to the prototype.
function setFoo(foo) {
data.foo = foo;
}
// Get 'foo'.
// Public; this gets exported to the prototype.
function getFoo() {
return data.foo;
}
// Export our public functions
return {
setFoo: setFoo,
getFoo: getFoo
};
})());
For me, I'm happy just having objects with behaviors and don't feel
the need for singleton "classes". But if you want them, or want
instance facades over shared state, hopefully the above is useful.
HTH,
--
T.J. Crowder
Independent Software Consultant
tj / crowder software / com
www.crowdersoftware.com
On Feb 5, 1:38 am, Daff <[email protected]> wrote:
Hi All,
Have spent the last hour looking for a singleton class in prototype,
with not a large amount of luck.
Found a reasonable attempt by Jim Higgson at
http://jimhigson.blogspot.com/2009/01/prototype-singleton-classes.html
but didn't like the fact that if you called new again it threw an
exception. To my mind, calling new should simply return the instance. I
realise that this goes against the normal singleton design pattern which
should be used with getInstance() rather than new, but as we can't make
'new' private as we would in other languages I felt that 'new' was the
logical function to make sure it got the singleton instance (i.e. it is
more likely to be used by others to create an object in Javascript and
with no compile to warn you until runtime, it should 'just work').
To make it work like that I have used the following code. Comments on
anything that you don't like, or that might cause an issue would be
appreciated.
var STUser = Class.create(
{
initialize: function() {
if (!STUser._instance) {
this.getInstance(); // Actual instance contructor, sets up
variables etc.
}
return this;
},
getInstance: function() {
this._timePeriod = 120;
},
getTimePeriod: function() {
return STUser._instance._timePeriod;
},
setTimePeriod: function(timePeriod) {
STUser._instance._timePeriod = timePeriod;
}
});
STUser._instance = new STUser();
Tested with the following code...
var user = new STUser();
alert("User 1 Time = " + user.getTimePeriod());
user.setTimePeriod(100);
alert("User 1 Time = " + user.getTimePeriod());
var user2 = new STUser();
alert("User 2 Time = " + user2.getTimePeriod());
Which worked as advertised,
Appreciate any feedback, and wonder if I missed something out of
prototype as was surprised there wasn't (or I couldn't find) a singleton
pattern.
--
daff
SOFTWARE ENGINEER
andrew 'daff' niles | spider tracks ltd | 117a the square
po box 5203 | palmerston north 4441 | new zealand
P: +64 6 353 3395 | M: +64 21 515 548
E: [email protected] <mailto:[email protected]>
W:www.spidertracks.com<http://www.spidertracks.com>