依賴倒置和控制反轉(zhuǎn)

依賴倒置

定義

依賴反轉(zhuǎn)原則(Dependency inversion principle,DIP)是指一種特定的解耦形式务荆,使得高層次的類不依賴于低層次的類的實現(xiàn)細節(jié),依賴關(guān)系被顛倒(反轉(zhuǎn))穷遂,從而使得低層次類依賴于高層次類的需求抽象函匕。

該原則規(guī)定:

  1. 高層次的類不應(yīng)該依賴于低層次的類,兩者都應(yīng)該依賴于抽象接口蚪黑。
  2. 抽象接口不應(yīng)該依賴于具體實現(xiàn)盅惜。而具體實現(xiàn)則應(yīng)該依賴于抽象接口中剩。

在傳統(tǒng)的應(yīng)用架構(gòu)中,低層次的組件設(shè)計用于被高層次的組件使用抒寂,這一點提供了逐步的構(gòu)建一個復(fù)雜系統(tǒng)的可能结啼。在這種結(jié)構(gòu)下,高層次的組件直接依賴于低層次的組件去實現(xiàn)一些任務(wù)屈芜。這種對于低層次組件的依賴限制了高層次組件被重用的可行性郊愧。

依賴反轉(zhuǎn)原則的目的是把高層次組件從對低層次組件的依賴中解耦出來,這樣使得重用不同層級的組件實現(xiàn)變得可能井佑。

聽起來很干属铁,我們先通過實現(xiàn)一個簡單的需求來描述依賴倒置原則要解決的問題。

需求

假設(shè)我們要實現(xiàn)一個簡單的緩存功能躬翁,主要負責把數(shù)據(jù)存儲起來方便以后調(diào)用焦蘑。

為了快速完成需求,我們決定采用最簡單的實現(xiàn)方式:存儲到文件盒发,所以我們設(shè)計其結(jié)構(gòu)如下:

1

圖 1 顯示在應(yīng)用程序中一共有兩個類例嘱。"Cache" 類負責調(diào)用"FileWrite"類來寫入文件。代碼實現(xiàn)如下:

<?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);
    }
}

在所有使用文件來作為存儲方式的系統(tǒng)中迹辐,上面的"Cache"類能很好的使用蝶防,然而在以其他方式來存儲的系統(tǒng)中,"Cache" 類是無法被重用的明吩。

例如间学,假設(shè)我們的系統(tǒng)是分布式服務(wù),部署在多臺機器上印荔,這時候如果使用文件存儲則緩存只能生效于本機器上低葫,無法被其他機器使用,故我們無法使用文件來作為緩存的存儲方式仍律,所以我們引入一個新的存儲方式:redis嘿悬。另外我們也希望復(fù)用 "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"類的代碼炕横。隨著需求的變化源内,我們可能有要支持其他的存儲方式,例如數(shù)據(jù)庫份殿,memcached膜钓,這時候我們就需要不斷添加新的類嗽交,不斷修改"Cache"類,而且把"Cache"淹沒在凌亂的"if/else"判斷中颂斜,這樣的設(shè)計的維護和拓展成本簡直不可想象夫壁。

出現(xiàn)這些問題的原因就在于類間的相互依賴,主要特征是包含高層邏輯的類依賴于低層類的細節(jié):"Cache"類的"set"功能完全依賴于下面的"FileWriter"的具體實現(xiàn)細節(jié)焚鲜,導致在使用環(huán)境發(fā)生變化的時候掌唾,"Cache"類無法復(fù)用放前。依賴倒置原則就是為了來解決這個問題的忿磅。

問題

  • 沒有抽象,耦合度高:當?shù)蛯幽K變動時凭语,高層模塊也得變動葱她;
  • 高層模塊過度依賴低層模塊,很難擴展似扔。
  • 這種依賴關(guān)系具有傳遞性吨些,即如果是多層次的調(diào)用,最低層改動會影響較高層……直到最高層炒辉。

解決

高層次的類不應(yīng)該依賴于低層次的類豪墅,兩者都應(yīng)該依賴于抽象接口:例如 "Cache" 類依賴于 "FileWriter" 類的實現(xiàn),所以才會無法適用使用環(huán)境的變化黔寇。所以我們要想辦法使 "Cache" 類不依賴于這些細節(jié)偶器,因為具體實現(xiàn)是不斷變化的,而抽象接口是相對穩(wěn)定的缝裤,所以我們要把數(shù)據(jù)的存儲抽象出來,成為一個接口憋飞,針對這個接口進行編程霎苗,這樣就無需面對頻繁變化的實現(xiàn)細節(jié)。

