深入探讨依赖注入

/ PHP

从测试角度探讨依赖注入

依赖反转原则是SOLID 中最难理解的原则,而依赖注入则是单元测试的基石,本文将从测试角度探讨依赖反转与依赖注入,并将Laravel 的service container、constructor injection 与method injection 应用在实务上。

Version

PHP 7.0.0

Laravel 5.2.29

实际案例

假设目前有3家货运公司,每家公司的计费方式不同,使用者可以动态选择不同的货运公司,将一步步的重构成依赖注入方式

传统写法

传统我们会使用 if elsenew 来建立物件。

BlackCat.php

app/Services/BlackCat.php
namespace  App \ Services ;

class  BlackCat
{ 
    /** 
     * @param int $weight 
     * @return int 
     */ 
    public  function  calculateFee ( $weight )
     { 
        return  100 + $weight * 10 ; 
    } 
}

黑猫的计费方式。

Hsinchu.php

app/Services/Hsinchu.php

namespace  App \ Services ;

class  Hsinchu
{ 
    /** 
     * @param int $weight 
     * @return int 
     */ 
    public  function  calculateFee ( $weight )
     { 
        return  80 + $weight * 15 ; 
    } 
}

新竹货运的计费方式。

PostOffice.php

app/Services/PostOffice.php

namespace  App \ Services ;

class  PostOffice
{ 
    /** 
     * @param int $weight 
     * @return int 
     */ 
    public  function  calculateFee ( $weight )
     { 
        return  70 + $weight * 20 ; 
    } 
}

邮局的计费方式。

ShippingService.php

app/Services/ShippingService.php

namespace  App \ Services ;

use  Exception ;

class  ShippingService
{ 
    /** 
     * @param string $companyName 
     * @param int $weight 
     * @return int 
     * @throws Exception 
     */ 
    public  function  calculateFee ( $companyName , $weight )
     { 
        if ( $companyName == 'BlackCat' ) { 
            $blackCat = new BlackCat(); 
            return  $blackCat ->calculateFee( $weight ); 
        } 
        elseif ( $companyName == 'Hsinchu' ) { 
            $hsinchu = new Hsinchu(); 
            return  $hsinchu ->calculateFee( $weight ); 
        } 
        elseif ( $companyName == 'PostOffice' ) { 
            $postOffice = new PostOffice(); 
            return  $postOffice ->calculateFee( $weight ); 
        } 
        else { 
            throw  new  Exception ( 'No company exception' ); 
        } 
    } 
}

calculateFee() 传入2个参数: $companyName$weight

使用者可自行由 $companyName 挑选货运公司,并传入 $weight 计算运费。

使用 if else 判断 $companyName 字串,并 new 出相对应物件,这是初学者学习物件导向时的写法。

ShippingService.php

app/Services/ShippingService.php

/** 
* @param string $companyName 
* @param int $weight 
* @return int 
* @throws Exception 
*/ 
public  function  calculateFee ( $companyName , $weight )
{ 
    switch ( $companyName ) { 
        case  'BlackCat' : 
            $blackCat = new BlackCat(); 
            return  $blackCat ->calculateFee( $weight ); 
        case  'Hsinchu' : 
            $hsinchu = new Hsinchu(); 
            return  $hsinchu ->calculateFee ( $weight ); 
        case  'PostOffice' : 
            $postOffice = new PostOffice(); 
            return  $postOffice ->calculateFee( $weight ); 
        default : 
            throw  new  Exception ( 'No company exception' ); 
    } 
}

if else 重构成 switch,可稍微改善程式码的可读性。

使用Interface

目前的写法,执行上没有什么问题,若以TDD开发,我们将得到第一个绿灯。

我们将继续重构成更好的程式。

目前我们是实际去 new Blackcat()new Hsinchu()new PostOffice(),也就是说 ShippingService 将直接相依于 BlackCatHshinchuPostOffice 3个class。

