深入探讨依赖注入

前端之家收集整理的这篇文章主要介绍了深入探讨依赖注入前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

从测试角度探讨依赖注入

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

Version

PHP 7.0.0

Laravel 5.2.29

实际案例

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

传统写法

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

BlackCat.PHP

  1. app/Services/BlackCat.PHP
  2. namespace App \ Services ;
  3.  
  4. class BlackCat
  5. {
  6. /**
  7. * @param int $weight
  8. * @return int
  9. */
  10. public function calculateFee ( $weight )
  11. {
  12. return 100 + $weight * 10 ;
  13. }
  14. }

黑猫的计费方式。

Hsinchu.PHP

  1. app/Services/Hsinchu.PHP
  2.  
  3. namespace App \ Services ;
  4.  
  5. class Hsinchu
  6. {
  7. /**
  8. * @param int $weight
  9. * @return int
  10. */
  11. public function calculateFee ( $weight )
  12. {
  13. return 80 + $weight * 15 ;
  14. }
  15. }

新竹货运的计费方式。

PostOffice.PHP

  1. app/Services/PostOffice.PHP
  2.  
  3. namespace App \ Services ;
  4.  
  5. class PostOffice
  6. {
  7. /**
  8. * @param int $weight
  9. * @return int
  10. */
  11. public function calculateFee ( $weight )
  12. {
  13. return 70 + $weight * 20 ;
  14. }
  15. }

邮局的计费方式。

ShippingService.PHP

  1. app/Services/ShippingService.PHP
  2.  
  3. namespace App \ Services ;
  4.  
  5. use Exception ;
  6.  
  7. class ShippingService
  8. {
  9. /**
  10. * @param string $companyName
  11. * @param int $weight
  12. * @return int
  13. * @throws Exception
  14. */
  15. public function calculateFee ( $companyName,$weight )
  16. {
  17. if ( $companyName == 'BlackCat' ) {
  18. $blackCat = new BlackCat();
  19. return $blackCat ->calculateFee( $weight );
  20. }
  21. elseif ( $companyName == 'Hsinchu' ) {
  22. $hsinchu = new Hsinchu();
  23. return $hsinchu ->calculateFee( $weight );
  24. }
  25. elseif ( $companyName == 'PostOffice' ) {
  26. $postOffice = new PostOffice();
  27. return $postOffice ->calculateFee( $weight );
  28. }
  29. else {
  30. throw new Exception ( 'No company exception' );
  31. }
  32. }
  33. }

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

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

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

ShippingService.PHP

  1. app/Services/ShippingService.PHP
  2.  
  3. /**
  4. * @param string $companyName
  5. * @param int $weight
  6. * @return int
  7. * @throws Exception
  8. */
  9. public function calculateFee ( $companyName,$weight )
  10. {
  11. switch ( $companyName ) {
  12. case 'BlackCat' :
  13. $blackCat = new BlackCat();
  14. return $blackCat ->calculateFee( $weight );
  15. case 'Hsinchu' :
  16. $hsinchu = new Hsinchu();
  17. return $hsinchu ->calculateFee ( $weight );
  18. case 'PostOffice' :
  19. $postOffice = new PostOffice();
  20. return $postOffice ->calculateFee( $weight );
  21. default :
  22. throw new Exception ( 'No company exception' );
  23. }
  24. }

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

  1. app/Services/LogisticsInterface.PHP
  2.  
  3. namespace App \ Services ;
  4.  
  5. interface LogisticsInterface
  6. {
  7. /**
  8. * @param int $weight
  9. * @return int
  10. */
  11. public function calculateFee ( $weight ) ;
  12. }

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

BlackCat.PHP

  1. app/Services/BlackCat.PHP
  2.  
  3. namespace App \ Services ;
  4.  
  5. class BlackCat implements LogisticsInterface
  6. {
  7. /**
  8. * @param int $weight
  9. * @return int
  10. */
  11. public function calculateFee ( $weight )
  12. {
  13. return 100 * $weight * 10 ;
  14. }
  15. }

BlackCat 实现 LogisticsInterface