抽象接口不應(yīng)該依賴于具體實現(xiàn)榛做。而具體實現(xiàn)則應(yīng)該依賴于抽象接口:在一開始做設(shè)計的時候唁盏,我們不要去考慮具體實現(xiàn),而應(yīng)該根據(jù)業(yè)務(wù)需求去設(shè)計接口检眯,例如上面的例子厘擂,我們一開始就已經(jīng)考慮到了用文件來存儲了,而且架構(gòu)也是基于此來進行設(shè)計的轰传,這就從一開始是高層次的類依賴于具體實現(xiàn)了驴党。然而實際上我們需要的功能是:數(shù)據(jù)存儲,把數(shù)據(jù)存儲到某個地方获茬,至于具體怎么存儲我們其實并不需要關(guān)心港庄,只需要知道能存即可倔既,所以我們一開始設(shè)計的時候就不應(yīng)該針對文件存儲來進行編程,而應(yīng)該針對抽象的存儲接口來編程鹏氧。同時具體實現(xiàn)也依據(jù)接口來進行編程渤涌。

因此我們優(yōu)化后的架構(gòu)如下:

2

此時類 "Cache" 既沒有依賴 "FileWriter" 也沒有依賴 "RedisWriter",而是依賴于接口"Writer"把还,同時"FileWriter" 和 "RedisWriter" 的具體實現(xiàn)也依賴于抽象实蓬。

<?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"安皱。在不同的環(huán)境條件下,我們只需要修改生成的"writer"類即可艇炎,"set"方法里面的邏輯完全不需要改動酌伊,因為這里面是針對抽象接口"Writer"編程,只要"Writer"沒有變缀踪,"set"方法也不需要做任何修改居砖。

使用場景

程序中所有的依賴關(guān)系都應(yīng)該終止于抽象類或者接口中,而不應(yīng)該依賴于具體類驴娃。
根據(jù)這個啟發(fā)式規(guī)則奏候,編程時可以這樣做:

  • 類中的所有成員變量必須是接口或抽象,不應(yīng)該持有一個指向具體類的引用或指針唇敞。
  • 任何類都不應(yīng)該從具體類派生蔗草,而應(yīng)該繼承抽象類,或者實現(xiàn)接口厚棵。
  • 任何方法都不應(yīng)該覆寫它的任何基類中已經(jīng)實現(xiàn)的方法蕉世。(里氏替換原則)
  • 任何變量實例化都需要實現(xiàn)創(chuàng)建模式(如:工廠方法/模式),或使用依賴注入框架(如:Spring IOC)婆硬。

優(yōu)點

  • 高層模塊和低層模塊徹底解耦狠轻,都很容易實現(xiàn)擴展
  • 抽象模塊具有很高的穩(wěn)定性、可重用性彬犯,對高/低層模塊來說才是真正"可依賴的"向楼。

缺點

  • 增加了一層抽象層,增加實現(xiàn)難度谐区;
  • 對一些簡單的調(diào)用關(guān)系來說湖蜕,可能是得不償失的。
  • 對一些穩(wěn)定的調(diào)用關(guān)系宋列,反而增加復(fù)雜度昭抒,是不正確的。

控制反轉(zhuǎn)

說完依賴倒置,我們再來說一個很相似的設(shè)計原則:控制反轉(zhuǎn)灭返。

看看上面的代碼盗迟,雖然"set"方法里的邏輯不會發(fā)現(xiàn)變化了,但是在構(gòu)造函數(shù)里還是要根據(jù)不同環(huán)境來生成對應(yīng)的"Writer"熙含,還是需要修改代碼罚缕,為了解決這個問題,我們引入控制反轉(zhuǎn)的原則怎静。

定義

控制反轉(zhuǎn)(Inversion of Control邮弹,縮寫為IoC ),是面向?qū)ο缶幊讨械囊环N設(shè)計原則蚓聘,可以用來減低計算機代碼之間的耦合度腌乡。其中最常見的方式叫做依賴注入(Dependency Injection,簡稱DI)或粮,還有一種方式叫“依賴查找”(Dependency Lookup)导饲。通過控制反轉(zhuǎn)捞高,對象在被創(chuàng)建的時候氯材,由一個調(diào)控系統(tǒng)內(nèi)所有對象的外界實體,將其所依賴的對象的引用傳遞給它硝岗。也可以說氢哮,依賴被注入到對象中。

控制反轉(zhuǎn)針對的是依賴對象的獲得方式型檀,也既依賴對象不在是自己內(nèi)部生成冗尤,而是由外界生成后傳遞進來。如下代碼:

<?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"把內(nèi)部依賴"Writer"的創(chuàng)建權(quán)力移交給了上層模塊胀溺,自己只關(guān)心依賴提供的功能裂七,但并不關(guān)心依賴的創(chuàng)建。IoC 是一種新的設(shè)計模式仓坞,它對上層模塊與底層模塊進行了更進一步的解耦背零。

實現(xiàn)方式