物件导向就是希望达到高内聚,低耦合的设计。所谓的低耦合,就是希望能减少相依于外部的class的数量。

何谓相依 ?

简单的说,有2 种写法会产生相依 :

  1. 去new 其他class。
  2. 去extends 其他class。

由于PHP 不用编译,所以可能较无法体会相依的严重性,但若是需要编译的程式语言,若你相依的class 的property 或method 改变,可能导致你的程式无法编译成功,也就是你必须配合相依的class 做相对应的修改才能通过编译,因此我们希望降低对其他class 的相依程度与数量。

GoF四人帮在设计模式曾说: Program to an Interface, not an Implementation。也就是程式应该只相依于interface,而不是相依于实际class,目的就是要藉由interface,降低对于实际class的相依程度。

若我们能将 BlackCatHshinchuPostOffice 3个class抽象化为1个 interface,则 ShippingService 将从相依3个class,降低成只相依于1个interface,
将大大降低 ShippingService与其他class的相依程度。

若以编译的角度,由于 ShippingService 只相依于 interface,因此 BlackCatHshinchuPostOffice 做任何修改都不会影响我 ShippingService 的编译。

LogisticsInterface.php

app/Services/LogisticsInterface.php

namespace  App \ Services ;

interface  LogisticsInterface
{ 
    /** 
     * @param int $weight 
     * @return int 
     */ 
    public  function  calculateFee ( $weight ) ; 
}

BlackCat 抽取出 LogisticsInterface,将 BlackCatHsinchuPostOffice 抽象化成 LogisticsInterface

BlackCat.php


app/Services/BlackCat.php

namespace  App \ Services ;

class  BlackCat  implements  LogisticsInterface
{ 
    /** 
     * @param int $weight 
     * @return int 
     */ 
    public  function  calculateFee ( $weight )
     { 
        return  100 * $weight * 10 ; 
    } 
}

BlackCat 实现 LogisticsInterface

Hsinchu.php


app/Services/Hsinchu.php

namespace  App \ Services ;

class  Hsinchu  implements  LogisticsInterface
{ 
    /** 
     * @param int $weight 
     * @return int 
     */ 
    public  function  calculateFee ( $weight )
     { 
        return  80 * $weight * 15 ; 
    } 
}

Hsinchu 实现 LogisticsInterface

PostOffice.php

app/Services/PostOffice.php

namespace  App \ Services ;

class  PostOffice  implements  LogisticsInterface
{ 
    /** 
     * @param int $weight 
     * @return int 
     */ 
    public  function  calculateFee ( $weight )
     { 
        return  70 * $weight * 20 ; 
    } 
}

PostOffice 实现 LogisticsInterface

ShippingService.php

app/Services/ShippingService.php

namespace  App \ Services ;

use  Exception ;

class  ShippingService
{ 
    /** 
     * @param string $companyName 
     * @param int $weight 
     * @return int 
     * @throws Exception 
     */ 
    public  function  calculateFee ( $companyName , $weight )
     { 
        switch ( $companyName ) { 
            case  'BlackCat' : 
                $logistics = new BlackCat(); 
                return  $logistics ->calculateFee( $weight ); 
            case  'Hsinchu' : 
                $logistics = new Hsinchu(); 
                return  $logistics ->calculateFee( $weight ); 
            case  'PostOffice' : 
                $logistics = new PostOffice(); 
                return  $logistics ->calculateFee( $weight ); 
            default : 
                throw  new  Exception ( 'No company exception' ); 
        } 
    } 
}

$logistics 的型别都是 LogisticsInterface,目前PHP 7对于变数还没有支援type hint,所以程式码看起来差异不大,
但藉由PHPDoc,在PhpStorm打 $logistics->,已经可以得到语法提示: calculateFee( )
表示PhpStorm已经知道 BlackCatHsinchuPostOffice 都是 LogisticsInterface 型别的物件,
也就是对于 ShippingService 来说,BlackCatHsinchuPostOffice 都已经抽象化成 LogisticsInterface

