依赖倒置和控制反转
依賴倒置
定義
依賴反轉(zhuǎn)原則(Dependency inversion principle,DIP)是指一種特定的解耦形式,使得高層次的類不依賴于低層次的類的實(shí)現(xiàn)細(xì)節(jié),依賴關(guān)系被顛倒(反轉(zhuǎn)),從而使得低層次類依賴于高層次類的需求抽象。
該原則規(guī)定:
在傳統(tǒng)的應(yīng)用架構(gòu)中,低層次的組件設(shè)計(jì)用于被高層次的組件使用,這一點(diǎn)提供了逐步的構(gòu)建一個(gè)復(fù)雜系統(tǒng)的可能。在這種結(jié)構(gòu)下,高層次的組件直接依賴于低層次的組件去實(shí)現(xiàn)一些任務(wù)。這種對(duì)于低層次組件的依賴限制了高層次組件被重用的可行性。
依賴反轉(zhuǎn)原則的目的是把高層次組件從對(duì)低層次組件的依賴中解耦出來,這樣使得重用不同層級(jí)的組件實(shí)現(xiàn)變得可能。
聽起來很干,我們先通過實(shí)現(xiàn)一個(gè)簡(jiǎn)單的需求來描述依賴倒置原則要解決的問題。
需求
假設(shè)我們要實(shí)現(xiàn)一個(gè)簡(jiǎn)單的緩存功能,主要負(fù)責(zé)把數(shù)據(jù)存儲(chǔ)起來方便以后調(diào)用。
為了快速完成需求,我們決定采用最簡(jiǎn)單的實(shí)現(xiàn)方式:存儲(chǔ)到文件,所以我們?cè)O(shè)計(jì)其結(jié)構(gòu)如下:
圖 1 顯示在應(yīng)用程序中一共有兩個(gè)類。“Cache” 類負(fù)責(zé)調(diào)用"FileWrite"類來寫入文件。代碼實(shí)現(xiàn)如下:
<?phpclass 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);} }在所有使用文件來作為存儲(chǔ)方式的系統(tǒng)中,上面的"Cache"類能很好的使用,然而在以其他方式來存儲(chǔ)的系統(tǒng)中,“Cache” 類是無法被重用的。
例如,假設(shè)我們的系統(tǒng)是分布式服務(wù),部署在多臺(tái)機(jī)器上,這時(shí)候如果使用文件存儲(chǔ)則緩存只能生效于本機(jī)器上,無法被其他機(jī)器使用,故我們無法使用文件來作為緩存的存儲(chǔ)方式,所以我們引入一個(gè)新的存儲(chǔ)方式:redis。另外我們也希望復(fù)用 “Cache” 類,但很不幸的是, “Cache” 類是直接依賴于 “FileWrite” 類的,無法直接被重用。
為了解決這個(gè)問題,我們需要修改下代碼,如下:
<?phpclass 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);}} }可以看到我們需要引入一個(gè)新的類,而且需要去修改"Cache"類的代碼。隨著需求的變化,我們可能有要支持其他的存儲(chǔ)方式,例如數(shù)據(jù)庫,memcached,這時(shí)候我們就需要不斷添加新的類,不斷修改"Cache"類,而且把"Cache"淹沒在凌亂的"if/else"判斷中,這樣的設(shè)計(jì)的維護(hù)和拓展成本簡(jiǎn)直不可想象。
出現(xiàn)這些問題的原因就在于類間的相互依賴,主要特征是包含高層邏輯的類依賴于低層類的細(xì)節(jié):"Cache"類的"set"功能完全依賴于下面的"FileWriter"的具體實(shí)現(xiàn)細(xì)節(jié),導(dǎo)致在使用環(huán)境發(fā)生變化的時(shí)候,"Cache"類無法復(fù)用。依賴倒置原則就是為了來解決這個(gè)問題的。
問題
- 沒有抽象,耦合度高:當(dāng)?shù)蛯幽K變動(dòng)時(shí),高層模塊也得變動(dòng);
- 高層模塊過度依賴低層模塊,很難擴(kuò)展。
- 這種依賴關(guān)系具有傳遞性,即如果是多層次的調(diào)用,最低層改動(dòng)會(huì)影響較高層……直到最高層。
解決
高層次的類不應(yīng)該依賴于低層次的類,兩者都應(yīng)該依賴于抽象接口:例如 “Cache” 類依賴于 “FileWriter” 類的實(shí)現(xiàn),所以才會(huì)無法適用使用環(huán)境的變化。所以我們要想辦法使 “Cache” 類不依賴于這些細(xì)節(jié),因?yàn)榫唧w實(shí)現(xiàn)是不斷變化的,而抽象接口是相對(duì)穩(wěn)定的,所以我們要把數(shù)據(jù)的存儲(chǔ)抽象出來,成為一個(gè)接口,針對(duì)這個(gè)接口進(jìn)行編程,這樣就無需面對(duì)頻繁變化的實(shí)現(xiàn)細(xì)節(jié)。
抽象接口不應(yīng)該依賴于具體實(shí)現(xiàn)。而具體實(shí)現(xiàn)則應(yīng)該依賴于抽象接口:在一開始做設(shè)計(jì)的時(shí)候,我們不要去考慮具體實(shí)現(xiàn),而應(yīng)該根據(jù)業(yè)務(wù)需求去設(shè)計(jì)接口,例如上面的例子,我們一開始就已經(jīng)考慮到了用文件來存儲(chǔ)了,而且架構(gòu)也是基于此來進(jìn)行設(shè)計(jì)的,這就從一開始是高層次的類依賴于具體實(shí)現(xiàn)了。然而實(shí)際上我們需要的功能是:數(shù)據(jù)存儲(chǔ),把數(shù)據(jù)存儲(chǔ)到某個(gè)地方,至于具體怎么存儲(chǔ)我們其實(shí)并不需要關(guān)心,只需要知道能存即可,所以我們一開始設(shè)計(jì)的時(shí)候就不應(yīng)該針對(duì)文件存儲(chǔ)來進(jìn)行編程,而應(yīng)該針對(duì)抽象的存儲(chǔ)接口來編程。同時(shí)具體實(shí)現(xiàn)也依據(jù)接口來進(jìn)行編程。
因此我們優(yōu)化后的架構(gòu)如下:
此時(shí)類 “Cache” 既沒有依賴 “FileWriter” 也沒有依賴 “RedisWriter”,而是依賴于接口"Writer",同時(shí)"FileWriter" 和 “RedisWriter” 的具體實(shí)現(xiàn)也依賴于抽象。
<?phpinterface 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);} }此時(shí),我們就可以重用 “Cache” 類,而不需要具體的"Writer"。在不同的環(huán)境條件下,我們只需要修改生成的"writer"類即可,"set"方法里面的邏輯完全不需要改動(dòng),因?yàn)檫@里面是針對(duì)抽象接口"Writer"編程,只要"Writer"沒有變,"set"方法也不需要做任何修改。
使用場(chǎng)景
程序中所有的依賴關(guān)系都應(yīng)該終止于抽象類或者接口中,而不應(yīng)該依賴于具體類。
根據(jù)這個(gè)啟發(fā)式規(guī)則,編程時(shí)可以這樣做:
- 類中的所有成員變量必須是接口或抽象,不應(yīng)該持有一個(gè)指向具體類的引用或指針。
- 任何類都不應(yīng)該從具體類派生,而應(yīng)該繼承抽象類,或者實(shí)現(xiàn)接口。
- 任何方法都不應(yīng)該覆寫它的任何基類中已經(jīng)實(shí)現(xiàn)的方法。(里氏替換原則)
- 任何變量實(shí)例化都需要實(shí)現(xiàn)創(chuàng)建模式(如:工廠方法/模式),或使用依賴注入框架(如:Spring IOC)。
優(yōu)點(diǎn)
- 高層模塊和低層模塊徹底解耦,都很容易實(shí)現(xiàn)擴(kuò)展
- 抽象模塊具有很高的穩(wěn)定性、可重用性,對(duì)高/低層模塊來說才是真正"可依賴的"。
缺點(diǎn)
- 增加了一層抽象層,增加實(shí)現(xiàn)難度;
- 對(duì)一些簡(jiǎn)單的調(diào)用關(guān)系來說,可能是得不償失的。
- 對(duì)一些穩(wěn)定的調(diào)用關(guān)系,反而增加復(fù)雜度,是不正確的。
控制反轉(zhuǎn)
說完依賴倒置,我們?cè)賮碚f一個(gè)很相似的設(shè)計(jì)原則:控制反轉(zhuǎn)。
看看上面的代碼,雖然"set"方法里的邏輯不會(huì)發(fā)現(xiàn)變化了,但是在構(gòu)造函數(shù)里還是要根據(jù)不同環(huán)境來生成對(duì)應(yīng)的"Writer",還是需要修改代碼,為了解決這個(gè)問題,我們引入控制反轉(zhuǎn)的原則。
定義
控制反轉(zhuǎn)(Inversion of Control,縮寫為IoC ),是面向?qū)ο缶幊讨械囊环N設(shè)計(jì)原則,可以用來減低計(jì)算機(jī)代碼之間的耦合度。其中最常見的方式叫做依賴注入(Dependency Injection,簡(jiǎn)稱DI),還有一種方式叫“依賴查找”(Dependency Lookup)。通過控制反轉(zhuǎn),對(duì)象在被創(chuàng)建的時(shí)候,由一個(gè)調(diào)控系統(tǒng)內(nèi)所有對(duì)象的外界實(shí)體,將其所依賴的對(duì)象的引用傳遞給它。也可以說,依賴被注入到對(duì)象中。
控制反轉(zhuǎn)針對(duì)的是依賴對(duì)象的獲得方式,也既依賴對(duì)象不在是自己內(nèi)部生成,而是由外界生成后傳遞進(jìn)來。如下代碼:
<?phpinterface 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è)計(jì)模式,它對(duì)上層模塊與底層模塊進(jìn)行了更進(jìn)一步的解耦。
實(shí)現(xiàn)方式
實(shí)現(xiàn)控制反轉(zhuǎn)主要有兩種方式:依賴注入和依賴查找。兩者的區(qū)別在于,前者是被動(dòng)的接收對(duì)象,在類A的實(shí)例創(chuàng)建過程中即創(chuàng)建了依賴的B對(duì)象,通過類型或名稱來判斷將不同的對(duì)象注入到不同的屬性中,而后者是主動(dòng)索取相應(yīng)類型的對(duì)象,獲得依賴對(duì)象的時(shí)間也可以在代碼中自由控制。
依賴注入
依賴注入有如下實(shí)現(xiàn)方式:
- 基于構(gòu)造函數(shù)。實(shí)現(xiàn)特定參數(shù)的構(gòu)造函數(shù),在新建對(duì)象時(shí)傳入所依賴類型的對(duì)象。
- 基于 set 方法。實(shí)現(xiàn)特定屬性的public set方法,來讓外部容器調(diào)用傳入所依賴類型的對(duì)象。
- 基于接口。實(shí)現(xiàn)特定接口以供外部容器注入所依賴類型的對(duì)象。
接口注入和setter方法注入類似,不同的是接口注入使用了統(tǒng)一的方法來完成注入,而setter方法注入的方法名稱相對(duì)比較隨意,接口的存在,表明了一種依賴配置的能力。
在軟件框架中,讀取配置文件,然后根據(jù)配置信息,框架動(dòng)態(tài)將一些依賴配置給特定接口的類,我們也可以說 Injector 也依賴于接口,而不是特定的實(shí)現(xiàn)類,這樣進(jìn)一步提高了準(zhǔn)確性與靈活性。
依賴查找
依賴查找相比于依賴注入更加主動(dòng),先配置好對(duì)象的生成規(guī)則,然后在需要的地方通過主動(dòng)調(diào)用框架提供的方法,根據(jù)相關(guān)的配置文件路徑、key等信息來獲取對(duì)象。
例如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 !
如果覺得文章對(duì)你有用,可以贊助我喝杯咖啡~
版權(quán)聲明
轉(zhuǎn)載請(qǐng)注明作者和文章出處
作者: X先生
https://blog.csdn.net/u013314679/article/details/105655583
總結(jié)
- 上一篇: 志宇-计算机网络
- 下一篇: 6. GDAL进行栅格转矢量