The Clean Architecture in PHP 读书笔记(四)

Dependency Injection

上篇最重要的是介绍了去耦的工具之一设计原则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直接实例化出类来使用,带来的问题有:

  1. It makes it hard to make changes later

    由于依赖于一个具体的实现,具体的东西一般都是易变的,根据SOLID中D(Dependency Inversion Principle)原则,这显然会导致代码不易重构

  2. It makes it hard to test

    由于内部生成使用的类,我们测试的时候无法去除易变量,保证不了测试的时候只有一个可变部分

  3. 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请求,这么做的好处是:

  1. 收敛了CustomerRepository的创建,一旦创建CustomerRepository需要改变什么,可以只要一处修改就可以了
  2. 方便测试,我们可以根据测试要求,返回我们需要的CustomerRepository,如下面代码所示:
$serviceLocator->setFactory( 'CustomerRepository', function () {
    return new Path\To\MockCustomerRepository(
        [
            1001 => ( new Customer() )->setName( 'ACME Corp' ),
        ] );
} );

到这边是不是已经是做合理的代码了呢?我们能不能进一步优化呢?答案是:yes!

我们来看下现在的实现还存在的问题:

  1. 仍然需要自己在需要时候,像serviceLocator请求
  2. 为了测试,我们需要修改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个:

  1. When the dependency is used by more than one component

    当有多个地方都去new同一个类,并且构建需要一些额外动作的时候,我们就要考虑将构建这个动作封装起来了,核心点是:代码复用

  2. The dependency has different configurations in different contexts

    当创建的逻辑变得复杂的时候,我们需要将创建抽取出来,核心点是:单一职责,关注点分离

  3. When you need a different configuration to test a component

    如果为了测试需要依赖类返回不同的测试数据,这就要将依赖变为注入的,核心点是:可测性

  4. 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

依赖注入那些事儿

laravel 学习笔记 —— 神奇的服务容器

ioc

如果大家对第一篇英文文章感兴趣,有时间我可以翻译下,通俗的给讲一下的。

这是The Clean Architecture in PHP的第四篇,你的鼓励是我继续写下去的动力,期待我们共同进步。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容