工厂模式

虽然已经将 BlackCatHsinchuPostOffice 抽象化成 LogisticsInterface,但是在 ShoppingService 中,仍看到 new Blackcat()new Hsinchu()
new PostOffice(),对于ShoppingService而言,我们看到了3个问题:

  1. 违反单一职责原则 : calculateFee()原本应该只负责计算运费,现在却还要负责建立货运公司物件。
  2. 违反开放封闭原则 :将来若有新的货运公司供使用者选择,势必修改switch。
  3. 实质相依数为3 :虽然已经重构出interface,但实际上却还必须new 3个class。

比较好的方式是将 new 封装在 LogisticsFactory

LogisticsFactory.php

app/Services/LogisticsFactory.php

namespace  App \ Services ;

use  Exception ;

class  LogisticsFactory
{ 
    /** 
     * @param string $companyName 
     * @return LogisticsInterface 
     * @throws Exception 
     */ 
    public  static  function  create (string $companyName )
     { 
        switch ( $companyName ) { 
            case  'BlackCat' : 
                return  new BlackCat(); 
            case  'Hsinchu ' : 
                return  new Hsinchu(); 
            case  'PostOffice' : 
                return  new PostOffice(); 
            default : 
                throw  new  Exception ( 'No company exception' ); 
        } 
    } 
}

Simple Factory模式使用了 static create(),专门负责建立货运公司物件:

  1. 专门负责建立货运公司的逻辑,符合单一职责原则。

ShippingService.php

app/Services/ShippingService.php

namespace  App \ Services ;

use  Exception ;

class  ShippingService
{ 
    /** 
     * @param string $companyName 
     * @param int $weight 
     * @return int 
     * @throws Exception 
     */ 
    public  function  calculateFee ( $companyName , $weight )
     { 
        $logistics = LogisticsFactory::create( $companyName ); 
        return  $logistics ->calculateFee( $weight ); 
    } 
}

将来有新的货运公司,也只要统一修改 LogisticsFactory 即可,将其变化封装在 LogisticsFactory,对于 ShoppingService 开放封闭。

ShoppingService 从相依于3个class降低成仅相依于 LogisticsInterfaceLogisticsFactory,实质相依数降为2。

程式的可测试性

符合spec 的程式,并不代表是好的程式,一个好的程式还要符合5 个要求 :

  1. 容易维护。
  2. 容易新增功能。
  3. 容易重复使用。
  4. 容易上Git,不易与其他人冲突。
  5. 容易写测试。
  6. 使用interface + 工厂模式,已经达到以上前4点要求,算是很棒的程式。

根据单元测试的定义:

单元测试必须与外部环境、类别、资源、服务独立,而不能直接相依。这样才是单纯的测试目标物件本身的逻辑是否符合预期。

若要对 ShippingService 进行单元测试,势必将 BlackCatHsinchuPostOffice 加以抽换隔离,但使用了工厂模式之后,
ShippingService 依然直接相依了 LogisticsFactory,而 LogisticsFactory 又直接相依 BlackCatHsinchuPostOffice
当我们对 ShippingService 做单元测试时,由于无法对 LogisticsFactory 做抽换隔离,因此无法对 ShippingService 做单元测试。

简单的说,interface + 工厂模式,仍然无法达到可测试性的要求,我们必须继续重构。

依赖反转

为了可测试性,单元测试必须可决定待测物件的相依物件,如此才可由单元测试将待测物件的相依物件加以抽换隔离。

换句话说,我们不能让待测物件直接相依其他class,而应该由单元测试订出interface,让待测物件仅能相依于interface,而实际相依的物件可由单元测试来决定,如此我们才能对相依物件加以抽换隔离。

这也就是所谓的依赖反转原则 :

高阶模组不该依赖低阶模组,两者都应该要依赖其抽象。
抽象不要依赖细节,细节要依赖抽象。

好像越讲越抽象XDD。

