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> -- You received this message because you are subscribed to the Google Groups "Prototype & script.aculo.us" group. To post to this group, send email to [email protected]. To unsubscribe from this group, send email to [email protected]. For more options, visit this group at http://groups.google.com/group/prototype-scriptaculous?hl=en.
