Page 1 of 2

Singletons vs Registry vs ServiceLocator

Posted: Wed May 24, 2006 8:51 am
by Maugrim_The_Reaper
Recently I've been playing critic on a small framework I've been pulling together for use in my projects (typically open source games with very simplified MVC needs - code is all GPL). One of the things I did originally since no one's first framework goes right on the first iteration, is create havoc using Singletons. They're everywhere. It's kind of smelly...it stinks in fact ;).

Anyway, I started tinkering with the concept of using a ServiceLocator class to manage and cache all those Singletons, push them into a single access point, and allow the ServiceLocator to be responsible for locating the class file, instantiating the object, caching it, and returning the new/cached copy. Whew! Anyways, the class so far...

Code: Select all

class Partholan_ServiceLocator /* implements Partholan_iServiceLocator */ {

	private static $instance = false;

        private $instances = array();

        private $factories = array();

        private $searchLocations = array();

        public function __construct() { // allow direct instances

        }

	static function getInstance() {
		if(!self::$instance)
		{
			self::$instance = new Partholan_ServiceLocator();
		}
		return self::$instance;
	}
     
        public function registerService($serviceName, $serviceInstance) {
		if(is_object($serviceInstance) && strlen($serviceName))
		{
			$this->instances[$serviceName] = $serviceInstance;
			return true;
		}
		trigger_error('Service could not be registered. Requires a valid Service name and object.');
	}

	public function removeService($serviceName) {
		unset($this->instances[$serviceName]);
	}

	public function getService($serviceName) {
                if(isset($this->instances[$serviceName]))
                {
                        return $this->instances[$serviceName];
                }
                elseif(is_class($serviceName))
                {
                        $this->instances[$serviceName] = new $serviceName();
                        return $this->instances[$serviceName];
                }
                else
                {
                        foreach($this->searchLocations as $location)
                        {
                                if(file_exists($location . DIRECTORY_SEPARATOR . $serviceName . '.php'))
                                {
                                        require_once($location . DIRECTORY_SEPARATOR . $serviceName . '.php');
                                        $this->instances[$serviceName] = new $serviceName();
                                        return $this->instances[$serviceName];
                                }
                        }
                }
		// factory lookups to be added later
                trigger_error('Specified Service could not be located.', E_USER_ERROR);
        }

	public function registerFactory($factoryName, $serviceFactory) {
		if(is_object($serviceFactory) && strlen($factoryName))
		{
			$this->factories[$factoryName] = $serviceFactory;
			return true;
		}
		trigger_error('Factory could not be registered. Requires a valid Factory name and object.');
	}

	public function addSearchLocations() {
		foreach(func_get_args() as $dirPath)
		{
			$this->searchLocations[] = $dirPath;
		}
	}

	public function hasService($serviceName) {
		if($this->instances[$serviceName]) 
		{
			return true;
		}
		return false;
	}

	public function hasFactory($factoryName) {
		if($this->factories[$factoryName]) 
		{
			return true;
		}
		return false;
	}

	public function hasSearchLocation($dirPath) {
		if(in_array($dirPath, $this->searchLocations))
		{
			return true;
		}
		return false;
	}

}
Dumb object for testing purposes...

Code: Select all

class EmptyObject {

	public function __construct() {}

}
Unit test...

Code: Select all

class Partholan_ServiceLocator_TestCase extends UnitTestCase {

	public function __construct() {
		$this->UnitTestCase();
	}

	public function setUp() { }

	public function tearDown() { }

	public function testRegisteringService() {
		$sl = new Partholan_ServiceLocator();
		$sl->registerService('EmptyObject', new EmptyObject);
		$this->assertTrue($sl->hasService('EmptyObject'));
		$this->assertIsA($sl->getService('EmptyObject'), 'EmptyObject');
	}

	public function testRemovingService() {
		$sl = new Partholan_ServiceLocator();
		$sl->registerService('EmptyObject', new EmptyObject);
		$sl->removeService('EmptyObject');
		$this->assertFalse($sl->hasService('EmptyObject'));
	}

