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 <d...@spidertracks.co.nz> 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: d...@spidertracks.co.nz <mailto:d...@spidertracks.co.nz>
>  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 prototype-scriptacul...@googlegroups.com.
To unsubscribe from this group, send email to 
prototype-scriptaculous+unsubscr...@googlegroups.com.
For more options, visit this group at 
http://groups.google.com/group/prototype-scriptaculous?hl=en.

Reply via email to