Jump to content

Critique this singleton design


roopurt18

Recommended Posts

I set out to develop a method of implementing singletons in PHP with a simple initial critera:  To not use any non-class helper functions and have as much of the behavior inherited as possible.  Here's what I came up with:

 

class singleton

<?php
  abstract class singleton {
    // associative array; "className" => "instance"
    private static $s_singletons = Array();
    
    /**
     * Retrieve the instance of the class if it exists, otherwise instantiate
     * and return the new instance.  Return null on failure.
     * @var string Class name
     * @var string Path to class definition file
     */
    public static function getInstance($class = __CLASS__, $path = null){
      $instance = null;                // No instance initially
      if($class == __CLASS__){         // No override check
        echo "derived class didn't override singleton::getInstance<br>";
        return $instance;
      }
      if(isset(singleton::$s_singletons[$class])){
        echo "already instantiated<br>";
        $instance = singleton::$s_singletons[$class];
      }else{
        echo "not instantiated<br>";
        // Here we would do a require_once using the $path, $class, etc.
        $instance = new $class();
        if(is_object($instance)){
          echo "created new instance<br>";
          singleton::$s_singletons[$class] = $instance; // Save instance
        }else{
          echo "couldn't create new instance<br>";
        }
      }
      return $instance;                // null or a class instance
    }
    
    public static function dumpInfo(){
      if(!count(singleton::$s_singletons)){
        echo "No singletons created.<br>";
        return;
      }
      echo "<pre>" . print_r(singleton::$s_singletons, true) . "</pre>";
    }
  }
?>

 

I then created three derived classes.  In the final derived class, c, I didn't override the getInstance() method to make sure my override check was working correctly.

<?php
  class a extends singleton {
    public static function getInstance(){
      parent::getInstance(__CLASS__, dirname(__FILE__));
    }
  }
  
  class b extends singleton {
    public static function getInstance(){
      parent::getInstance(__CLASS__, dirname(__FILE__));
    }
  }

  class c extends singleton {
  }
?>

 

I then ran the following:

<?php
  a::getInstance();
  b::getInstance();
  c::getInstance();
  singleton::dumpinfo();
  a::getInstance();
  b::getInstance();
  singleton::dumpinfo();
  
  $a = new a();
  echo "<pre>" . print_r($a, true) . "</pre>";
?>

 

Here is the output:

not instantiated
created new instance
not instantiated
created new instance
derived class didn't override singleton::getInstance

Array
(
    [a] => a Object
        (
        )

    [b] => b Object
        (
        )

)

already instantiated
already instantiated

Array
(
    [a] => a Object
        (
        )

    [b] => b Object
        (
        )

)

a Object
(
)

 

99% of the time in an application I'm only ever using a single instance of a Database object.  So the code will end up doing something like $db->select($sql);  However, sometimes it would be nice to access database functions using the currently instantiated Database object but without having a reference to it.  Here is one way of implementing that:

<?php
  class database extends singleton {
    /**
     * Run a SELECT query
     */
    public function select($sql){ }

    /**
     * Run a SELECT query statically
     */
    public static function st_select($sql){
      $obj = __CLASS__::getInstance();
      return is_object($obj) ? $obj->select($sql) : null;
    }
  }
?>

 

This enables me elsewhere in the code to do the following:

  // If I have a reference to the $db object, I can do:
  $db->select($sql);

  // But if I don't have a reference to the $db object, I can still run the following:
  // (It's slightly more typing, but it may allow me to be lazy in other regards, such
  //  as finding a nifty way to pass an object somewhere it was never intended to
  // go.)
  database::st_select($sql);

 

Overall, I like this approach because it uses inheritance and locates the actual getInstance() method in a single location, should I need to modify it later.  Also there are no non-class functions which also makes me happy since I try to avoid polluting the global name space.

 

One reservation that I have about it though is that I can create an additional instance of any of the derived classes.  I'm sure I can disable that ability somehow, but I'm not sure I want to.  This would seem to defeat the purpose of creating singletons in the first place, but here is a possible scenario.

 

Let's say for arguments sake that 99% of my application is using a single Logging object.  This object is writing to a specific file in a specific directory; this means the majority of my program would be using calls like $log->msg($msg); or logger::msg($msg);  However, now let's say I need to debug a portion of the program and logging is the best way to do so, but I don't want to add extra information into the current logging process.  I could still create an extra instance of the logger object, set it to log to a different file, and use it temporarily for debugging.

 

Curious what you folks think about all this.

 

Link to comment
Share on other sites

I'll just leave a quick response now, and I'll come back later most likely.  Taking a look at what you've done I wonder why you don't implement a factory class instead of spreading your factories across each class?  You could create a factory class which generates singletons or new instances, and through that you would not have to write code in each class setting up the factory method.  Of course the one drawback is that your IDE wouldn't be able to help you with autocompletes based on the return value of the factory (assuming that the factory returns disparate types, though you could make factories for each hierarchy).

 

As an example:

