One of the key point in the architectural discussion is singletons usage. Singletons are scheduled for termination and in my opinion they just have to go (unless they represent a global state which cannot be reset for real, such as autoloading).
It is actually very simple to eliminate singletons: just force the components to ask for what they need in the constructor or via setters or via inject*() methods, instead of looking up a singleton trough a static method only to obtain a reference.
Once this fundamental decoupling is achieved, the hard part is tackling the construction problem: Zend Framework has a big codebase and writing a factory (manual dependency injection) for every use case is not viable.
Thus, automatic dependency injection needs to be called upon. There are many xml-configured dependency injection frameworks for php to incorporate, but let's show a simple example of how they works.
Suppose we want a reference to an instance of My_Class in a controller, and we want to have its collaborators automatically injected by some component. As the requirements say, My_Class has a unified constructor which would pass the collaborators to the setters, but dependencies resolution is possible also with setter-based injection. I would really prefer a bunch of setters if there are many dependencies.
This code is based on Zend Framework 1.x classes as I do not want to counfound anyone.
<?php class My_Class { public function __construct($options) { // calling setters or having them called... } public function setAdapter(Zend_Db_Adapter $adapter) { if (isset($this->_adapter)) { throw new Exception('Adapter cannot be changed once set.'); } $this->_adapter = $adapter; } }Since I want to show how to eliminate singletons, I have declared a dependency towards a Zend_Db_Adapter instance, which is commonly put in Zend_Db_Table::setDefaultAdapter() as a singleton. It does not matter that singletonitis is cared for by another class: in this case it is still mutable global state and the same problem is present for the front controller instance. I did not feel like including a Zend_Front_Controller instance in this example as it is often used only as a mean to access other objects, and the underlying problem (Law of Demeter breakage) is not resolved by injecting it.
Configuration has to be defined in a plain old array which will be used by the container:
<?php $config = array( 'My_Class' => array( 'adapter' => 'Zend_Db_Adapter_Mysqli' ), 'Zend_Db_Adapter_Mysqli' => array( 'driver_options' => ... // username and password here ), 'My_Controller' => array( 'my_class' => 'My_Class' ) );Eventually, the controller can now ask for collaborators instead of grabbing them from other sources. This data structure is very basic but it will do the trick for now.
An automatic dependency injection component would simply recursively resolve dependencies:
<?php class Injector { private $_config; public function __construct($config) { // assigning config to private member.. } public function newInstance($class) { if (!isset($this->_config[$class])) { // it's a literal value like 'mydbpassword'; return $class; } $collaborators = array(); foreach ($this->_config[$class] as $collaboratorName => $collaboratorClass) { $collaborators[$collaboratorName] = $this->newInstance($collaboratorClass); } return new $class($collaborators); } }and the bootstrap would be very simple:
$injector = new Injector($config); $application = $injector->newInstance('Zend_Application');Of course Zend_Application might be refactored to become the injector itself, so the object really constructed could be an action or a front controller. This code is very basic and can be improved with objects caching (to prevent multiple connections from being instantiated) where appropriate, a list of "global" classes (which scope is however limited to a Zend_Application object and can be throwed away whenever you want) to prevent configuration to grow too much, and in another hundred ways.
The problem with this approach is that we have, for example, to instance all the controllers and all the view helpers because we don't know which will be used during this request, since the object graph construction process is completed before the request management: what did you expect from ten lines of not-TDDed code? :)
This paradigm of one-time instantiation is typical in Java applications, where nearly eveything is instanced in the bootstrap "just in case". Php has a shared-nothing architecture and instancing more than the necessary objects would be a waste.
In the next post I will solve this big issue using deferred istantiation and different injectors, and showing how nearly all singletons can be reduced to injected collaborators.
No comments:
Post a Comment