	public function testAddingSearchLocations {
		$sl = new Partholan_ServiceLocator();
		$sl->addSearchLocations('/tmp', '/home');
		$this->assertTrue($sl->hasSearchLocation('/tmp'));
		$this->assertTrue($sl->hasSearchLocation('/home'));
	}

	// test: no class, no file included - get file and instantiate object
	public function testGettingServiceInstanceFromFile() {
		$sl = new Partholan_ServiceLocator();
		$sl->setSearchLocations(dirname(__FILE__)); // or location of EmptyObject.php if other
		$mk = $sl->getService('EmptyObject');
		$this->assertIsA($mk, 'EmptyObject');
		$this->assertTrue($sl->hasService('EmptyObject'));
	}

	// test: is_class method where file pre-included()
	public function testGettingServiceInstance() {
		$sl = new Partholan_ServiceLocator();
		$mk = $sl->getService('EmptyObject');
		$this->assertIsA($mk, 'EmptyObject');
		$this->assertTrue($sl->hasService('EmptyObject'));
	}

	public function testRegisteringFactory() {
		$sl = new Partholan_ServiceLocator();
		$sl->registerFactory('EmptyObject', new EmptyObject);
		$this->assertTrue($sl->hasFactory('EmptyObject'));
		$this->assertIsA($sl->getFactory('EmptyObject'), 'EmptyObject');
	}

}
Forgive the unit tests - not my strong suit though I'm working on it. I was wondering what other approaches people have taken. How do you prefer to limit dependencies like Singletons/constructor instantiation etc. which make reusing classes difficult? Does anyone have comments on the above and whether it can be improved? Other similar approaches?

Posted: Wed May 24, 2006 10:33 am
by santosj
I like the plugin method, which is sort of like your idea, except the methods become part of the class and can be called from the class. I don't cache the plugin classes, or do you mean cache inside of the class instead of file caching? I do cache the objects inside the class, but they would most likely be destroyed after each request. The funny thing however is that I have yet to even use this method.

I did do some code with SPL Observer, but that is about it.

Posted: Wed May 24, 2006 10:37 am
by Christopher
Looks good. You might want to have a setting for use with autoload where instead of searching it just creates an object and assumes that autoload will include it. Also you have not finished the Factories part. What were your plans for that?

Posted: Wed May 24, 2006 11:46 am
by Maugrim_The_Reaper
I'll check into using autoload functionality - I spend too much time in PHP4 mode as it is I forget about such things... :roll:
santosj wrote:I like the plugin method, which is sort of like your idea, except the methods become part of the class and can be called from the class. I don't cache the plugin classes, or do you mean cache inside of the class instead of file caching? I do cache the objects inside the class, but they would most likely be destroyed after each request. The funny thing however is that I have yet to even use this method.
By caching I meant within the object (the $instances/$factories properties). There's no file caching planned as yet - outside the requirement threshold for now. Maybe I should stop calling it caching since it's usually seen as something stored across requests...
aborint wrote:Also you have not finished the Factories part. What were your plans for that?
Haven't gotten that far yet. The rough idea is for the class to locate an object in a few ways, finally resorting to checking for a registered Factory if available. Factories would be required for several areas - the simplest being the Database connectivity and Template engine (both of which can be instances of one of a selection of alternate libraries). So there would be a TemplateEngineFactory creating a Smarty/Savant/Other instance. Not quite that simple - but that's about it.

More than likely these steps will end up being extracted into separate private methods - as the unit tests structure suggests. Once I have this finished I'll have an entertaining time replacing all those mangled singletons in the framework...

Re: Singletons vs Registry vs ServiceLocator

Posted: Wed May 24, 2006 12:26 pm
by santosj
Maugrim_The_Reaper wrote:

Code: Select all

class Partholan_ServiceLocator /* implements Partholan_iServiceLocator */ {

	private static $instance = false;

        private $instances = array();

