Home » Code » 依赖注入与控制反转的PHP实现

依赖注入与控制反转的PHP实现

首先,来看一下相关概念。

依赖倒置原则(Dependence Inversion Principle, DIP)

DIP是一种软件设计的指导思想。传统软件设计中,上层代码依赖于下层代码,当下层出现变动时, 上层代码也要相应变化,维护成本较高。而DIP的核心思想是上层定义接口,下层实现这个接口, 从而使得下层依赖于上层,降低耦合度,提高整个系统的弹性。这是一种经实践证明的有效策略。

控制反转(Inversion of Control, IoC)

IoC就是DIP的一种具体思路,DIP只是一种理念、思想,而IoC是一种实现DIP的方法。 IoC的核心是将类(上层)所依赖的单元(下层)的实例化过程交由第三方来实现。 一个简单的特征,就是类中不对所依赖的单元有诸如 new 的实例化语句。

依赖注入(Dependence Injection, DI)

DI是IoC的一种设计模式,是一种套路,按照DI的套路,就可以实现IoC,就能符合DIP原则。 DI的核心是把类所依赖的单元的实例化过程,放到类的外面去实现。

控制反转容器(IoC Container)|依赖注入容器(DI Container)

当项目比较大时,依赖关系可能会很复杂。 而IoC Container或者DI Container提供了动态地创建、注入依赖单元,映射依赖关系等功能,减少了许多代码量。

总的来说就是:通过依赖注入设计模式,可以实现控制反转理念,从而实现依赖倒置原则。而为了更好的利用依赖注入实现控制反转,往往会设计一个控制反转容器或者依赖注入容器(反正就是一个容器,叫哪个都成)。

那么,先来看看传统的不遵循依赖倒置指导思想的程序是什么样的:

class A
{
	public function doSomething()
	{
		echo __METHOD__,'|';
	}
}

class B
{
	public function doSomething()
	{
		$a = new A();
		$a->doSomething();
		echo __METHOD__,'|';
	}
}

class C
{
	public function doSomething()
	{
		$b = new B();
		$b->doSomething();
		echo __METHOD__,'|';
	}
}

$c = new C();
$c->doSomething();//A::doSomething|B::doSomething|C::doSomething|

以上程序,类C的doSomething依赖于类B,而类B又依赖于类A,有着很明显的依赖关系。这样子的写法,如果下层的类A发生了变化,比如它的类名不叫A了,那么上层类B也要跟着进行修改。再或者,上层B类不想再依赖下层A,而是想依赖另一个下层D类,同样的要回去修改A类。前面说也说过很多次程序设计中有这么一个开闭原则:只扩展,不修改。现在这个样子,这显然是不好的,用另一个专业术语来说就是,耦合了。我们的程序要尽量的达到这个程度:无需修改任何一行程序代码,将功能加入至原先的应用程序中,也可以在不修改任何程序的情况下移除。

回到这个问题,使用依赖注入设计模式的话,C类需要B类,B类需要A类,先创建A类,再创建B类并将A类注入,再创建C类并将B类注入:

class A
{
	public function doSomething()
	{
		echo __METHOD__,'|';
	}
}

class B
{
	private $a;

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

	public function doSomething()
	{
		$this->a->doSomething();
		echo __METHOD__,'|';
	}
}

class C
{
	private $b;

	public function __construct($b)
	{
		$this->b = $b;
	}
	public function doSomething()
	{
		$this->b->doSomething();
		echo __METHOD__,'|';
	}
}

$c = new C(new B(new A()));
$c->doSomething();//A::doSomething|B::doSomething|C::doSomething|

这就是应用依赖注入思想实现了控制反转。你可以完全控制依赖关系,通过调整不同的注入对象,来控制程序的行为。上层的程序控制被反转到了下层。这样假如你下载的A类名称变了,只是调用的时候将A改为对应的名称即可,不需要修改上层B。类之间的耦合也被解除了。

上面代码示例的注入使用的是构造函数方式(Constructor injection),另外一种常见的注入方式则是定义一个setter方法(Setter injection)。

class A
{
	public function doSomething()
	{
		echo __METHOD__,'|';
	}
}

class B
{
	private $a;

	public function seta($a)
	{
		$this->a = $a;
	}

	public function doSomething()
	{
		$this->a->doSomething();
		echo __METHOD__,'|';
	}
}

class C
{
	private $b;

	public function setb($b)
	{
		$this->b = $b;
	}
	public function doSomething()
	{
		$this->b->doSomething();
		echo __METHOD__,'|';
	}
}

$c = new C();
$b = new B();
$a = new A();

$b->seta($a);
$c->setb($b);
$c->doSomething();//A::doSomething|B::doSomething|C::doSomething|

再另外一种做法就是通过魔术方法__set,__get直接设置属性来注入(Property Injection),反正要达到目的都相同:在后期将依赖手动注入,而不是一开始在类内部写死。

但这样做也并不完美,如果依赖少还可以,要是一多,得传入多个构造参数或者多次set或者设置很多个属性,代码看起来有那么一点累赘。这时候,IoC容器或者称DI窗口景要粉墨登场了。真实的容器可以实现自动绑定(Autowiring)或 自动解析(Automatic Resolution)、注释解器(Annotations)、延迟注入(Lazy injection)等功能,比较复杂。在使用之前必须先将依赖注入,如果一个类依赖多个类,个人感觉也一样得写上多个,跟多次set等没有什么本质区别。使用工厂模式等在获得类实例的方法里面多次set,也是只需要写一次而已,所以我个人是感觉不到这个容器的意义有多大。纠结了一阵问大神,大神说这容器主要是维护框架自已实例化的各种类的实例,比如router类,Controller类,等等,当然自己要使用也可以。好吧,历练不到,很难领悟到其精华。因此决定不写这一部分了,其实依赖注入&控制反转最核心的内容就是上边的——摒弃类内部的直接赋值依赖,采用外部set注入!也有这么一句话,不能为了设计模式而设计模式,还是得按需选择。当然,感觉兴趣的可以点击参考链接了解更多。

参考链接:

http://segmentfault.com/a/1190000002424023
http://www.digpage.com/di.html
http://blog.sina.com.cn/s/blog_7141dace0100lopb.html

2 comments

Leave a Reply

Your email address will not be published. Required fields are marked *

*

Time limit is exhausted. Please reload CAPTCHA.