上篇最重要的是介绍了去耦的工具之一设计原则SOLID,本篇将继续介绍去耦工具:依赖注入。
本文为系列文章的第四篇,前3篇地址是
The Clean Architecture in PHP 读书笔记(一)
The Clean Architecture in PHP 读书笔记(二)
The Clean Architecture in PHP 读书笔记(三)
The Clean Architecture in PHP 读书笔记(四)
到目前为止,我们在面向对象中遇到的最坏的code是:直接在一个类中实例化出另一个类,然后使用的。看代码:
class CustomerController {
public function viewAction()
{
$repository = new CustomerRepository();
$customer = $repository->getById( 1001 );
return $customer;
}
}
此处CustomerController
类如果脱离了CustomerRepository
类,将无法正常执行,对CustomerRepository
是强依赖。
这种通过new class直接实例化出类来使用,带来的问题有:
-
It makes it hard to make changes later
由于依赖于一个具体的实现,具体的东西一般都是易变的,根据SOLID中D(Dependency Inversion Principle)原则,这显然会导致代码不易重构
-
It makes it hard to test
由于内部生成使用的类,我们测试的时候无法去除易变量,保证不了测试的时候只有一个可变部分
-
We have no control over dependencies
由于使用new来新建类,我们无法根据条件,选择性的使用依赖类
控制反转
首先我们回答第一个问题:控制反转是什么?
原先我们在类中直接new出我们依赖的类,如前面CustomerController
中直接创建了CustomerRepository
;此时我们为了获得一定的灵活性,可能通过配置的方式来实例化出需要的Repository
来,简单的配置方式就是一些if,else
语句,现在我们再进一步,将这些if,else
配置移出CustomerController
类,通过一些外部的手段来达到相同的目的。
而上面我们介绍的这一个过程从类内部到类外部的过程就是所谓的:控制反转。上面提到的外部的手段主要有两种:
- 服务定位模式(Service Locator Pattern)
- 依赖注入(dependency injection)
下面先介绍第一个手段:服务定位模式
服务定位模式
先上代码,有个感性认识
public function viewAction()
{
$repository = $this->serviceLocator->get( 'CustomerRepository' );
$customer = $repository->getById( 1001 );
return $customer;
}
$serviceLocator->setFactory( 'CustomerRepository', function ( $sl ) {
return new Path\To\CustomerRepository(
$sl->get( 'Connection' )
);
} );
此时我们不会在viewAction
内部直接new出CustomerRepository
,而是向serviceLocator
请求,这么做的好处是:
- 收敛了
CustomerRepository
的创建,一旦创建CustomerRepository
需要改变什么,可以只要一处修改就可以了 - 方便测试,我们可以根据测试要求,返回我们需要的
CustomerRepository
,如下面代码所示:
$serviceLocator->setFactory( 'CustomerRepository', function () {
return new Path\To\MockCustomerRepository(
[
1001 => ( new Customer() )->setName( 'ACME Corp' ),
] );
} );
到这边是不是已经是做合理的代码了呢?我们能不能进一步优化呢?答案是:yes!
我们来看下现在的实现还存在的问题:
- 仍然需要自己在需要时候,像
serviceLocator
请求 - 为了测试,我们需要修改
setFactory
的方法,给出测试的实现
那有没有办法解决呢,当然有,那就是下面介绍的依赖注入
依赖注入
依赖注入是一个过程:将类它所依赖的外部类由它自己管理变为从外部注入。
下面会介绍两种注入方法:
- Setter injection
- Constructor injection
使用setter injection
此时前面的代码会变为:
$controller = new CustomerController();
$controller->setCustomerRepository( new CustomerRepository() );
$customer = $controller->viewAction();
class CustomerController {
protected $repository;
public function setCustomerRepository( CustomerRepository $repo )
{
$this->repository = $repo;
}
public function viewAction()
{
$customer = $this->repository->getById( 1001 );
return $customer;
}
}
依赖是通过setter方法注入的。
此时测试的时候,我们只需要通过setter方法设置MockCustomerRepository
即可。
这种方法有一个缺点:如果我们忘记了调用setter方法,那就完蛋了。
$controller = new CustomerController();
$customer = $controller->viewAction();
上面这种代码,只能等着fatal了。
使用Constructor injection
此时的代码长这个样子:
$controller = new CustomerController( new CustomerRepository() );
$customer = $controller->viewAction();
class CustomerController {
protected $repository;
public function __construct( CustomerRepository $repo )
{
$this->repository = $repo;
}
public function viewAction()
{
$customer = $this->repository->getById( 1001 );
return $customer;
}
}
我们不会出现之前setter injection那种忘了设计的问题了,因为我们通过构造函数声明了我们的规则,必须遵守。
讲了这么多依赖注入了,那我们什么时候使用呢?
什么时候使用依赖注入
依赖注入解决的是依赖问题,目的是去耦,那自然有耦合的地方就会有依赖注入,主要的场景有下面4个:
-
When the dependency is used by more than one component
当有多个地方都去new同一个类,并且构建需要一些额外动作的时候,我们就要考虑将构建这个动作封装起来了,核心点是:代码复用
-
The dependency has different configurations in different contexts
当创建的逻辑变得复杂的时候,我们需要将创建抽取出来,核心点是:单一职责,关注点分离
-
When you need a different configuration to test a component
如果为了测试需要依赖类返回不同的测试数据,这就要将依赖变为注入的,核心点是:可测性
-
When the dependency being injected is part of a third party library
当我们使用的依赖是第三方库的时候,我们更应该使用依赖注入,核心点是:依赖抽象,不变的
有那么多适合使用依赖注入的场景,那自然会有不适合的地方,如果需要构建的依赖足够简单,没有配置,我们无需引入依赖注入,依赖注入的引入是为了解决问题,而不是为了增加代码复杂性。
使用工厂来创建依赖
class CustomerController {
protected $repository;
protected $responseFactory;
public function __construct( CustomerRepository $repo, ResponseFactory $factory )
{
$this->repository = $repo;
$this->responseFactory = $factory;
}
public function viewAction()
{
$customer = $this->repository->getById( 1001 );
$response = $this->responseFactory->create( $this->params( 'context' ) );
$response->setData( 'customer', $customer );
return $response;
}
}
上面的需求是:我们希望能够根据参数的不同,创建不同的响应,可能是Html的,也可能是Json或者XML的,此时我们传入一个工厂,让工厂来负责根据不同的参数来产生不同的对象,核心点还是说依赖抽象的,不变的,易变的东西我们都不要。
处理很多依赖
依赖处理不好,很容易出现下面的代码:
public function __construct(
CustomerRepository $customerRepository,
ProductRepository $productRepository,
UserRepository $userRepository,
TaxService $taxService,
InvoiceFactory $invoiceFactory,
ResponseFactory $factory,
// ...
){
// ...
}
出现上面的原因是因为类违反了单一职责原则。没有一个硬性的指标说我们应该依赖多少类,但是一般来说是越少越好,意味着职责更专一。
我们仍然耦合嘛?
我们上面介绍了这么多依赖注入,目的都是为了去耦,回过头来看下,我们做了这么多是否去耦了呢?
最初我们的代码是:
public function viewAction()
{
$repository = new CustomerRepository();
$customer = $repository->getById( 1001 );
return $customer;
}
重构后是:
class CustomerController {
protected $repository;
public function __construct( CustomerRepository $repo )
{
$this->repository = $repo;
}
public function viewAction()
{
$customer = $this->repository->getById( 1001 );
return $customer;
}
}
此时CustomerController
仍然依赖于CustomerRepository
,具体是实现那就是易变的,我们的原则是依赖不变的,抽象的,因此,我们需要将进一步的去耦,这就是下面要介绍的:通过接口来定义契约。
最后给出一些讲依赖注入非常好的几篇文章:
Dependency "Injection" Considered Harmful
如果大家对第一篇英文文章感兴趣,有时间我可以翻译下,通俗的给讲一下的。
这是The Clean Architecture in PHP的第四篇,你的鼓励是我继续写下去的动力,期待我们共同进步。