其中相依与依赖是相同的,只是翻译用字的问题。

何谓高阶模组? 何谓低阶模组?

高阶与低阶是相对的。

简单的说:

  • 当A class去new B class,A就是高阶模组,B就是低阶模组。

若以本例而言 :

  1. ShippingService 相对于 BlackCatShippingService 是高阶模组,BlackCat 是低阶模组,
  2. 单元测试相对于 ShippingService,单元测试是高阶模组,ShippingService 是低阶模组。
  3. ShippingController 相对于 ShippingServiceShippingController 是高阶模组,ShippingService 是低阶模组。

何谓抽象? 何谓细节?

  • interface 为抽象, abstract class 为抽象。
  • class 为细节去implement interface,class 为细节去extends abstract class。
    若以本例而言 :

img

在没有使用interface 前 :

  • ShippingService 直接 new BlackCat()
  • ShippingService 直接相依于 BlackCat
  • 也就是高阶模组依赖低阶模组。

img2

使用了interface 之后 :

  • ShippingService 没有相依于 BlackCat,也就是高阶模组没有依赖于低阶模组。
  • ShippingService 改成相依于 LogisticsInterface,也就是高阶模组依赖其抽象(因为new而相依)。
  • BlackCat 改成相依于 LogisticsInterface,也就是低阶模组也依赖其抽象(因为implements而相依)。
  • 也就是目前高阶模组与低阶模组都改成依赖其抽象。
  • 高阶模组 ShippingService 原本依赖的是低阶模组 BlackCatcalculateFee(),有了 interface 之后,变成反过来低阶模组 BlackCat 要依赖高阶模组所定义 LogisticsInterface的calculateFee(),所以称为依赖反转。

更简单的说,依赖反转就是要你使用interface 来写程式,而不要直接相依于class。

我们之前已经重构出 LogisticsInterface,事实上已经符合依赖反转。

依赖注入

有了依赖反转还不足以达成可测试性,依赖反转只确保了待测物件的相依物件相依于interface。

既然相依物件相依于interface,若单元测试可以产生该interface 的物件,并加以注入,就可以将相依物件加以抽换隔离,这就是依赖注入。

Constructor Injection

ShippingService.php


app/Services/ShippingService.php

namespace App\Services;

class ShippingService
{
    /** @var LogisticsInterface */
    private $logistics;

    /**
     * ShippingService constructor.
     * @param LogisticsInterface $logistics
     */
    public function __construct(LogisticsInterface $logistics)
    {
        $this->logistics = $logistics;
    }

    /**
     * @param int $weight
     * @return int
     */
    public function calculateFee($weight)
    {
        return $this->logistics->calculateFee($weight);
    }
}

12行

/** @var LogisticsInterface */
private $logistics;

/**
 * ShippingService constructor.
 * @param LogisticsInterface $logistics
 */
public function __construct(LogisticsInterface $logistics)
{
    $this->logistics = $logistics;
}

原本相依的 LogisticsInterface 型别的物件,改由 constructor 注入,藉由PHP的 type hint,描述要注入的物件型别为 LogisticsInterface

原本使用interface +工厂模式,实质相依数为2,改用constructor injection之后,连 LogisticsFactory 都不需要了,仅相依于 LogisticsInterface,实质相依数降为1。

17行

/** 
* @param int $weight 
* @return int 
*/ 
public  function  calculateFee ( $weight )
{ 
    return  $this ->logistics->calculateFee( $weight ); 
}

将原本的 logistics 物件改成field。

Service Container

我们目前已经有了依赖注入,对于可测试性只剩下最后一哩路,若我们能将mock 出的假物件,透过依赖注入取代掉原来的相依物件,就能将相依物件加以抽换隔离,达成隔离测试的要求,service container 就是要帮我们将相依物件抽换隔离。

