早在2004年,Martin Fowler就提出了“哪些方面的控制被反转了?”这个问题。他总结出是依赖对象的获得被反转了。基于这个结论,他为控制反转创造了一个更好的名字:依赖注入。许多非凡的应用(比HelloWorld.java更加优美,更加复杂)都是由两个或是更多的类通过彼此的合作来实现业务逻辑,这使得每个对象都需要与其合作的对象(也就是它所依赖的对象)的引用。如果这个获取过程要靠自身实现,那么如你所见,这将导致代码高度耦合并且难以测试。
依赖倒置原则
依赖倒置原则(Dependence Inversion Principle)是程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。
A.高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。
B.抽象不应该依赖于具体实现,具体实现应该依赖于抽象。
实现方式1
实现一个轻量级的依赖注入容器
首先我们创建一个类,看起来是这样的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <?php class Di { protected $_service = []; public function set($name, $definition) { $this->_service[$name] = $definition; } public function get($name) { if (isset($this->_service[$name])) { $definition = $this->service[$name]; } else { throw new Exception("Service '" . name . "' wasn't found in the dependency injection container"); }
if (is_object($definition)) { $instance = call_user_func($definition); }
return $instance; } }
|
现在我们已经有了一个简单的类,包含一个属性和两个方法。假设我们现在有两个类,redisDB和cache,redisDB提供一个redis数据库的操作,cache负责缓存功能的实现并且依赖于redisDB。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| class redisDB { protected $_di;
protected $_options;
public function __construct($options = null) { $this->_options = $options; }
public function setDI($di) { $this->_di = $di; }
public function find($key, $lifetime) { }
public function save($key, $value, $lifetime) { }
public function delete($key) { } }
|
在这个类中我们简单实现了redis的查询、保存和删除。你可能会有疑问,另外一个方法setDi是做什么的。待我继续为你讲解。另一个类和当前这个类结构很像:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| class cache { protected $_di;
protected $_options;
protected $_connect;
public function __construct($options = null) { $this->_options = $options; }
public function setDI($di) { $this->_di = $di; }
protected function _connect() { $options = $this->_options; if (isset($options['connect'])) { $service = $options['connect']; } else { $service = 'redis'; }
return $this->_di->get($service); }
public function get($key, $lifetime) { $connect = $this->_connect; if (!is_object($connect)) { $connect = $this->_connect() $this->_connect = $connect; } ... return $connect->find($key, $lifetime); }
public function save($key, $value, $lifetime) { $connect = $this->_connect; if (!is_object($connect)) { $connect = $this->_connect() $this->_connect = $connect; } ... return $connect->save($key, $lifetime); }
public function delete($key) { $connect = $this->_connect; if (!is_object($connect)) { $connect = $this->_connect() $this->_connect = $connect; } ... $connect->delete($key, $lifetime); } }
|
现在我们就当已经实现了redisDB和cache这两个组件,具体的细节这里就先不做讨论了,来看看如何使用使用吧。首先需要将两个组件注入到容器中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <?php $di = new Di(); $di->set('redis', function() { return new redisDB([ 'host' => '127.0.0.1', 'port' => 6379 ]); }); $di->set('cache', function() use ($di) { $cache = new cache([ 'connect' => 'redis' ]); $cache->setDi($di); return $cache; });
$cache = $di->get('cache'); $cache->get('key'); $cache->save('key', 'value', 'lifetime'); $cache->delete('key');
|
到这里你可能会觉得这样以来反而有点繁琐了。cache和redisDB的结构如此之像,完全可以把redis写到cache中而没必要单独分离出来?但是你想过没有,有些数据及时性没那么高而且数量比较大,用redis有点不合适,mongodb是更好的选择;有些数据更新频率更慢,对查询速度也没要求,直接写入文件保存到硬盘可能更为合适;再或者,你的客户觉得redis运维难度有点大,让你给他换成memcache… 这就是为什么把它分离出来了。然后,继续改进代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| interface BackendInterface { public function find($key, $lifetime); public function save($key, $value, $lifetime); public function delete($key); }
class redisDB implements BackendInterface { public function find($key, $lifetime) { } public function save($key, $value, $lifetime) { } public function delete($key) { } }
class mongoDB implements BackendInterface { public function find($key, $lifetime) { } public function save($key, $value, $lifetime) { } public function delete($key) { } }
class file implements BackendInterface { public function find($key, $lifetime) { } public function save($key, $value, $lifetime) { } public function delete($key) { } }
$di = new Di();
$di->set('redis', function() { return new redisDB([ 'host' => '127.0.0.1', 'port' => 6379 ]); });
$di->set('mongo', function() { return new mongoDB([ 'host' => '127.0.0.1', 'port' => 12707 ]); });
$di->set('file', function() { return new file([ 'path' => 'path' ]); });
$di->set('fastCache', function() use ($di) { $cache = new cache([ 'connect' => 'redis' ]); $cache->setDi($di); return $cache; });
$di->set('cache', function() use ($di) { $cache = new cache([ 'connect' => 'mongo' ]); $cache->setDi($di); return $cache; });
$di->set('slowCache', function() use ($di) { $cache = new cache([ 'connect' => 'file' ]); $cache->setDi($di); return $cache; });
$cache = $di->get('cache');
|
我们新增加了一个接口BackendInterface,规定了redisDB,mongoDB,file这三个类必须实现这个接口所要求的功能,至于其他锦上添花的功能,随你怎么发挥。而cache的代码,好像没有变,因为cache不需要关心数据是怎么存入数据库或者文件中。而cache的调用者,也不需要关心cache具体是怎么实现的,只要根据接口实现相应的方法就行了。多人协作你会更加受益,你们只需要商定好接口,然后分别实现就行了。
这就是依赖注入的魅力所在了,虽然看似如此简单。
以上代码还可以继续改进,直到你认为无可挑剔为止。比如,redis服务在一个请求中可能会调用多次,而每次调用都会重新创建,这将有损性能。只需扩展一下DI容器就好增加一个参数或增加一个方法,随你。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| class Di { protected $_service = []; protected $_sharedService = []; public function set($name, $definition, $shared = false) { if ($shared) { $this->_sharedService[$name] = $definition; } else { $this->_service[$name] = $definition; } } public function get($name) { if (isset($this->_service[$name])) { $definition = $this->service[$name]; } else if ($this->_sharedService[$name]) { $definition = $this->_sharedService[$name]; } else { throw new Exception("Service '" . name . "' wasn't found in the dependency injection container"); } ... } ``` 这样以来,如果某个服务在一次请求中要调用多次,你就可以将shared属性设置为true,以减少不必要的浪费。如果你觉得每次在注入时都要setDi有点繁琐,想让他自动setDi,那可以这么做:
```php interface DiAwareInterface { public function setDI($di); public function getDI(); }
class Di { protected $service;
public function set($name, $definition) { $this->service[$name] = $definition; }
public function get($name) { ... if (is_object($definition)) { $instance = call_user_func($definition); }
if (is_object($instance)) { if ($instance instanceof DiAwareInterface) { $instance->setDI($this); } }
return $instance; } }
class redisDB implements BackendInterface, DiAwareInterface { public function find($key, $lifetime) { } public function save($key, $value, $lifetime) { } public function delete($key) { } }
$di->set('cache', function() { return new cache([ 'connect' => 'mongo' ]); });
|
我们现在所实现的这个DI容器还很简陋,还不支持复杂的注入,你可以继续完善它。
实现方式2
实现如何不考虑加载顺序,在实现前就要明白要是不考虑加载顺序就意味着让程序自动进行加载自动进行实例化。类要实例化,只要保证完整的传递给’__construct’函数所必须的参数就OK了,在类中如果要引用其他类,也必须在构造函数中注入,否则调用时仍然会发生错误。那么我们需要一个类,来保存类实例化所需要的参数,依赖的其他类或者对象以及各个类实例化后的引用
该类命名为盒子 ‘Container.class.php’, 其内容如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241
|
class Container{
private $_definitions = array();
private $_params = array();
private $_reflections = array();
private $_dependencies = array();
public function set($class, $defination = array(), $params = array()) { $this->_params[$class] = $params; $this->_definitions[$class] = $this->initDefinition($class, $defination); }
public function get($class, $params = array(), $properties = array()) { if(!isset($this->_definitions[$class])) { return $this->bulid($class, $params, $properties); }
$defination = $this->_definitions[$class];
if(is_callable($defination, true)) { $params = $this->parseDependencies($this->mergeParams($class, $params)); $obj = call_user_func($defination, $this, $params, $properties); } elseif(is_array($defination)) { $originalClass = $defination['class']; unset($definition['class']);
$properties = array_merge((array)$definition, $properties);
$params = $this->mergeParams($class, $params); if($originalClass === $class) { $obj = $this->bulid($class, $params, $properties); } else { $obj = $this->get($originalClass, $params, $properties); } } elseif(is_object($defination)) { return $defination; } else { throw new Exception($class . ' 声明错误!'); } return $obj; }
protected function mergeParams($class, $params = array()) { if(empty($this->_params[$class])) { return $params; } if(empty($params)) { return $this->_params; }
$result = $this->_params[$class]; foreach($params as $key => $value) { $result[$key] = $value; } return $result; }
protected function initDefinition($class, $defination) { if(empty($defination)) { return array('class' => $class); } if(is_string($defination)) { return array('class' => $defination); } if(is_callable($defination) || is_object($defination)) { return $defination; } if(is_array($defination)) { if(!isset($defination['class'])) { $definition['class'] = $class; } return $defination; } throw new Exception($class. ' 声明错误'); }
protected function bulid($class, $params, $properties) { list($reflection, $dependencies) = $this->getDependencies($class);
foreach ((array)$params as $index => $param) { $dependencies[$index] = $param; }
$dependencies = $this->parseDependencies($dependencies, $reflection);
$obj = $reflection->newInstanceArgs($dependencies);
if(empty($properties)) { return $obj; }
foreach ((array)$properties as $name => $value) { $obj->$name = $value; }
return $obj; }
protected function getDependencies($class) { if(isset($this->_reflections[$class])) { return array($this->_reflections[$class], $this->_dependencies[$class]); }
$dependencies = array(); $ref = new ReflectionClass($class); $constructor = $ref->getConstructor(); if($constructor !== null) { foreach($constructor->getParameters() as $param) { if($param->isDefaultValueAvailable()) { $dependencies[] = $param->getDefaultValue(); } else { $temp = $param->getClass(); $temp = ($temp === null ? null : $temp->getName()); $temp = Instance::getInstance($temp); $dependencies[] = $temp; } } } $this->_reflections[$class] = $ref; $this->_dependencies[$class] = $dependencies; return array($ref, $dependencies); }
protected function parseDependencies($dependencies, $reflection = null) { foreach ((array)$dependencies as $index => $dependency) { if($dependency instanceof Instance) { if ($dependency->id !== null) { $dependencies[$index] = $this->get($dependency->id); } elseif($reflection !== null) { $parameters = $reflection->getConstructor()->getParameters(); $name = $parameters[$index]->getName(); $class = $reflection->getName(); throw new Exception('实例化类 ' . $class . ' 时缺少必要参数:' . $name); } } } return $dependencies; } }
|
下面是’Instance’类的内容,该类主要用于记录类的名称,标示是否需要获取实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| class Instance{
public $id;
public function __construct($id) { $this->id = $id; }
public static function getInstance($id) { return new self($id); } }
|
然后我们在’Container.class.php’中还是实现了为类的实例动态添加属性的功能,若要动态添加属性,需使用魔术方法’__set’来实现,因此所有使用依赖加载的类需要实现该方法,那么我们先定义一个基础类 ‘Base.class.php’,内容如下
1 2 3 4 5 6 7 8 9 10 11 12
| class Base{
public function __set($name, $value) { $this->{$name} = $value; } }
|
然后我们来实现’A,B,C’类,A类的实例 依赖于 B类的实例,B类的实例依赖于C类的实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| class A extends Base{ private $instanceB;
public function __construct(B $instanceB) { $this->instanceB = $instanceB; }
public function test() { $this->instanceB->test(); } }
class B extends Base{ private $instanceC;
public function __construct(C $instanceC) { $this->instanceC = $instanceC; }
public function test() { return $this->instanceC->test(); } }
class C extends Base{ public function test() { echo 'this is C!'; } }
|
然后我们在’index.php’中获取’A’的实例,要实现自动加载,需要使用SPL类库的’spl_autoload_register’方法,代码如下
1 2 3 4 5 6 7 8 9
| function autoload($className) { include_once $className . '.class.php'; } spl_autoload_register('autoload', true, true); $container = new Container;
$a = $container->get('A'); $a->test();
|
上面的例子看起来是不是很爽,根本都不需要考虑’B’,’C’ (当然,这里B,C 除了要使用相应类的实例外,没有其他参数,如果有其他参数,必须显要调用’$container->set(xx)’方法进行注册,为其制定实例化必要的参数)。有细心同学可能会思考,比如我在先获取了’A’的实例,我在另外一个地方也要获取’A’的实例,但是这个地方’A’的实例需要其中某个属性不一样,我怎么做到?
你可以看到’Container’ 类的 ‘get’ 方法有其他两个参数,’$params’ 和 ‘$properties’ , 这个’$properties’ 即可实现刚刚的需求,这都依赖’__set’魔术方法,当然这里你不仅可以注册类,也可以注册方法或者对象,只是注册方法时要使用回调函数,例如
1 2 3 4 5 6
| $container->set('foo', function($container, $params, $config){ print_r($params); print_r($config); });
$container->get('foo', array('name' => 'foo'), array('key' => 'test'));
|
还可以注册一个对象的实例,例如
1 2 3 4 5 6 7 8 9 10 11 12
| class Test { public function mytest() { echo 'this is a test'; } }
$container->set('testObj', new Test());
$test = $container->get('testObj'); $test->mytest();
|
以上自动加载,依赖控制的大体思想就是将类所要引用的实例通过构造函数注入到其内部,在获取类的实例的时候通过PHP内建的反射解析构造函数的参数对所需要的类进行加载,然后进行实例化,并进行缓存以便在下次获取时直接从内存取得
应用实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
| <?php namespace Server\PromotionExtend\Lib;
class DI implements \ArrayAccess{
private static $_instance; protected $_service = []; protected $_sharedService = [];
public static function getInstance(){ if(!(self::$_instance instanceof self)){ self::$_instance = new self; } return self::$_instance; }
public function __clone(){ trigger_error('Clone is not allow!',E_USER_ERROR); }
public function get($name){ if(isset($this->_service[$name])){ return $this->_service[$name]; } if( ! isset($this->_sharedService[$name])){ trigger_error("Service {$name} Not Find !!!\n"); return null; } $concrete = $this->_sharedService[$name]['class']; $params = $this->_sharedService[$name]['params']; $obj = null; if($concrete instanceof \Closure){ $obj = call_user_func_array($concrete,$params); }elseif(is_string($concrete)){ if(empty($params)){ $obj = new $concrete; }else{ $class = new \ReflectionClass($concrete); $obj = $class->newInstanceArgs($params); } } if($this->_sharedService[$name]['shared'] == true && $obj){ $this->_service[$name] = $obj; } return $obj; }
public function has($name){ return isset($this->_sharedService[$name]) or isset($this->_service[$name]); }
public function remove($name){ unset($this->_sharedService[$name],$this->_service[$name]); return true; }
public function set($name, $class, $params = array()){ $this->registerService($name, $class, $params); return true; }
public function setShared($name, $class, $params = array()){ $this->registerService($name, $class, $params, true); }
private function registerService($name, $class, $params = array(), $shared = false){ $this->remove($name); if( ! ($class instanceof \Closure) && is_object($class)){ $this->_service[$name] = $class; }else{ $this->_sharedService[$name] = array('class'=>$class,'shared'=>$shared,'params'=>$params); } }
public function offsetExists($offset){ return $this->has($offset); }
public function offsetGet($offset){ return $this->get($offset); }
public function offsetSet($offset, $value){ return $this->set($offset,$value); }
public function offsetUnset($offset){ return $this->remove($offset); } }
class DiTest(){ public function test(){ $di = Di::getInstance(); $redis = new Redis($config['redis']['test']); $di->setShared('redis', $redis); $di['redis'] = $redis; $di->setShard('redis', function() use(config) { return new Redis($config['redis']['test']); }); $di->setShard('redis', 'Redis', $config['redis']['test']); $di['redis'] = function() use ($config) { return new Redis($config['redis']['test']); }; } }
|