依賴倒置和控制反轉

依賴倒置

定義

依賴反轉原則(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/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章