        private $factories = array();
If you are going to have multiple types then why mess with the fuss. Since you are working with PHP 5, use constants.

Code: Select all

const SERVICE = 'service';
const FACTORY = 'factory';
const UNDEFINED = 'none';
const AUTO = 'auto';

private $store = array();
use $store to replace $factories and $instances. Also if you want to add another type, it would be easy to do so without adding extra functions.

Code: Select all

public function registerService($serviceName, $serviceInstance) {
		if(is_object($serviceInstance) && strlen($serviceName))
		{
			$this->instances[$serviceName] = $serviceInstance;
			return true;
		}
		trigger_error('Service could not be registered. Requires a valid Service name and object.');
	}

	public function removeService($serviceName) {
		unset($this->instances[$serviceName]);
	}

	public function getService($serviceName) {
                if(isset($this->instances[$serviceName]))
                {
                        return $this->instances[$serviceName];
                }
                elseif(is_class($serviceName))
                {
                        $this->instances[$serviceName] = new $serviceName();
                        return $this->instances[$serviceName];
                }
                else
                {
                        foreach($this->searchLocations as $location)
                        {
                                if(file_exists($location . DIRECTORY_SEPARATOR . $serviceName . '.php'))
                                {
                                        require_once($location . DIRECTORY_SEPARATOR . $serviceName . '.php');
                                        $this->instances[$serviceName] = new $serviceName();
                                        return $this->instances[$serviceName];
                                }
                        }
                }
		// factory lookups to be added later
                trigger_error('Specified Service could not be located.', E_USER_ERROR);
        }

	public function registerFactory($factoryName, $serviceFactory) {
		if(is_object($serviceFactory) && strlen($factoryName))
		{
			$this->factories[$factoryName] = $serviceFactory;
			return true;
		}
		trigger_error('Factory could not be registered. Requires a valid Factory name and object.');
	}

	public function hasService($serviceName) {
		if($this->instances[$serviceName]) 
		{
			return true;
		}
		return false;
	}

	public function hasFactory($factoryName) {
		if($this->factories[$factoryName]) 
		{
			return true;
		}
		return false;
	}

	public function addSearchLocations() {
		foreach(func_get_args() as $dirPath)
		{
			$this->searchLocations[] = $dirPath;
		}
	}

	public function hasSearchLocation($dirPath) {
		if(in_array($dirPath, $this->searchLocations))
		{
			return true;
		}
		return false;
	}

}
I don't do unit tests, I do tests and if the code works, then great, if not, then I try again.

I would add the following functions
  • register($class, $type = Partholan_ServiceLocator::UNDEFINED);
  • retrieve($className, $type = Partholan_ServiceLocator::AUTO);
  • exists($className);
  • remove($className, $type = Partholan_ServiceLocator::AUTO);
replacing:
  • Register: registerService, registerFactory
  • Retrive: getService, getFactory
  • exists: hasFactory, hasService
  • remove: removeFactory, removeService
$this->store

Code: Select all

$this->store[] = array('type' => Partholan_ServiceLocator::UNDEFINED, 'name' => $name, 'class' => $instance);
Class Name

I can understand why you would allow the user to manually name the instance since it would help with unique values and better control. However you could have it as a second optional parameter and just autodetect the name.

Code: Select all

if($name === null) $name = get_class($class);

Re: Singletons vs Registry vs ServiceLocator

