Today we will discuss the Singleton pattern, the most controversial creational pattern.
A Singleton is a global point of access for a resource that must have only one instance in the whole application: database connections, http streams, and similar objects are the typical examples of such a class.
This behavior is implemented trough a static method which returns a lazy-created instance, and by rendering the constructor private to avoid creation of an instance from outside the class code.
The problems of a Singleton class are the same of its non object-oriented equivalent: a global variable. The Api of Clients of the Singleton lies, because they do not declare in any signature that they are taking advantage of the Singleton's capabilities; moreover, global mutable state is introduced and encapsulation is disregarded.
There is a famous credit card example, which describes the usage of a CreditCard class. One day Misko Hevery was writing some tests in a system he does not know very well, to improve his knowledge of the codebase.
He wrote this code:
testCreditCardCharge() { CreditCard c = new CreditCard( "1234 5678 9012 3456", 5, 2008); c.charge(100); }The test ran, and at the end of the month he got the bill and see he was out $100! The story is probably made up, but you get the point: if a Client references Singletons, you have no idea of what can happen when calling a method.
The Api of CreditCard would have been honest if it was like:
testCreditCardCharge() { CreditCard c = new CreditCard( "1234 5678 9012 3456", 5, 2008); c.charge(100, creditCardProcessor); }Have you ever seen a CreditCard that can be used without a POS?
In my opinion the classic Singleton pattern is adequate only if the static instance has no state, or it is an utility without side effects which you may want to substitute. The sample code reflects this weltanschauung.
There are people that subclass Singletons, but if you feel the need to write a subclass it probably means you are already gone too far. Refactor the code to inject the Singleton as a collaborator of Client.
PARTICIPANTS:
- Singleton: class that manages to keep one and only one instance of itself.
- Client: every class that references a Singleton.
<?php /** * The most harmless example of a Singleton I can think of is a Logger. * However, I feel that injecting the Logger or managing events with an * Observer pattern would be better here. * This Singleton infringes every rule of testability: * - constructor does real work * - destructor does other work * - constructor is private, you cannot instantiate it in tests * - instance is static, you cannot throw it away */ class LoggerSingleton { private static $_instance; private $_fp; /** * We need only one instance, which will lock the file * from further editing. */ private function __construct() { $this->_fp = fopen('log.txt', 'a'); } public function __destruct() { fclose($this->_fp); } public function log($text) { $line = $text . "\n"; fwrite($this->_fp, $line); } /** * An instance of this class is lazy created and returned. * No further instances are created if there is already one available. * This method is the standard way to create a Singleton. */ public static function getInstance() { if (self::$_instance === null) { self::$_instance = new self(); } return self::$_instance; } } /** * The Client receives a name and returns a welcome message, while * logging the operation. * *Little* side effect: it writes a file. */ class Client { public function createWelcomeMessage($name) { LoggerSingleton::getInstance()->log("Greeted $name"); return "Hello $name, have a nice day.\n"; } } // isn't it strange that these two lines write a file on disk? $client = new Client(); echo $client->createWelcomeMessage('Giorgio');The one instance myth is overrated, and there are other means to achieve the same behavior from a class. Test suites could have dozen of instances of "unique" services and connections (some of them as Fakes and Stubs); in fact, a proof of testability and good design consists in being able to run two applications in the same php script, Jvm or main method.
The difference is that maintaining a unique instance is correct, but only within the boundaries of an application. If you control the creation process, your factories will never create more than one instance of a critical resource. Using a Singleton extends this uniqueness along all the execution environment and hides a crucial dependency under the carpet, effectively causing hassle to everyone trying to figure out what is going on, in debugging as in testing.
5 comments:
I'm a little confused by the talk of running two applications in the same PHP script. So for each app, you have an application object? What does this application object do? Perhaps this is in the context of the Zend Framework (which I'm not familiar with, so sometimes I get lost in your posts on it). But I'm trying to understand what use an application object might be (I suppose I can sort of see how it might be useful in a single-entry, front-controller based app. Thanks!
An Application object composes directly or indirectly all the objects in the application. Zend_Application is an example of this "pattern" though there are singletons in this framework which contaminate the usefulness of this object.
Ideally in each integration test you can instantiate a new application object, thus being able to effectively do more than one http request. If different application objects do not interfer with each other, it's a proof of good and decoupled design (no global, shared state).
Maybe that's what I'm already doing, I just don't have an App object layer on top of it all. I'll have to look at the ZF for a concrete example. Thanks
Non-factory-style singleton:
class singleton{
private static $properties = array();
public function __construct(){}
public function __get( $name ){
if( array_key_exists( $name, self::$properties ) ){
return self::$properties[$name];
}
}
public funtion __set( $name, $value ){
self::$properties[$name] = $value;
}
}
Some notes on your code:
- be careful with objects identity, because different objects are actually created, while they has the same static state;
- of course global mutable state should be implemented with caution.
Post a Comment