Hsinchu.PHP

  1. app/Services/Hsinchu.PHP
  2.  
  3. namespace App \ Services ;
  4.  
  5. class Hsinchu implements LogisticsInterface
  6. {
  7. /**
  8. * @param int $weight
  9. * @return int
  10. */
  11. public function calculateFee ( $weight )
  12. {
  13. return 80 * $weight * 15 ;
  14. }
  15. }

Hsinchu 实现 LogisticsInterface

PostOffice.PHP

  1. app/Services/PostOffice.PHP
  2.  
  3. namespace App \ Services ;
  4.  
  5. class PostOffice implements LogisticsInterface
  6. {
  7. /**
  8. * @param int $weight
  9. * @return int
  10. */
  11. public function calculateFee ( $weight )
  12. {
  13. return 70 * $weight * 20 ;
  14. }
  15. }

PostOffice 实现 LogisticsInterface

ShippingService.PHP

  1. app/Services/ShippingService.PHP
  2.  
  3. namespace App \ Services ;
  4.  
  5. use Exception ;
  6.  
  7. class ShippingService
  8. {
  9. /**
  10. * @param string $companyName
  11. * @param int $weight
  12. * @return int
  13. * @throws Exception
  14. */
  15. public function calculateFee ( $companyName,$weight )
  16. {
  17. switch ( $companyName ) {
  18. case 'BlackCat' :
  19. $logistics = new BlackCat();
  20. return $logistics ->calculateFee( $weight );
  21. case 'Hsinchu' :
  22. $logistics = new Hsinchu();
  23. return $logistics ->calculateFee( $weight );
  24. case 'PostOffice' :
  25. $logistics = new PostOffice();
  26. return $logistics ->calculateFee( $weight );
  27. default :
  28. throw new Exception ( 'No company exception' );
  29. }
  30. }
  31. }

$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

  1. app/Services/LogisticsFactory.PHP
  2.  
  3. namespace App \ Services ;
  4.  
  5. use Exception ;
  6.  
  7. class LogisticsFactory
  8. {
  9. /**
  10. * @param string $companyName
  11. * @return LogisticsInterface
  12. * @throws Exception
  13. */
  14. public static function create (string $companyName )
  15. {
  16. switch ( $companyName ) {
  17. case 'BlackCat' :
  18. return new BlackCat();
  19. case 'Hsinchu ' :
  20. return new Hsinchu();
  21. case 'PostOffice' :
  22. return new PostOffice();
  23. default :
  24. throw new Exception ( 'No company exception' );
  25. }
  26. }
  27. }

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

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

ShippingService.PHP

  1. app/Services/ShippingService.PHP
  2.  
  3. namespace App \ Services ;
  4.  
  5. use Exception ;
  6.  
  7. class ShippingService
  8. {
  9. /**
  10. * @param string $companyName
  11. * @param int $weight
  12. * @return int
  13. * @throws Exception
  14. */
  15. public function calculateFee ( $companyName,$weight )
  16. {
  17. $logistics = LogisticsFactory::create( $companyName );
  18. return $logistics ->calculateFee( $weight );
  19. }
  20. }

将来有新的货运公司,也只要统一修改 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。
    若以本例而言 :

在没有使用interface 前 :

  • ShippingService 直接 new BlackCat()

  • ShippingService 直接相依于 BlackCat

  • 也就是高阶模组依赖低阶模组。

使用了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

  1. app/Services/ShippingService.PHP
  2.  
  3. namespace App\Services;
  4.  
  5. class ShippingService
  6. {
  7. /** @var LogisticsInterface */
  8. private $logistics;
  9.  
  10. /**
  11. * ShippingService constructor.
  12. * @param LogisticsInterface $logistics
  13. */
  14. public function __construct(LogisticsInterface $logistics)
  15. {
  16. $this->logistics = $logistics;
  17. }
  18.  
  19. /**
  20. * @param int $weight
  21. * @return int
  22. */
  23. public function calculateFee($weight)
  24. {
  25. return $this->logistics->calculateFee($weight);
  26. }
  27. }

12行

  1. /** @var LogisticsInterface */
  2. private $logistics;
  3.  
  4. /**
  5. * ShippingService constructor.
  6. * @param LogisticsInterface $logistics
  7. */
  8. public function __construct(LogisticsInterface $logistics)
  9. {
  10. $this->logistics = $logistics;
  11. }

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

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

