依赖倒置和控制反转

依赖倒置

定义

依赖反转原则(Dependency inversion principle,DIP)是指一种特定的解耦形式,使得高层次的类不依赖于低层次的类的实现细节,依赖关系被颠倒(反转),从而使得低层次类依赖于高层次类的需求抽象。

该原则规定:

  1. 高层次的类不应该依赖于低层次的类,两者都应该依赖于抽象接口。
  2. 抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。

在传统的应用架构中,低层次的组件设计用于被高层次的组件使用,这一点提供了逐步的构建一个复杂系统的可能。在这种结构下,高层次的组件直接依赖于低层次的组件去实现一些任务。这种对于低层次组件的依赖限制了高层次组件被重用的可行性。

依赖反转原则的目的是把高层次组件从对低层次组件的依赖中解耦出来,这样使得重用不同层级的组件实现变得可能。

听起来很干,我们先通过实现一个简单的需求来描述依赖倒置原则要解决的问题。

需求

假设我们要实现一个简单的缓存功能,主要负责把数据存储起来方便以后调用。

为了快速完成需求,我们决定采用最简单的实现方式:存储到文件,所以我们设计其结构如下:

1

图 1 显示在应用程序中一共有两个类。"Cache" 类负责调用"FileWrite"类来写入文件。代码实现如下:

<?php

class FileWriter{
    
    public function writeToFile($key,$value=null){
        //....
    }
}

class Cache{
    protected FileWriter $file_writer;
    
    public function __contruct(){
        $this->file_writer=new FileWriter();
    }
    
    public function set($key,$value=null){
        $this->file_writer->writeToFile($key,$value);
    }
}

在所有使用文件来作为存储方式的系统中,上面的"Cache"类能很好的使用,然而在以其他方式来存储的系统中,"Cache" 类是无法被重用的。

例如,假设我们的系统是分布式服务,部署在多台机器上,这时候如果使用文件存储则缓存只能生效于本机器上,无法被其他机器使用,故我们无法使用文件来作为缓存的存储方式,所以我们引入一个新的存储方式:redis。另外我们也希望复用 "Cache" 类,但很不幸的是, "Cache" 类是直接依赖于 "FileWrite" 类的,无法直接被重用。

为了解决这个问题,我们需要修改下代码,如下:

<?php

class FileWriter{
    
    public function writeToFile($key,$value=null){
        //....
    }
}

class RedisWriter{
    
    public function writeToRedis($key,$value=null){
        //....
    }
}
class Cache{
    protected FileWriter $file_writer;
    protected RedisWriter $redis_writer; 
    protected $type;

    public function __contruct($type){
        $this->type=$type;
        if($this->type=='redis'){
            $this->file_writer=new FileWriter();
        }else if($this->type=='file'){
            $this->redis_writer=new RedisWriter();
        }
    }
    
    public function set($key,$value=null){
        if($this->type=='redis'){
            $this->file_writer->writeToRedis($key,$value);
        }else if($this->type=='file'){
            $this->file_writer->writeToFile($key,$value);
        }
    }
}

可以看到我们需要引入一个新的类,而且需要去修改"Cache"类的代码。随着需求的变化,我们可能有要支持其他的存储方式,例如数据库,memcached,这时候我们就需要不断添加新的类,不断修改"Cache"类,而且把"Cache"淹没在凌乱的"if/else"判断中,这样的设计的维护和拓展成本简直不可想象。

出现这些问题的原因就在于类间的相互依赖,主要特征是包含高层逻辑的类依赖于低层类的细节:"Cache"类的"set"功能完全依赖于下面的"FileWriter"的具体实现细节,导致在使用环境发生变化的时候,"Cache"类无法复用。依赖倒置原则就是为了来解决这个问题的。

问题

  • 没有抽象,耦合度高:当低层模块变动时,高层模块也得变动;
  • 高层模块过度依赖低层模块,很难扩展。
  • 这种依赖关系具有传递性,即如果是多层次的调用,最低层改动会影响较高层……直到最高层。

