从测试角度探讨依赖注入
依赖反转原则是SOLID 中最难理解的原则,而依赖注入则是单元测试的基石,本文将从测试角度探讨依赖反转与依赖注入,并将Laravel 的service container、constructor injection 与method injection 应用在实务上。
Version
PHP 7.0.0
Laravel 5.2.29
实际案例
假设目前有3家货运公司,每家公司的计费方式不同,使用者可以动态选择不同的货运公司,将一步步的重构成依赖注入方式
传统写法
传统我们会使用
if else
与 new
来建立物件。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
将直接相依于 BlackCat
、Hshinchu
与PostOffice
3个class。
物件导向就是希望达到高内聚,低耦合的设计。所谓的低耦合,就是希望能减少相依于外部的class的数量。
何谓相依 ?
简单的说,有2 种写法会产生相依 :
- 去new 其他class。
- 去extends 其他class。
由于PHP 不用编译,所以可能较无法体会相依的严重性,但若是需要编译的程式语言,若你相依的class 的property 或method 改变,可能导致你的程式无法编译成功,也就是你必须配合相依的class 做相对应的修改才能通过编译,因此我们希望降低对其他class 的相依程度与数量。
GoF四人帮在设计模式曾说: Program to an Interface, not an Implementation。也就是程式应该只相依于interface,而不是相依于实际class,目的就是要藉由interface,降低对于实际class的相依程度。
若我们能将
将大大降低
BlackCat
、Hshinchu
与 PostOffice
3个class抽象化为1个 interface
,则 ShippingService
将从相依3个class,降低成只相依于1个interface,将大大降低
ShippingService
与其他class的相依程度。
若以编译的角度,由于
ShippingService
只相依于 interface
,因此 BlackCat
、Hshinchu
与 PostOffice
做任何修改都不会影响我 ShippingService
的编译。LogisticsInterface.php
app/Services/LogisticsInterface.php
namespace App \ Services ;
interface LogisticsInterface
{
/**
* @param int $weight
* @return int
*/
public function calculateFee ( $weight ) ;
}
从
BlackCat
抽取出 LogisticsInterface
,将 BlackCat
、Hsinchu
与 PostOffice
抽象化成 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已经知道
BlackCat
、Hsinchu
与 PostOffice
都是 LogisticsInterface
型别的物件,也就是对于
ShippingService
来说,BlackCat
、Hsinchu
与 PostOffice
都已经抽象化成 LogisticsInterface
工厂模式
虽然已经将
与
BlackCat
、Hsinchu
与 PostOffice
抽象化成 LogisticsInterface
,但是在 ShoppingService
中,仍看到 new Blackcat()
、new Hsinchu()
与
new PostOffice()
,对于ShoppingService
而言,我们看到了3个问题:- 违反单一职责原则 : calculateFee()原本应该只负责计算运费,现在却还要负责建立货运公司物件。
- 违反开放封闭原则 :将来若有新的货运公司供使用者选择,势必修改switch。
- 实质相依数为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()
,专门负责建立货运公司物件:- 专门负责建立货运公司的逻辑,符合单一职责原则。
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降低成仅相依于 LogisticsInterface
与 LogisticsFactory
,实质相依数降为2。程式的可测试性
符合spec 的程式,并不代表是好的程式,一个好的程式还要符合5 个要求 :
- 容易维护。
- 容易新增功能。
- 容易重复使用。
- 容易上Git,不易与其他人冲突。
- 容易写测试。
- 使用interface + 工厂模式,已经达到以上前4点要求,算是很棒的程式。
根据单元测试的定义:
单元测试必须与外部环境、类别、资源、服务独立,而不能直接相依。这样才是单纯的测试目标物件本身的逻辑是否符合预期。
若要对
当我们对
ShippingService
进行单元测试,势必将 BlackCat
、Hsinchu
与 PostOffice
加以抽换隔离,但使用了工厂模式之后,ShippingService
依然直接相依了 LogisticsFactory
,而 LogisticsFactory
又直接相依 BlackCat
、Hsinchu
与 PostOffice
,当我们对
ShippingService
做单元测试时,由于无法对 LogisticsFactory
做抽换隔离,因此无法对 ShippingService
做单元测试。
简单的说,interface + 工厂模式,仍然无法达到可测试性的要求,我们必须继续重构。
依赖反转
为了可测试性,单元测试必须可决定待测物件的相依物件,如此才可由单元测试将待测物件的相依物件加以抽换隔离。
换句话说,我们不能让待测物件直接相依其他class,而应该由单元测试订出interface,让待测物件仅能相依于interface,而实际相依的物件可由单元测试来决定,如此我们才能对相依物件加以抽换隔离。
这也就是所谓的依赖反转原则 :
高阶模组不该依赖低阶模组,两者都应该要依赖其抽象。
抽象不要依赖细节,细节要依赖抽象。
好像越讲越抽象XDD。
其中相依与依赖是相同的,只是翻译用字的问题。
何谓高阶模组? 何谓低阶模组?
高阶与低阶是相对的。
简单的说:
- 当A class去new B class,A就是高阶模组,B就是低阶模组。
若以本例而言 :
ShippingService
相对于BlackCat
,ShippingService
是高阶模组,BlackCat
是低阶模组,- 单元测试相对于
ShippingService
,单元测试是高阶模组,ShippingService
是低阶模组。 ShippingController
相对于ShippingService
,ShippingController
是高阶模组,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
原本依赖的是低阶模组BlackCat
的calculateFee()
,有了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)讲的是同一件事情,也就是由单元测试决定待测物件的相依物件。
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,所以必须先使用
10行
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 的专属传输介面规格,就可以将电脑画面传送到显示器。
这样虽然可以达成需求,但有几个问题:
- 传输介面由design house 规定,只要design house 传输介面更改,系统厂的电路板就得跟着修改。
- Design house 的专属传输介面,需要搭该公司的控制IC,因此系统厂还被绑死要使用该design house 的控制IC。
- 由于使用专属传输介面,因此系统厂无法使用替代料,只能乖乖使用该design house 的IC,没有议价空间,且备料时间也被绑死。
- 这就是典型的高阶模组依赖低阶模组,也就是系统厂被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
reference : https://segmentfault.com/a/1190000005117345
沒有留言:
張貼留言