Laravel 4称为IoC container,Laravel 5称为service container。
17以下句子来自于30天快速上手TDD Day 5:如何隔离相依性-基本的可测试性
事实上IoC (Inversion of Conttrol)与DI (Dependency Inversion)讲的是同一件事情,也就是由单元测试决定待测物件的相依物件。

单元测试

ShippingService.php

tests/Services/ShippingServiceTest.php

use  App \ Services \ BlackCat ; 
use  App \ Services \ LogisticsInterface ; 
use  App \ Services \ ShippingService ;

class  ShippingServiceTest  extends  TestCase
{ 
    /** @test */ 
    public  function黑猫单元测试()
     { 
        /** arrange */ 
        $expected = 110 ; 
        $weight = 1 ;

        $mock = Mockery::mock(BlackCat::class); 
        $mock ->shouldReceive( 'calculateFee' ) 
            ->once() 
            ->withAnyArgs() 
            ->andReturn( $expected );

        App::instance(LogisticsInterface::class, $mock );

        $target = App::make(ShippingService::class);

        /** act */ 
        $actual = $target ->calculateFee( $weight );

        /** assert */ 
        $this ->assertEquals( $expected , $actual ); 
    } 
}

14行

$mock = Mockery::mock(BlackCat::class); 
$mock ->shouldReceive( 'calculateFee' ) 
    ->once() 
    ->withAnyArgs() 
    ->andReturn( $expected );

因为单元测试,我们只想测试 ShippingService,因此想将其相依的 LogisticsInterface 物件抽换隔离,因此利用 Mockery 根据 BlackCat 建立假物件$mock,
并定义 calculateFee() 回传的期望值为 $expected

once() 为预期 calculateFee() 会被执行一次,且只会被执行一次,若完全没被执行,或执行超过一次,PHPUnit会显示红灯。

withAngArgs() 为不特别在乎 calculateFee() 的参数型别与个数,一般来说,单元测试在乎的是被mock method是否被正确执行,以及其回传值是否如预期,至于参数则不太重要。

20行

App::instance(LogisticsInterface::class, $mock );

mock物件已经建立好,接着要告诉service container,当constructor injection的type hint遇到 LogisticsInterface 时,该使用我们刚建立的 $mock 物件抽换隔离,而不是原来的相依物件。

App::instance() 用到的地方不多,一般就是用在需要mock时。

22行

$target = App::make(ShippingService::class);

当mock与service container都准备好时,接着要建立待测物件准备测试,这里不能再使用new建立物件,而必须使用service container提供的 App::make() 来建立物件,因为我们就是希望靠service container帮我们将mock物件抽换隔离原来的相依物件,因此必须改用service container提供的 App::make()

整合测试

ShippingService.php

/** @test */ 
public  function黑猫整合测试()
{ 
    /** arrange */ 
    $expected = 110 ; 
    $weight = 1 ;

    App::bind(LogisticsInterface::class, BlackCat::class);

    $target = App::make(ShippingService::class);

    /** act */ 
    $actual = $target ->calculateFee( $weight );

    /** assert */ 
    $this ->assertEquals( $expected , $actual ); 
}

当执行整合测试时,我们会希望实际执行相依物件的功能,而不再使用mock 将其相依物件抽换隔离。

第8行
App::bind(LogisticsInterface::class, BlackCat::class);

当constructor injection配合type hint时,若是class,Laravel的service container会自动帮我们注入其相依物件,但若type hint为interface时,因为可能有很多class implements该interface,所以必须先使用 App::bind( ) 告诉service container,当type hint遇到 LogisticsInterface 时,实际上要注入的是 BlackCat 物件。
10行

$target = App::make(ShippingService::class);

App::bind() 完成后,就可以使用 App::make() 建立待测物件,service container也会根据刚刚 App::bind() 的设定,自动依赖注入 BlackCat 物件。

Method Injection

Laravel 4 提出了constructor injection 实现了依赖注入,而Laravel 5 更进一步提出了method injection。

有constructor injection 不就已经可测试了吗? 为什么还需要method injection 呢?

