更多优质内容
请关注公众号

面向对象和设计模式(九)SOLID原则之依赖倒置原则 DIP(Dependency Inversion Principle)-张柏沛IT博客

正文内容

面向对象和设计模式(九)SOLID原则之依赖倒置原则 DIP(Dependency Inversion Principle)

栏目:其他内容 系列:面向对象与设计模式 发布时间:2023-01-14 13:28 浏览量:1036
本系列文章目录
展开/收起

在介绍依赖倒置原则之前,不得不说一下控制反转(IOC)和 依赖注入(DI)这两个概念。


控制反转IOC (Inversion Of Control)

控制反转是一种指导流程化逻辑处理的思想,“控制”是指对程序执行流程的控制,“反转”是指整个流程的执行不应该由功能类或业务类自己控制,而应该由上层的一个专门的控制类控制,使得流程控制的逻辑写在控制类中,控制类依赖功能类提供的接口完成整个流程。

控制反转是一种比较笼统的设计思想,一般用来指导框架层面的设计以及流程化逻辑。很多人会将控制反转和依赖注入搞混,或者认为控制反转就是依赖注入。而其实依赖注入只是控制反转实现的方式之一。


下面我们看一个例子来体会一下什么是控制反转。假如有一个测试类用来测试用户相关的逻辑,测试一个模块需要做一些前置操作和后置操作。

<?php
class UserServiceTest {
  public static function doTest() {
    // ... 具体的测试逻辑
  }
  
  public static function run($args) {
    doSomethingBeforeTest();
    
    if (self::doTest()) {
      echo "Test succeed.";
    } else {
      echo "Test failed.";
    }
    
    doSomethingAfterTest();
  }
}


UserServiceTest就是一个具体的功能类或者说实现需求的类。run方法是一个测试流程的逻辑,表示要先做什么后做什么。整个测试流程中有固定的行为和多态的行为(不固定的行为),例如我还有一个测试订单Service的需求,需要写多一个OrderServiceTest类,并且也实现doTest()方法,订单的doTest和用户的doTest的具体实现肯定不同,那么doTest行为是不固定的,而doSomethingBeforeTest()和doSomethingAfterTest()则是固定的通用的行为。

此时我们大可不必在OrderServiceTest类的run方法中也写一遍对doSomethingBeforeTest()和doSomethingAfterTest()的调用。

而应该将流程化的逻辑run()抽到一个专门处理流程的类 UnitApplication,这个 UnitApplication 只需要依赖调用 Test类预留的doTest接口即可。


如下所示:

interface TestCase {
  public function doTest();
}

 class UserServiceTest implements{
  public static function doTest() {
    // ... 具体的测试逻辑
  }
  // run方法移除掉了
}


class UnitApplication {
  private static $testCases = [];
  
  public static function register(TestCase $testCase) {
    self::$testCases[] = $testCase;
  }
  
  public static function run() {
    foreach (self::$testCases as $testCase) {
       self::doSomethingBeforeTest($testCase);
       if($testCase->doTest()){
          echo "Test succeed.";
        } else {
          echo "Test failed.";
        }
       self::doSomethingAfterTest($testCase);
    }
  }
  
   public static function doSomethingBeforeTest(TestCase $testCase){
       //...   
   }
   
   public static function doSomethingAfterTest(TestCase $testCase){
       //...   
   }
}


将run(也就是流程的逻辑)的控制权反转(也就是交给专门控制流程的类)之后,代码的可扩展性和复用性得到极大的提升。

最后总结下,控制反转就是将流程化逻辑(第一步做什么,第二步做什么,第三步做什么)抽到一个专门的流程控制类中,对于流程中通用的逻辑就直接实现在控制类里,对于流程中的多态逻辑,则实现在依赖类中,委托依赖类来调用。



依赖注入DI(Dependency Injection)


依赖注入是指,不要在调用类中创建依赖对象,而是将依赖对象在外部创建好之后,通过传参的方式传给调用类使用。

依赖注入是一个标价 25 美元,实际上只值 5 美分的概念。听起来很“高大上”,实际上用起来非常简单。


例如:

<?php
abstract class Weapon(){		// 武器
    // ...
}
class Sword() extends Weapon{
    // ...
}

class Player(){		// 玩家
    public function fight(){
        $weapon = new Sword();
        // .. 用$weapon做一些事情    
    }
}

这段代码就是没有使用依赖注入的样子。


class Player(){		// 玩家
    public function fight(Weapon $weapon){
        // .. 用$weapon做一些事情    
    }
}

这段代码就是有使用依赖注入的样子。

