Contents |
One of the most fundamental decision in object design is deciding where to put responsibilities. No one, and I mean no one, gets it right the first time. That's why refactoring is so important. As Kent Beck puts it, refactoring is the process of taking a system and adding to its value, not by changing its behavior but by giving it more of these qualities that enable us to continue developing at speed.
Zend_Controller 1.x issues:
The Front Controller needs one or more paths to directories containing action controllers in order to dispatch the request:
require_once 'Zend/Controller/Front.php'; $frontController = Zend_Controller_Front::getInstance(); $frontController->throwExceptions(true); $frontController->setControllerDirectoryName('controllers'); $frontController->setControllerDirectory('../application'); $frontController->dispatch();
To add additional directory names and paths, the user has to create a class that extends Zend_Controller_Dispatcher_Abstract and implements Zend_Controller_Dispatcher_Interface:
require_once 'Zend/Controller/Front.php'; $frontController = Zend_Controller_Front::getInstance(); $frontController->throwExceptions(true); /* Zend_Controller_Dispatcher_Extended violates the DRY priciple */ require_once 'Zend/Controller/Dispatcher/Extended.php'; $dispatcher = new Zend_Controller_Dispatcher_Extended(); $dispatcher->setControllerDirectoryName('controllers'); $dispatcher->setControllerDirectory('../application'); $dispatcher->setModelDirectory('../application/models'); $frontController->setDispatcher($dispatcher);
Extending the Zend_Controller_Dispatcher_Abstract class leads to the following problems:
In order to add additional methods, the user has to duplicate the behaviour and internal structure of the Zend_Controller_Dispatcher_Standard class. This approach violates the DRY principle:
abstract class Zend_Controller_Dispatcher_Abstract implements Zend_Controller_Dispatcher_Interface {} class Zend_Controller_Dispatcher_Extended extends Zend_Controller_Dispatcher_Abstract { // Required: New behaviour public function addModelDirectory($path) public function setModelDirectory($path) public function getModelDirectory() // Not required: Existent behaviour public function __construct(array $params = array()) public function formatModuleName($unformatted) public function formatClassName($moduleName, $className) public function classToFilename($class) public function isDispatchable(Zend_Controller_Request_Abstract $request) public function dispatch(Zend_Controller_Request_Abstract $request, Zend_Controller_Response_Abstract $response) public function loadClass($className) public function getControllerClass(Zend_Controller_Request_Abstract $request) public function isValidModule($module) public function getDefaultControllerClass(Zend_Controller_Request_Abstract $request) public function getDispatchDirectory() public function getActionMethod(Zend_Controller_Request_Abstract $request) }
Because the Front Controller is a singleton and cannot be subclassed, the user is unable to add additional accessor methods to the Front Controller, therefore there's no consistency in the way the data is provided and retrieved from the Dispatcher:
$frontController->addControllerDirectory('../application'); $frontController->getDispatcher()->addModelDirectory('../application/models'); $frontController->addModuleDirectory('../application/modules') $frontController->getDispatcher()->getDefaultModule();
You've probably heard that a class should handle a few clear responsibilities. In practice, classes grow. You add some operations here, a bit of data there. You add a responsibility to a class feeling that it's not worth a separate class, but as that responsibility grows and breeds, the class becomes too complicated. Extract Class is a common technique for moving features between objects.
Extract Class:
"You have one class doing work that should be done by two."
Zend Framework organizes code in a project structure and puts the project files into different directory structures:
The framework allows users to choose between one or the other. My main goal is to move this responsibility away from the Zend_Controller_Front class. To do this I need to use the Extract Class to alter the internal structure of the class without changing its external behaviour. This refactoring will reduce the complexity of the Front Controller and increase its flexibility. Also, it will allow users to define custom directory names and paths.
First, I need to identify the methods I want to extract from the Zend_Controller_Front class:
addControllerDirectory() setControllerDirectory() getControllerDirectory() removeControllerDirectory() addModuleDirectory() setModuleControllerDirectoryName() getModuleControllerDirectoryName()
Then, I need to create a set of classes to express the split-off responsibilities:
Zend_Controller_Directory_Abstract Zend_Controller_Directory_Exception Zend_Controller_Directory_Standard
And finally, I need to use Move Field and Move Method to move fields and methods over from Zend_Controller_Front to the new classes.
abstract class Zend_Controller_Directory_Abstract { public function setControllerDirectoryName() public function getControllerDirectoryName() public function setControllerDirectory() public function getControllerDirectory() public function addControllerDirectory() public function removeControllerDirectory() } class Zend_Controller_Directory_Standard extends Zend_Controller_Directory_Abstract { public function addApplicationDirectory() public function addModuleDirectory() }
All I need to do now is add a setter and getter method to the Zend_Controller_Front class and inject the object. For the sake of this example, I'll assume that I can add additional methods to the Zend_Controller_Front class.
class Zend_Controller_Front { /** * Instance of Zend_Controller_Directory_Abstract * @var Zend_Controller_Directory_Abstract */ protected $_directory = null; /** * Resets all object properties of the singleton instance * * Primarily used for testing; could be used to chain front controllers. * * @return void */ public function resetInstance() { $reflection = new ReflectionObject($this); foreach ($reflection->getProperties() as $property) { $name = $property->getName(); switch ($name) { case '_instance': break; case '_invokeParams': $this->{$name} = array(); break; case '_plugins': $this->{$name} = new Zend_Controller_Plugin_Broker(); break; case '_throwExceptions': case '_returnResponse': $this->{$name} = false; break; case '_directory': $this->{$name} = null; break; default: $this->{$name} = null; break; } } } /** * Convenience feature, calls setControllerDirectory()->setRouter()->dispatch() * * In PHP 5.1.x, a call to a static method never populates $this -- so run() * may actually be called after setting up your front controller. * * @param string|array $controllerDirectory Path to Zend_Controller_Action * controller classes or array of such paths * @return void * @throws Zend_Controller_Exception if called from an object instance */ public static function run($controllerDirectory) { $frontController = self::getInstance(); $frontController->getDirectory()->setControllerDirectory($controllerDirectory); $frontController->dispatch(); } /** * Return the directory object * * @return Zend_Controller_Directory_Abstract */ public function getDirectory() { if (!$this->_directory instanceof Zend_Controller_Directory_Abstract) { require_once 'Zend/Controller/Directory/Standard.php'; $this->_directory = new Zend_Controller_Directory_Standard(); } return $this->_directory; } /** * Set the directory object * * @param Zend_Controller_Directory_Abstract $directory * @return Zend_Controller_Front */ public function setDirectory(Zend_Controller_Directory_Abstract $directory) { $this->_directory = $directory; return $this; } }
Before:
require_once 'Zend/Controller/Front.php'; $frontController = Zend_Controller_Front::getInstance(); $frontController->throwExceptions(true); $frontController->setModuleControllerDirectoryName('controllers'); $frontController->setDefaultModule('default'); $frontController->addModuleDirectory('../application/modules');
After:
require_once 'Zend/Controller/Directory/Standard.php'; $directory = new Zend_Controller_Directory_Standard(); $directory->setControllerDirectoryName('controllers'); $directory->setDefaultModule('default'); $directory->addModuleDirectory('../application/modules'); require_once 'Zend/Controller/Front.php'; $frontController = Zend_Controller_Front::getInstance(); $frontController->throwExceptions(true); $frontController->setDirectory($directory);
I've demonstrated how to refactor the Front Controller of the Zend Framework using a refactoring known as “Extract Class”. Martin Fowler discusses this technique in "Refactoring: Improving the Design of Existing Code".
http://svn.fedecarg.com/repo/Zend/Controller/