由于Laravel 4 只有constructor injection,所以只要class 要实现依赖注入,唯一的管道就是constructor injection,若有些相依物件只有单一method 使用一次,也必须使用constructor injection,这将导致最后constructor 的参数爆炸而难以维护。

对于一些只有单一method 使用的相依物件,若能只在method 的参数加上type hint,就可自动依赖注入,而不需要动用constructor,那就太好了,这就是method injection。

public  function  store (StoreBlogPostRequest $request )
{ 
    // The incoming request is valid...
 }

如大家熟悉的form request,就是使用method injection,相依的StoreBlogPostRequest物件并不是透过constructor注入,而是在 store() 注入。

ShippingService.php

namespace  App \ Services ;

class  ShippingService
{ 
    /** 
     * @param LogisticsInterface $logistics 
     * @param int $weight 
     * @return int 
     */ 
    public  function  calculateFee (LogisticsInterface $logistics , $weight )
     { 
        return  $logistics ->calculateFee( $weight ); 
    } 
}

重构成method injection后,就不必再使用constructor与field,程式更加精简。

第1个参数为我们要注入的 LogisticsInterface 物件,第2个参数为我们原本要传的 $weight 参数

单元测试

ShippingService.php


use  App \ Services \ BlackCat ; 
use  App \ Services \ LogisticsInterface ; 
use  App \ Services \ ShippingService ;

class  ShippingServiceTest  extends  TestCase
{ 
    /** @test */ 
    public  function 黑猫单元测试()
     { 
        /** arrange */ 
        $expected = 110 ; 
        $weight = 1 ;

        $mock = Mockery::mock(BlackCat::class); 
        $mock ->shouldReceive( 'calculateFee' ) 
            ->once() 
            ->withAnyArgs() 
            ->andReturn( $expected );

        App::instance(LogisticsInterface::class, $mock );

        /** act */ 
        $actual = App::call(ShippingService::class . '@calculateFee' , [ 
            'weight' => $weight
         ]);

        /** assert */ 
        $this ->assertEquals( $expected , $actual ); 
    } 
}

20行


/** act */ 
$actual = App::call(ShippingService::class . '@calculateFee' , [ 
    'weight' => $weight
 ]);

之前mock 的部分,与constructor injection 相同,就不再解释。

关键在于 App::call(),这是一个在Laravel官方文件没有介绍的method,但Laravel内部却到处在用。

之前我们使用constructor injection,就要搭配 App::make() 才能自动依赖注入。

现在我们使用method injection,就要搭配 App::call() 才能自动依赖注入。

第1个参数要传的字串,是class完整名称,加上@与method名称。

第2 个参数要传的是阵列,也就是我们自己要传的参数,其中参数名称为key,参数值为value。

整合测试

ShippingService.php

public  function 黑猫整合测试()
{ 
    /** arrange */ 
    $expected = 110 ; 
    $weight = 1 ;

    App::bind(LogisticsInterface::class, BlackCat::class);

    /** act */ 
    $actual = App::call(ShippingService::class . '@calculateFee' , [ 
        'weight' => $weight
     ]);

    /** assert */ 
    $this ->assertEquals( $expected , $actual ); 
}

10行

/** act */ 
$actual = App::call(ShippingService::class . '@calculateFee' , [ 
    'weight' => $weight
 ]);

关键一样是使用 App::call()

为什么只能在controller 使用method injection,而无法在自己的presenter、service 或repository 使用method injection?

当初学习method injection时,我也非常兴奋,总算可以解决Laravel 4的constructor参数爆炸的问题,但发现只能用在controller,但无法用在自己的presenter、service或repository,一直学习到App::call ()时,问题才迎刃而解。

因为Laravel内部使用 App::call() 呼叫controller的method,因此你可以在controller无痛使用method injection,但若你自己的presenter、service或repository要使用method injection,就必须在controller搭配 App::call( ),如此service containter才会帮你自动依赖注入相依物件。

再谈可测试性