解决

高层次的类不应该依赖于低层次的类,两者都应该依赖于抽象接口:例如 "Cache" 类依赖于 "FileWriter" 类的实现,所以才会无法适用使用环境的变化。所以我们要想办法使 "Cache" 类不依赖于这些细节,因为具体实现是不断变化的,而抽象接口是相对稳定的,所以我们要把数据的存储抽象出来,成为一个接口,针对这个接口进行编程,这样就无需面对频繁变化的实现细节。

抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口:在一开始做设计的时候,我们不要去考虑具体实现,而应该根据业务需求去设计接口,例如上面的例子,我们一开始就已经考虑到了用文件来存储了,而且架构也是基于此来进行设计的,这就从一开始是高层次的类依赖于具体实现了。然而实际上我们需要的功能是:数据存储,把数据存储到某个地方,至于具体怎么存储我们其实并不需要关心,只需要知道能存即可,所以我们一开始设计的时候就不应该针对文件存储来进行编程,而应该针对抽象的存储接口来编程。同时具体实现也依据接口来进行编程。

因此我们优化后的架构如下:

2

此时类 "Cache" 既没有依赖 "FileWriter" 也没有依赖 "RedisWriter",而是依赖于接口"Writer",同时"FileWriter" 和 "RedisWriter" 的具体实现也依赖于抽象。

<?php

interface Writer{
    public writer($key,$value=null);
}
class FileWriter implement Writer{
    
    public function write($key,$value=null){
        //....
    }
}

class RedisWriter{
    
    public function write($key,$value=null){
        //....
    }
}
class Cache{
    protected Writer $writer;
    
    public function __contruct(){
        //$this->wirter=new RedisWriter();
        $this->wirter=new FileWriter();
    }
    
    public function set($key,$value=null){
        $this->file_writer->write($key,$value);
    }
}

此时,我们就可以重用 "Cache" 类,而不需要具体的"Writer"。在不同的环境条件下,我们只需要修改生成的"writer"类即可,"set"方法里面的逻辑完全不需要改动,因为这里面是针对抽象接口"Writer"编程,只要"Writer"没有变,"set"方法也不需要做任何修改。

使用场景

程序中所有的依赖关系都应该终止于抽象类或者接口中,而不应该依赖于具体类。
根据这个启发式规则,编程时可以这样做:

  • 类中的所有成员变量必须是接口或抽象,不应该持有一个指向具体类的引用或指针。
  • 任何类都不应该从具体类派生,而应该继承抽象类,或者实现接口。
  • 任何方法都不应该覆写它的任何基类中已经实现的方法。(里氏替换原则)
  • 任何变量实例化都需要实现创建模式(如:工厂方法/模式),或使用依赖注入框架(如:Spring IOC)。

优点

  • 高层模块和低层模块彻底解耦,都很容易实现扩展
  • 抽象模块具有很高的稳定性、可重用性,对高/低层模块来说才是真正"可依赖的"。

缺点

  • 增加了一层抽象层,增加实现难度;
  • 对一些简单的调用关系来说,可能是得不偿失的。
  • 对一些稳定的调用关系,反而增加复杂度,是不正确的。

控制反转

说完依赖倒置,我们再来说一个很相似的设计原则:控制反转。

看看上面的代码,虽然"set"方法里的逻辑不会发现变化了,但是在构造函数里还是要根据不同环境来生成对应的"Writer",还是需要修改代码,为了解决这个问题,我们引入控制反转的原则。

定义

控制反转(Inversion of Control,缩写为IoC ),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup)。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体,将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。

控制反转针对的是依赖对象的获得方式,也既依赖对象不在是自己内部生成,而是由外界生成后传递进来。如下代码:

<?php

interface Writer{
    public writer($key,$value=null);
}
class FileWriter implement Writer{
    