實現(xiàn)控制反轉(zhuǎn)主要有兩種方式:依賴注入和依賴查找。兩者的區(qū)別在于无埃,前者是被動的接收對象徙瓶,在類A的實例創(chuàng)建過程中即創(chuàng)建了依賴的B對象,通過類型或名稱來判斷將不同的對象注入到不同的屬性中嫉称,而后者是主動索取相應(yīng)類型的對象侦镇,獲得依賴對象的時間也可以在代碼中自由控制。

依賴注入

依賴注入有如下實現(xiàn)方式:

  • 基于構(gòu)造函數(shù)织阅。實現(xiàn)特定參數(shù)的構(gòu)造函數(shù)壳繁,在新建對象時傳入所依賴類型的對象。
  class Cache{
      protected Writer $writer;

      public function __contruct(Writer $writer){
          $this->wirter=$writer
      }
  }
  • 基于 set 方法。實現(xiàn)特定屬性的public set方法闹炉,來讓外部容器調(diào)用傳入所依賴類型的對象伍派。
  class Cache{
      protected Writer $writer;

      public function setWriter(Writer $writer){
          $this->wirter=$writer
      }
  }
  • 基于接口。實現(xiàn)特定接口以供外部容器注入所依賴類型的對象剩胁。
  interface WriterSetter {
       public function setWriter(Writer $writer);
  }
  class Cache implement WriterSetter{
      protected Writer $writer;
      
      @Override
      public function setWriter(Writer $writer){
          $this->wirter=$writer
      }
  }

接口注入和setter方法注入類似诉植,不同的是接口注入使用了統(tǒng)一的方法來完成注入,而setter方法注入的方法名稱相對比較隨意昵观,接口的存在晾腔,表明了一種依賴配置的能力。

在軟件框架中啊犬,讀取配置文件灼擂,然后根據(jù)配置信息,框架動態(tài)將一些依賴配置給特定接口的類觉至,我們也可以說 Injector 也依賴于接口剔应,而不是特定的實現(xiàn)類,這樣進一步提高了準確性與靈活性语御。

依賴查找

依賴查找相比于依賴注入更加主動峻贮,先配置好對象的生成規(guī)則,然后在需要的地方通過主動調(diào)用框架提供的方法应闯,根據(jù)相關(guān)的配置文件路徑纤控、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 !

如果覺得文章對你有用碉纺,可以贊助我喝杯咖啡~

版權(quán)聲明

轉(zhuǎn)載請注明作者和文章出處
作者: 小魚兒
首發(fā)于 https://blog.ricoo.top/yi-lai-dao-zhi-he-kong-zhi-fan-zhuan/

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末船万,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子骨田,更是在濱河造成了極大的恐慌耿导,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件态贤,死亡現(xiàn)場離奇詭異舱呻,居然都是意外死亡,警方通過查閱死者的電腦和手機抵卫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門狮荔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人介粘,你說我怎么就攤上這事殖氏。” “怎么了姻采?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵雅采,是天一觀的道長。 經(jīng)常有香客問我,道長婚瓜,這世上最難降的妖魔是什么宝鼓? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮巴刻,結(jié)果婚禮上愚铡,老公的妹妹穿的比我還像新娘。我一直安慰自己胡陪,他們只是感情好沥寥,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著柠座,像睡著了一般邑雅。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上妈经,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天淮野,我揣著相機與錄音,去河邊找鬼吹泡。 笑死骤星,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的荞胡。 我是一名探鬼主播妈踊,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼泪漂!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起歪泳,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤萝勤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后呐伞,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體敌卓,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年伶氢,在試婚紗的時候發(fā)現(xiàn)自己被綠了趟径。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡癣防,死狀恐怖蜗巧,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蕾盯,我是刑警寧澤幕屹,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響望拖,放射性物質(zhì)發(fā)生泄漏渺尘。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一说敏、第九天 我趴在偏房一處隱蔽的房頂上張望鸥跟。 院中可真熱鬧,春花似錦盔沫、人聲如沸锌雀。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽腋逆。三九已至,卻和暖如春侈贷,著一層夾襖步出監(jiān)牢的瞬間惩歉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工俏蛮, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留撑蚌,地道東北人。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓搏屑,卻偏偏與公主長得像争涌,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子辣恋,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容

  • 原文傳送門 翻譯方式: 中英文對照亮垫, 意譯 In this article we will talk about ...
    蕭哈哈閱讀 554評論 0 3
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)伟骨,斷路器饮潦,智...
    卡卡羅2017閱讀 134,651評論 18 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,071評論 25 707
  • 本文首發(fā)公眾號“閑情筆譚” 誰的職場生涯中不會遇見一兩個讓人頭疼的后輩呢?對于年輕人携狭,誰能為你的成長買單呢继蜡,應(yīng)該只...
    月尾紫閱讀 492評論 0 1
  • 我喜歡你認真的模樣,可是看到你眼底流露的憂傷逛腿,我會心疼稀并。 這一路,我想陪著你走单默。 這一次碘举,我絕不放開你。 沒有什么...
    梅花樹下梨花白閱讀 155評論 0 0