/**
 * Returns a single instance of a given class
 * 
 * @throws ReflectionException // custom exception for the Reflection Facade
 * 
 * @param String $classname class name following the PEAR convention
 * @param array $arguments arguments for the constructor
 * @return unknown
 */
public function getSingleton($classname, array $arguments) {
	static $existing_classes;
	if (!isset($existing_classes[$classname])) {
		using($existing_classes); // function which includes files based on PEAR convention class names
		$class = ReflectionFacade::getInstance($classname, $arguments); // facade around the Reflection API
		$existing_classes[$classname] = $class;
	}
	return $existing_classes[$classname];
}

 

I think it's pretty clear how you could fill in the blanks I've left, and how to adapt things to fit your own conventions.

 

With something like the above you could generate single instances of classes whenever you wanted without ever writing new code to do so.  Also I think it's easy to see how you could use the reflection API (or a wrapper such as my facade) to call methods dynamically and as if they were static.  For instance you could have this second method rely on the first to get its hands on an instance of the object in question.  Thus the call to the reflection wrapper would be static even though it would be executing an ordinary method call.

 

Those are my thoughts, I'll come back later and take a closer look.

Link to comment
Share on other sites

A singleton is a single instance of a given object.  The goal of the pattern is to restrict the number of instances of a given object to one.  For example, if you have an expensive DB object which is costly to instantiate you might just pass around a single instance of it via a factory or method instead of just recreating it every time it is needed.

Link to comment
Share on other sites

For us novices following along, would you mind explaining the __CLASS__ bit?  I sort of see how it works, but the default value in the getInstance() function signature is throwing me off.  Also, am I right in guessing you merely left out the bit that deals with the class' path?  Or am I missing something obvious?

 

Thanks! :)

Link to comment
Share on other sites

I'll answer your questions in reverse.

 

1)  I did leave off the bit about the class path, in it's place is a comment:

// Here we would do a require_once using the $path, $class, etc.

Although honestly I don't think that part would be necessary.  If the function is accessed through the class statically then you would have to assume the file had already been included.

 

2)  __CLASS__ is the same as __LINE__ or __FILE__.  They are replaced with the actual value at run-time.  In this case, __CLASS__ evaluates to the name of the class it is used within.  The default value of the singleton::getInstance will evaluate to "singleton".  This gives the singleton a way of knowing if the function is being called through the singleton class itself or a derived class; an instance should be created only if being called through a derived class.  Therefore, there is a check at the top of getInstance to see if $class is equal to the default value ("singleton"), if it is, the function returns early.

 

I still don't know how I feel about this design.  It has the disadvantages of requiring extra code in each derived class, most of which is duplicated but can't be derived.  It also has the disadvantage of the class file needing to be included before being used.

 

The alternative is a factory method, such as that suggested by Buyocat.  It has the obvious advantage of not requiring extra code in each class, additionally class files don't need to be included by the calling code as the factory method can take care of that step.

 

Decisions, decisions!

Link to comment
Share on other sites

I'll answer your questions in reverse.

 

1)  I did leave off the bit about the class path, in it's place is a comment:

// Here we would do a require_once using the $path, $class, etc.

Although honestly I don't think that part would be necessary.  If the function is accessed through the class statically then you would have to assume the file had already been included.

 

2)  __CLASS__ is the same as __LINE__ or __FILE__.  They are replaced with the actual value at run-time.  In this case, __CLASS__ evaluates to the name of the class it is used within.  The default value of the singleton::getInstance will evaluate to "singleton".  This gives the singleton a way of knowing if the function is being called through the singleton class itself or a derived class; an instance should be created only if being called through a derived class.  Therefore, there is a check at the top of getInstance to see if $class is equal to the default value ("singleton"), if it is, the function returns early.

 

I still don't know how I feel about this design.  It has the disadvantages of requiring extra code in each derived class, most of which is duplicated but can't be derived.  It also has the disadvantage of the class file needing to be included before being used.

 

The alternative is a factory method, such as that suggested by Buyocat.  It has the obvious advantage of not requiring extra code in each class, additionally class files don't need to be included by the calling code as the factory method can take care of that step.

 

Decisions, decisions!

 

Ah, okay, cool.  Makes total sense.  And yeah, I didn't see that comment about __FILE__ until you just mentioned it.

 

Thanks! :)

Link to comment
Share on other sites

Are you repeating the question?  Or did you forget to type a body in your post?

 

If you're repeating the question:  A singleton is an object which is intended to be instantiated only once during a programs execution.  When the program requests a singleton object, let's say the DB, it first checks if the object is instantiated.  If it is, it returns the existing instance.  If it is not, it instantiates the object (via new) and returns the instance; it also has to save a reference to that instance  in case the program requests the singleton again later.

Link to comment
Share on other sites

LOL, no I'm not repeating the question, I know what a singleton is.  :D  I did edit the post because I found out one could access PHP Application Design part 3 (Design Patterns), which is far from finished, so I replaced it with a link to a thread in the Recommended lounge which Ronald should be able to access, but non mods/admins/recommended shouldn't.

Link to comment
Share on other sites

This thread is more than a year old. Please don't revive it unless you have something important to add.

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.