Posted: Wed May 24, 2006 1:09 pm
by Christopher
santosj wrote:I don't do unit tests, I do tests and if the code works, then great, if not, then I try again.
:(
santosj wrote:I can understand why you would allow the user to manually name the instance since it would help with unique values and better control. However you could have it as a second optional parameter and just autodetect the name.

Code: Select all

if($name === null) $name = get_class($class);
Beause it is being used to Lazy Load and no class exists yet.

Re: Singletons vs Registry vs ServiceLocator

Posted: Wed May 24, 2006 2:31 pm
by santosj
arborint wrote:
santosj wrote:I don't do unit tests, I do tests and if the code works, then great, if not, then I try again.
:(
It isn't like it has bit me in the ass yet. The tests I do are to only see if a error is returned, but I only call it debugging. I dont' have the brain power to try to think of every single case of which I'm going to use the class.
arborint wrote:Beause it is being used to Lazy Load and no class exists yet.
Reflection would be better.

Code: Select all

public function retrieve($class, $type = Partholan_ServiceLocator::AUTO)
{
	if(is_string($class))
	{
		$key = array_search($class, $this->store);
		if($key !== false)
		{
			$array = $this->store[$key];
			if(($type == Partholan_ServiceLocator::AUTO) or ($array['type'] == $type))
			{
				return $array['class'];
			}
			else
			{
				trigger_error("Couldn't return Class because the type is incorrect.");
			}
		}
		else
		{
			foreach($this->searchLocations as $location)
			{
				if(file_exists($location . DIRECTORY_SEPARATOR . $class. '.php'))
				{
					require_once($location . DIRECTORY_SEPARATOR . $class. '.php');
					// Most likely will throw an exception if class doesn't exist.
					$reflectClass = new ReflectionClass($class); 
					$newClass = $reflectClass->newInstance(); // Return class if it exists.
					$this->store[][$class] = $newClass;
					return $newClass;
				}
			}
		}
	}
	else if(is_object($class))
	{
		$name = get_class($class);
		$reflection = new ReflectionClass($name);
		$class = $reflection->newInstance();
		$this->store[][$name] = $class;
		return $class;
	}
}
Or maybe not.

Posted: Wed May 24, 2006 2:46 pm
by Maugrim_The_Reaper
santosj wrote:I don't do unit tests, I do tests and if the code works, then great, if not, then I try again.
Believe me, I would have agreed totally not so long ago. Since then I've discovered Unit Tests let you get an implementation up that works (the current stage in this code), and then make it simple to refactor where the need arises (test, fix bug, test again, refactor, test, ...). Its horribly counterintuitive to the point you need to just dive in and stick with it. Even if you feel like its an uphill battle it eventually pays dividends.
santosj wrote:
Maugrim_The_Reaper wrote:

Code: Select all

class Partholan_ServiceLocator /* implements Partholan_iServiceLocator */ {

        private static $instance = false;

        private $instances = array();

        private $factories = array();

If you are going to have multiple types then why mess with the fuss. Since you are working with PHP 5, use constants.

Code: Select all

const SERVICE = 'service';
const FACTORY = 'factory';
const UNDEFINED = 'none';
const AUTO = 'auto';

private $store = array();

use $store to replace $factories and $instances. Also if you want to add another type, it would be easy to do so without adding extra functions.
Nice refactor...:) Still need specified names however, as aborint points out the stored objects are only loaded when/if requested (Lazy Load) unless explicitly registered of course. Need some way to reference the required object so that the ServiceLocator can attempt to locate the class to instantiate. Names in this specific case have a direct relation to class name.

Off to amend my unit tests...

Posted: Wed May 24, 2006 3:40 pm
by Christopher
I have yet to figure out santosj's obsession with reflection. I don't see the differece between these two below except that using reflection is considerably slower and less clear:

Code: Select all

$reflectClass = new ReflectionClass($class);
	$newClass = $reflectClass->newInstance();
and

Code: Select all

if (class_exists($class)) {
		$newClass = new $class();
	} else {
		$newClass = null;
	}
}

Posted: Wed May 24, 2006 3:48 pm
by santosj
Maugrim_The_Reaper wrote:Believe me, I would have agreed totally not so long ago. Since then I've discovered Unit Tests let you get an implementation up that works (the current stage in this code), and then make it simple to refactor where the need arises (test, fix bug, test again, refactor, test, ...). Its horribly counterintuitive to the point you need to just dive in and stick with it. Even if you feel like its an uphill battle it eventually pays dividends.
Ah, there is more than one way to skin a human.

If the ends are the same then what is the point of what you use to achieve that end.
Nice refactor...:) Still need specified names however, as aborint points out the stored objects are only loaded when/if requested (Lazy Load) unless explicitly registered of course. Need some way to reference the required object so that the ServiceLocator can attempt to locate the class to instantiate. Names in this specific case have a direct relation to class name.
I see, so the name isn't just the class name but also the location to the file. That is interesting. What don't you create a data object to store the object along with that information? Of course, I'm just tossing out suggestions, to implement it would be a waste of time, since you could spend the time updating the rest of the classes.

Well, if the file doesn't exist, then you could of course run a search of the known paths and check each file. Once you find it, I would cache the location. It would painfully slow depending on how many files there are.