依赖注入需要注意的是,尽可能依赖抽象而非实现。也就是说,声明注入的依赖对象时,要声明成它的接口类型或抽象类类型,而不要声明成它的具体类的类型或普通父类类型,这也遵循基于接口而非实现编程。


依赖注入的好处是做到类与类之间松耦合,避免紧耦合,有利于扩展。如果我在上述fight方法中new一个Sword类,那么fight方法就只能依赖“剑”这种武器,与 Swrod 类发生了强耦合,不利于扩展其他武器。而如果是通过依赖注入$weapon,并且声明Weapon抽象类型,那么Player类就是与武器类发送松耦合,不依赖单独的某一种武器。


当然,如果注入依赖类的时候声明对象的类型是一个具体类型而非抽象类型,那么即使使用了依赖注入,也达不到松耦合的目的。如下所示:

class Player(){		// 玩家
    public function fight(Sword $weapon){	// 依旧是紧耦合
        // .. 用$weapon做一些事情    
    }
}


依赖注入的三种方式:

1. 构造函数注入

class Player(){
    protected $weapon;
    public function __construct(Weapon $weapon){
        $this->weapon = $weapon;
    }
    
    public function fight(){
        // ...    
    }
}

构造函数注入方式下一旦注入了依赖对象后,就无法变更依赖对象。


2. setter方法注入

class Player(){
    protected $weapon;
    
    public function setWeapon(Weapon $weapon){
        $this->weapon = $weapon;
    }
    
    public function fight(){
        // ...    
    }
}

setter方法注入方式下,注入了依赖对象后,只需再调一次setter方法就可以变更依赖对象。


3. 调用者只在需要依赖的方法中注入

class Player(){		// 玩家
    public function fight(Weapon $weapon){  // 此时$weapon甚至可以不作为Player的属性,而是只作为一个普通变量
        // .. 用$weapon做一些事情    
    }
}

普通方法注入方式下,每次调用方法,都需要注入一次依赖对象,耦合性最低。

三种方式的耦合性依次是:构造函数注入 > setter方法注入 > 普通方法中注入。



依赖注入框架 DI Framework

依赖注入能降低类与类之间的耦合度,但是创建依赖对象、组装(或注入)对象的工作仅仅是被移动到了更上层代码而已,还是需要开发者自己来实现这一步。而且对象创建和依赖注入的工作,本身跟具体的业务无关,我们完全可以抽象成框架来自动完成。


举个例子:

class A{
    protected $b;
    protected $c;
    public function __construct(B $b, C $c){
            $this->b = $b;
            $this->c = $c;
    }
}

class B{
    protected $d;
    public function __construct(D $d){
            $this->d = $d;
    }
}

class C{
    protected $e, $f;
    public function __construct(E $e, F $f){
            $this->e = $e;
    }
}

class application{
    public function run(){
        $e = new E();
        $f = new F();
        $d = new D();
        $b = new B($d);
        $c = new ($e, $f);
        $a = new A($b, $c)
        
        // ... 用$a做一些事情
    }
}

application类的run方法为了使用对象A做一些事情,不得不在run()方法中实例化 $b~$e 对象,然后再一一注入。


为了解决创建依赖对象的问题,我们可以写一个专门获取对象的类,这个类就是依赖注入框架。类似于这样:

class application{
    public function run(){
        $a = Container::get('A');
        
        // ... 用$a做一些事情
    }
}

Container::get要做的事情是把A对象所依赖的$b~$e对象创建出来并且注入到A中,最后把A对象返回出来。


那么Container类如何知道A对象需要哪些依赖,B对象需要哪些依赖,如何将这些依赖组装起来?Container具体要如何实现,可以参考我的另一篇文章,这里不再赘述:

PHP设计模式篇(三) 注册树模式 + 反射 实现依赖注入容器



依赖倒置原则DIP(Dependency Inversion Principle)

依赖倒置原则又称为依赖反转原则,它的定义是:高层模块不要依赖低层模块。高层模块和低层模块应该通过抽象来互相依赖。除此之外,抽象不要依赖具体实现细节,具体实现细节应该依赖抽象。

这个原则的核心思想是依赖抽象(接口和抽象类)替代依赖实现(具体的类),基于接口而非实现编程。

依赖注入 + 依赖抽象 = 依赖倒置原则,或者说 依赖注入 + 依赖抽象 就能满足依赖倒置原则所要求的内容。




更多内容请关注微信公众号
zbpblog微信公众号

如果您需要转载,可以点击下方按钮可以进行复制粘贴;本站博客文章为原创,请转载时注明以下信息

张柏沛IT技术博客 > 面向对象和设计模式(九)SOLID原则之依赖倒置原则 DIP(Dependency Inversion Principle)

热门推荐
推荐新闻