17行

  1. /**
  2. * @param int $weight
  3. * @return int
  4. */
  5. public function calculateFee ( $weight )
  6. {
  7. return $this ->logistics->calculateFee( $weight );
  8. }

将原本的 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

  1. tests/Services/ShippingServiceTest.PHP
  2.  
  3. use App \ Services \ BlackCat ;
  4. use App \ Services \ LogisticsInterface ;
  5. use App \ Services \ ShippingService ;
  6.  
  7. class ShippingServiceTest extends TestCase
  8. {
  9. /** @test */
  10. public function黑猫单元测试()
  11. {
  12. /** arrange */
  13. $expected = 110 ;
  14. $weight = 1 ;
  15.  
  16. $mock = Mockery::mock(BlackCat::class);
  17. $mock ->shouldReceive( 'calculateFee' )
  18. ->once()
  19. ->withAnyArgs()
  20. ->andReturn( $expected );
  21.  
  22. App::instance(LogisticsInterface::class,$mock );
  23.  
  24. $target = App::make(ShippingService::class);
  25.  
  26. /** act */
  27. $actual = $target ->calculateFee( $weight );
  28.  
  29. /** assert */
  30. $this ->assertEquals( $expected,$actual );
  31. }
  32. }

14行

  1. $mock = Mockery::mock(BlackCat::class);
  2. $mock ->shouldReceive( 'calculateFee' )
  3. ->once()
  4. ->withAnyArgs()
  5. ->andReturn( $expected );

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

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

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

20行

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

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

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

22行

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

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

整合测试

ShippingService.PHP

  1. /** @test */
  2. public function黑猫整合测试()
  3. {
  4. /** arrange */
  5. $expected = 110 ;
  6. $weight = 1 ;
  7.  
  8. App::bind(LogisticsInterface::class,BlackCat::class);
  9.  
  10. $target = App::make(ShippingService::class);
  11.  
  12. /** act */
  13. $actual = $target ->calculateFee( $weight );
  14.  
  15. /** assert */
  16. $this ->assertEquals( $expected,$actual );
  17. }

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

第8行

  1. 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行

  1. $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。

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

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

ShippingService.PHP

  1. namespace App \ Services ;
  2.  
  3. class ShippingService
  4. {
  5. /**
  6. * @param LogisticsInterface $logistics
  7. * @param int $weight
  8. * @return int
  9. */
  10. public function calculateFee (LogisticsInterface $logistics,$weight )
  11. {
  12. return $logistics ->calculateFee( $weight );
  13. }
  14. }

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

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

单元测试

ShippingService.PHP

  1. use App \ Services \ BlackCat ;
  2. use App \ Services \ LogisticsInterface ;
  3. use App \ Services \ ShippingService ;
  4.  
  5. class ShippingServiceTest extends TestCase
  6. {
  7. /** @test */
  8. public function 黑猫单元测试()
  9. {
  10. /** arrange */
  11. $expected = 110 ;
  12. $weight = 1 ;
  13.  
  14. $mock = Mockery::mock(BlackCat::class);
  15. $mock ->shouldReceive( 'calculateFee' )
  16. ->once()
  17. ->withAnyArgs()
  18. ->andReturn( $expected );
  19.  
  20. App::instance(LogisticsInterface::class,$mock );
  21.  
  22. /** act */
  23. $actual = App::call(ShippingService::class . '@calculateFee',[
  24. 'weight' => $weight
  25. ]);
  26.  
  27. /** assert */
  28. $this ->assertEquals( $expected,$actual );
  29. }
  30. }

20行

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

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

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

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

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

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

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

整合测试

ShippingService.PHP

  1. public function 黑猫整合测试()
  2. {
  3. /** arrange */
  4. $expected = 110 ;
  5. $weight = 1 ;
  6.  
  7. App::bind(LogisticsInterface::class,BlackCat::class);
  8.  
  9. /** act */
  10. $actual = App::call(ShippingService::class . '@calculateFee',[
  11. 'weight' => $weight
  12. ]);
  13.  
  14. /** assert */
  15. $this ->assertEquals( $expected,$actual );
  16. }

10行

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

关键一样是使用 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

猜你在找的设计模式相关文章