    public function write($key,$value=null){
        //....
    }
}

class RedisWriter{
    
    public function write($key,$value=null){
        //....
    }
}
class Cache{
    protected Writer $writer;

    public function __contruct(Writer $writer){
        $this->wirter=$writer
    }
    
    public function set($key,$value=null){
        $this->file_writer->write($key,$value);
    }
}

"Cache"把内部依赖"Writer"的创建权力移交给了上层模块,自己只关心依赖提供的功能,但并不关心依赖的创建。IoC 是一种新的设计模式,它对上层模块与底层模块进行了更进一步的解耦。

实现方式

实现控制反转主要有两种方式:依赖注入和依赖查找。两者的区别在于,前者是被动的接收对象,在类A的实例创建过程中即创建了依赖的B对象,通过类型或名称来判断将不同的对象注入到不同的属性中,而后者是主动索取相应类型的对象,获得依赖对象的时间也可以在代码中自由控制。

依赖注入

依赖注入有如下实现方式:

  • 基于构造函数。实现特定参数的构造函数,在新建对象时传入所依赖类型的对象。
  class Cache{
      protected Writer $writer;

      public function __contruct(Writer $writer){
          $this->wirter=$writer
      }
  }
  • 基于 set 方法。实现特定属性的public set方法,来让外部容器调用传入所依赖类型的对象。
  class Cache{
      protected Writer $writer;

      public function setWriter(Writer $writer){
          $this->wirter=$writer
      }
  }
  • 基于接口。实现特定接口以供外部容器注入所依赖类型的对象。
  interface WriterSetter {
       public function setWriter(Writer $writer);
  }
  class Cache implement WriterSetter{
      protected Writer $writer;
      
      @Override
      public function setWriter(Writer $writer){
          $this->wirter=$writer
      }
  }

接口注入和setter方法注入类似,不同的是接口注入使用了统一的方法来完成注入,而setter方法注入的方法名称相对比较随意,接口的存在,表明了一种依赖配置的能力。

在软件框架中,读取配置文件,然后根据配置信息,框架动态将一些依赖配置给特定接口的类,我们也可以说 Injector 也依赖于接口,而不是特定的实现类,这样进一步提高了准确性与灵活性。

依赖查找

依赖查找相比于依赖注入更加主动,先配置好对象的生成规则,然后在需要的地方通过主动调用框架提供的方法,根据相关的配置文件路径、key等信息来获取对象。

例如lumen里面:

app()->bind('classA', function ($app) {
    return new ClassA();
});

//使用
$classA=app()->make('classA');

参考

https://zh.wikipedia.org/wiki/%E4%BE%9D%E8%B5%96%E5%8F%8D%E8%BD%AC%E5%8E%9F%E5%88%99

https://zh.wikipedia.org/wiki/%E6%8E%A7%E5%88%B6%E5%8F%8D%E8%BD%AC

https://blog.csdn.net/briblue/article/details/75093382

Enjoy it !

如果觉得文章对你有用,可以赞助我喝杯咖啡~

版权声明

转载请注明作者和文章出处
作者: 小鱼儿
首发于 https://blog.ricoo.top/yi-lai-dao-zhi-he-kong-zhi-fan-zhuan/

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

推荐阅读更多精彩内容

  • 原文传送门 翻译方式: 中英文对照, 意译 In this article we will talk about ...
    萧哈哈阅读 546评论 0 3
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,497评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,057评论 25 707
  • 本文首发公众号“闲情笔谭” 谁的职场生涯中不会遇见一两个让人头疼的后辈呢?对于年轻人,谁能为你的成长买单呢,应该只...
    月尾紫阅读 488评论 0 1
  • 我喜欢你认真的模样,可是看到你眼底流露的忧伤,我会心疼。 这一路,我想陪着你走。 这一次,我绝不放开你。 没有什么...
    梅花树下梨花白阅读 149评论 0 0