Posted: Wed May 24, 2006 4:43 pm
by Maugrim_The_Reaper
Well, if the file doesn't exist, then you could of course run a search of the known paths and check each file. Once you find it, I would cache the location. It would painfully slow depending on how many files there are.
True, but it's one of those optimisations I prefer deferring until the class, as a functional unit, is more or less complete.
Ah, there is more than one way to skin a human.
;). Point taken. I'm becoming a unit testing enthusiast probably but I like where it leads. Up to now I've been using more specific functional tests.
I see, so the name isn't just the class name but also the location to the file. That is interesting. What don't you create a data object to store the object along with that information? Of course, I'm just tossing out suggestions, to implement it would be a waste of time, since you could spend the time updating the rest of the classes.
Hardly seems worth the cost of adding it... There's just a once off cost to amending the dependent classes which would need to happen anyway for several reasons. Notably a ServiceLocator is useful as a parameter object where classes were being passed as individual parameters to a constructor/init() method previously.

Trying to avoid Reflection unless it's clearly the best option. Some of the changes need to be degradeable to PHP4 if necessary (an annoying limitation given the state of shared hosting today...). Not all, but enough to allow some push backs to the older PHP4 version of what I came up with a long while back.

Thanks for the feedback thus far guys!

Posted: Wed May 24, 2006 4:55 pm
by Christopher
santosj wrote:If the ends are the same then what is the point of what you use to achieve that end.
In software development the process is everything -- the "end" is never done and we rarely actually use the "end" either. There is just another "end" after that or changes to the previous "end." It's the means that our craft is all about.

Posted: Wed May 24, 2006 5:47 pm
by santosj
arborint wrote:In software development the process is everything -- the "end" is never done and we rarely actually use the "end" either. There is just another "end" after that or changes to the previous "end." It's the means that our craft is all about.
I'm sorry, a better word would be 'solution'.

For example I would use while or foreach (or even for), each has its own advantage over the other. While I like to use Foreach, another person may not. The 'end' will be the same thing, just use a different method.
Trying to avoid Reflection unless it's clearly the best option. Some of the changes need to be degradeable to PHP4 if necessary (an annoying limitation given the state of shared hosting today...). Not all, but enough to allow some push backs to the older PHP4 version of what I came up with a long while back.
As do I up to a point. I have stopped writing for PHP 4, so I don't really care, but I just want the name of a class I will use get_class() instead. I haven't done any benchmarking of the Reflection classes over the function method, but I do like less code over a lot. I find that using call_user_func() can be difficult when you use classes, that is just me. I did try the call_user_func_array() method before I tried the Reflection method. I couldn't get the call_user_func_array() to work after 30 minutes and I tried the Reflection class next and I was able to get to work with only two debug tests. Working solution in under 10 minutes versus no working code in 30 minutes, which would you love and cherish? It is true that if I ever plan to backport any of my code I will eventually have to learn how to get call_user_func to work properly. I'll put that off for as long as possible.

Posted: Wed May 24, 2006 5:57 pm
by Christopher
santosj wrote:I'm sorry, a better word would be 'solution'.

For example I would use while or foreach (or even for), each has its own advantage over the other. While I like to use Foreach, another person may not. The 'end' will be the same thing, just use a different method.
Even 'solution', I see a major difference between construct/algorithm choice (what you do) and methodology choice (how you do it).

Posted: Wed May 24, 2006 6:10 pm
by santosj
arborint wrote:Even 'solution', I see a major difference between construct/algorithm choice (what you do) and methodology choice (how you do it).
I should probably do more research on this subject because it seems to stump me on most class design patterns and grasping some more advanced abstract programming concepts, but could you explain this more.

What I do is get the class name and create an instance of it.

How I do it is use get_class() and Reflection, as opposed to using call_user_func() to call methods (ReflectionMethod) and 'new $classname()' (which I didn't think would work, so I learned something new).

I believe the algorithm is how you do something and the methodology is what you do, but I'm a tool. Still from my basic grasps of programming, that is how I would represent it.

Well, there is only minor differences between the two class method algorithms.