本文从头到尾,都是以可测试性的角度去谈依赖注入,而我个人也的确是在写单元测试之后,才领悟依赖反转与依赖注入的重要性。

若是不写测试,是否就不需要依赖反转与依赖注入呢?

之前曾经提到 :

IoC (Inversion of Conttrol) 与DI (Dependency Inversion) 讲的是同一件事情,也就是由单元测试决定待测物件的相依物件。
根据之前的经验,我们可以发现待测物件的相依物件都是在测试的App::bind()所决定。

之前有提到所谓的高阶模组与低阶模组是相对的,单元测试相对于service,单元测试是高阶模组,而service 是低阶模组。

对照于实际状况,controller 相对​​于service,controller是高阶模组,而service 是低阶模组。

我们可以在单元测试以 App::bind() 决定service的相依物件,同样的,我们也可以在controller以 App::bind() 去决定service的相依物件。

既然我们可以由controller去决定,去注入service的相依物件,我们就不再被底层绑死,不再依赖底层service,而是由低阶模组去依赖高阶模组所制定的interface,再由controller的 App::bind() 来决定低阶模组的相依物件,这就是所谓的依赖反转。

也就是说,若高层模组可以决定低阶模组的相依物件,那整个设计的弹性与扩充性会非常好,因为需求都来自于人,而人所面对的是高阶模组,而高阶模组可以透过依赖注入去决定低阶模组的相依物件,而不是被低阶模组绑死,可弹性地依照需求而改变。

若程式符合可测试性的要求,表示其具有低耦合的特性,也就是物件导向强调的高内聚,低耦合,因此程式将更容易维护,更容易新增功能,更容易重复使用,更容易上Git,不易与其他人冲突,也就是说我们可以将程式的可测试性,当成是否为好程式的指​​标之一。

生活中的依赖反转

举个生活上实际的例子,事实上硬体产业就大量使用依赖反转。

比如电脑需要将画面送到显示器,系统厂对design house 发出需求,此时系统厂相当于高阶模组,而design house 相当于低阶模组。

Design house 当然可以设计出IC 符合系统厂需求,但由于系统厂没有规定任何传输介面规格,只提出显示需求,因此design house 可以使用自己设计的专属传输介面,系统厂的电路板只要符合design house 的专属传输介面规格,就可以将电脑画面传送到显示器。

这样虽然可以达成需求,但有几个问题:

  1. 传输介面由design house 规定,只要design house 传输介面更改,系统厂的电路板就得跟着修改。
  2. Design house 的专属传输介面,需要搭该公司的控制IC,因此系​​统厂还被绑死要使用该design house 的控制IC。
  3. 由于使用专属传输介面,因此系统厂无法使用替代料,只能乖乖使用该design house 的IC,没有议价空间,且备料时间也被绑死。
  4. 这就是典型的高阶模组依赖低阶模组,也就是系统厂被design house 绑死了。

所以系统厂很聪明,会联络各大系统厂一起制定传输介面规格,如VGA、HDMI、Display Port…等,如此deisgn house 就得乖乖的依照系统厂制定的传输介面规格来设计IC,这样系统厂就不再被单一design house 绑死,可以自行选择控制IC,还可以找替代料,增加议价空间,备料时间也更加弹性,这就是典型的低阶模组反过来依赖高阶模组所制定的规格,也就是依赖反转

Conclusion

  • Interface + 工厂模式无法达成可测试性的要求,因此才有了依赖注入与service container。
  • 若很多method 都使用相同相依物件,可使用constructor injection,若只有单一method 使用的相依物件,建议改用method injection。
  • Method Injection必须搭配 App::call(),除了自动依赖注入相依物件外,也可以自行传入其他参数。
  • 可测试性与物件导向是相通的,我们可以藉由程式的可测试性,当成是否为好程式的指​​标之一。

Sample Code

完整的范例

本文翻译转自:http://oomusou.io/tdd/tdd-di/#Method_Injection