控制反轉(zhuǎn)和依賴注入

依賴

控制反轉(zhuǎn)(Inversion of Control)、依賴反轉(zhuǎn)(Dependency Inversion Principle)、依賴注入(Dependency Injection)、控制反轉(zhuǎn)容器(Inversion of Control Container)等等,這些術(shù)語有的描述的是一種思想,有的描述的是一種技術(shù)實(shí)現(xiàn),但是目的都是為了解決依賴關(guān)系而產(chǎn)生的。

什么是依賴?其實(shí)可以理解為產(chǎn)品設(shè)計(jì)之初各個(gè)零部件之間的耦合關(guān)系。
舉個(gè)例子,一臺(tái)筆記本電腦我們可以理解這是一個(gè)完整的產(chǎn)品,它是由cpu、主板、硬盤、內(nèi)存等等零部件構(gòu)成的(這里的主板就相當(dāng)于我們程序設(shè)計(jì)里的主類或者說主方法)。有些主板兼容性強(qiáng),支持各種各樣的協(xié)議接口,因此我們可以在內(nèi)存接口上插DDR3 、DDR4,可以在顯卡接口上接入amd顯卡也可以接入NVIDIA顯卡,可以接入sata硬盤可以接入NGFF硬盤等等。那假如我們拿到了一臺(tái)定制電腦,他的協(xié)議和內(nèi)置規(guī)則限制了各個(gè)接口只能使用指定的零件,例如只能用三星的ddr3內(nèi)存條,只能用日立的sata3硬盤,只能使用amd的顯卡,那是不是立馬就感覺這臺(tái)機(jī)器的升級(jí)擴(kuò)展能力、維護(hù)能力非常差了?

程序上的依賴也是類似的道理,在面向?qū)ο缶幊汤?,我們通常?huì)在一個(gè)類中調(diào)用其他的類及其方法,避免重復(fù)造輪子的問題。但是在早期的編程或編程思路設(shè)計(jì)中,往往會(huì)忽視自己寫出來的類是否具有高擴(kuò)展性高維護(hù)性,使得自己寫出的模塊十分依賴于某一些特定的模塊,即模塊的耦合較高而內(nèi)聚較差。

public class MyServer{
    private _db = new MysqlDB ();
    public void main(){
        _db.dosomething();
    }
    class MysqlDB{
        public void dosomething();
    }
}

例如上面的這段代碼,MyServer需要實(shí)例化一個(gè)MysqlDB 的對(duì)象來進(jìn)行操作,MyServer的實(shí)現(xiàn)依賴于MysqlDB的實(shí)現(xiàn)。這種依賴關(guān)系導(dǎo)致了這個(gè)MyServer無法處理access、sqlserver等其他數(shù)據(jù)庫,只能處理Mysql數(shù)據(jù)庫。這個(gè)簡單的示例程序里面可能只要更換MysqlDB類即可,但是在實(shí)際的項(xiàng)目中,使用這種方式組織代碼會(huì)使得類與類之間形成成一張龐大的依賴關(guān)系網(wǎng),一旦某個(gè)類出現(xiàn)了改動(dòng),那么依賴于它的類也需要進(jìn)行修改,而更上層的各個(gè)類可能也要跟著修改,這是違反開放封閉原則的。

DIP

依賴倒置原則(Dependency Inversion Principle)就是其中的一種解決這種依賴問題的思想。需要注意的是DIP是一種原則或者說是一種思考方向,因此并未提供具體操作方法。

DIP:高層模塊不應(yīng)當(dāng)依賴于低層模塊,高層模塊和低層模塊應(yīng)當(dāng)都同時(shí)依賴于抽象。

我們拿計(jì)算機(jī)舉個(gè)例子:
如果一臺(tái)電腦,其主板設(shè)計(jì)出來只能使用因特爾公司的網(wǎng)卡,維護(hù)性會(huì)很差,這可以大致理解為高層模塊依賴于低層模塊。而如果某個(gè)廠家的內(nèi)存條只能在指定的一些主板上使用,那就使得這個(gè)內(nèi)存的應(yīng)用范圍太狹窄了,即低層模塊太過依賴于高層模塊。而依賴翻轉(zhuǎn)就是說這兩個(gè)不同層級(jí)的模塊都不應(yīng)當(dāng)互相依賴,他們應(yīng)當(dāng)依賴于接口,即內(nèi)存、網(wǎng)卡等等的接口協(xié)議。

放在代碼里,可以理解為高層模塊類和低層模塊類在設(shè)計(jì)的時(shí)候都應(yīng)該基于一個(gè)統(tǒng)一的接口來設(shè)計(jì),即具有的屬性、可調(diào)用的方法等等都是一致的,這樣就在一定程度上既解決了高低層模塊直接的依賴關(guān)系,又兼顧到了模塊的移植擴(kuò)展能力。

至于為什么這個(gè)叫依賴翻轉(zhuǎn),查到的資料是說這種抽象接口是和高層模塊在同一等級(jí)的,因此之前的高層模塊依賴于低層模塊變成了低層模塊依賴于高層接口?;蛘呃斫鉃榻涌趨f(xié)議高于各個(gè)模塊,所以之前的依賴方向就由高層依賴低層變成了低層依賴高層,因此就翻轉(zhuǎn)倒置(Inversion)了。

IOC

控制反轉(zhuǎn)Inversion of Control是通過改變業(yè)務(wù)邏輯流程的控制方,由之前通過模塊內(nèi)部的代碼確定邏輯流程,變更為由客戶在使用時(shí)指定操作邏輯。在代碼中的表現(xiàn),是從一開始由代碼書寫者通過new class確定使用什么模塊、通過方法調(diào)用確定使用什么邏輯流程,轉(zhuǎn)變成在代碼中只聲明具有什么特點(diǎn)能實(shí)現(xiàn)什么功能,而只有在客戶使用時(shí)通過交互事件、修改執(zhí)行配置文件等方式才確定具體的執(zhí)行模塊和執(zhí)行流程。即將控制權(quán)由代碼書寫者交給了客戶、ioc容器或框架。

根據(jù)上面的描述可以知道,在書寫代碼的時(shí)候我們是不會(huì)確定模塊具體new了哪一個(gè)類,調(diào)用了什么方法,因此也就避免了我們寫出來的模塊和其他模塊直接產(chǎn)生依賴關(guān)系,即實(shí)現(xiàn)了解耦。

IOC:代碼本職之外的工作都應(yīng)該由某個(gè)第三方(IOC容器或框架)完成

試想一個(gè)場景,我們在網(wǎng)站登錄賬號(hào)時(shí),在登錄賬號(hào)一欄里可以使用賬號(hào)密碼登錄、手機(jī)驗(yàn)證碼登錄。按照傳統(tǒng)的寫法,我們會(huì)在用戶管理類中創(chuàng)建一個(gè)登錄類,在登錄類中對(duì)登錄方式進(jìn)行判斷,然后再根據(jù)不同方式進(jìn)行登錄認(rèn)證并返回認(rèn)證結(jié)果。如果在以后有了需求變更,需要增加二維碼登錄、qq等第三方登錄,那我們就要回去修改這個(gè)登錄類了,這就違背了開放封閉原則。根據(jù)ioc的建議,我們不應(yīng)當(dāng)在用戶管理類中實(shí)例化具體的登錄類,而應(yīng)當(dāng)在用戶使用時(shí)(不管是手動(dòng)臨時(shí)寫邏輯代碼還是通過交互式點(diǎn)擊或輸入)才具體指定要實(shí)例化的是哪一個(gè)登錄類。

這就是控制反轉(zhuǎn)原則,將模塊的創(chuàng)建、銷毀、調(diào)度等等的控制權(quán)限由代碼書寫者交給使用者,由原本模塊代碼內(nèi)部定義好關(guān)聯(lián)依賴關(guān)系轉(zhuǎn)換成用戶通過交互式操作、自己手動(dòng)書寫新的控制邏輯甚至是交給容器框架托管。這樣使得各個(gè)模塊在使用時(shí)才建立起依賴關(guān)系,避免了依賴問題。

DI

DI是其中依據(jù)IOC原則產(chǎn)生的一種設(shè)計(jì)模式。依賴注入Dependency injection,根據(jù)其字面意思可知模塊直接的關(guān)系是在用戶使用時(shí)注入進(jìn)去的,而不是原始代碼邏輯中就已經(jīng)存在的。

DI:依賴通過“注入”的方式提供給需要的類,是 DIP 和 IoC 的具體實(shí)現(xiàn)

我們以網(wǎng)絡(luò)爬蟲舉個(gè)例子:

//RunSpider.java
public class RunSpider {
    String url,path;
    Spider163 spider163;
    Parse163 parse163;
    Download download;
    public RunSpider(String url,String path){
        spider163 = new Spider163();
        parse163 = new Parse163();
        download = new Download();
    }

    public static void main(String[] args) {
        //具體的循環(huán)爬取、異步協(xié)程管理、解析、下載流程
    }
}

上面的RunSpider 是依據(jù)基本的面向?qū)ο髮懗鰜淼呐老x執(zhí)行類。它在構(gòu)造函數(shù)中創(chuàng)建爬蟲、數(shù)據(jù)解析、數(shù)據(jù)下載這幾個(gè)類的實(shí)例,并在main方法中執(zhí)行具體的邏輯方法實(shí)現(xiàn)循環(huán)爬取、異步爬取、解析數(shù)據(jù)并保存至指定位置的功能。

很明顯地可以看到這種寫法有個(gè)很嚴(yán)重的問題,它嚴(yán)重依賴于這幾個(gè)具體的處理類(假設(shè)這幾個(gè)類都是針對(duì)163網(wǎng)站而寫的),當(dāng)我們需要去爬取其他網(wǎng)站的內(nèi)容時(shí)我們又將需要重新修改代碼或重新創(chuàng)建新的項(xiàng)目。而實(shí)際上爬取邏輯中有大量的重復(fù)操作,只是在不同網(wǎng)頁的解析、反爬處理等上有變化,而主體的請求過程、異步處理等等是一致的。或者說,SpiderRun它只負(fù)責(zé)爬蟲流程管理,他不應(yīng)該關(guān)注爬取的頁面是哪個(gè),應(yīng)該用哪個(gè)對(duì)應(yīng)的解析模塊來解析。

一個(gè)很自然的想法就是利用接口實(shí)現(xiàn)解析、爬取等等模塊的多態(tài)問題:

public class RunSpider {
    String url,path;
    SpiderI spider;
    ParseI parse;
    DownloadI download;
    public RunSpider(SpiderI spiderImpl,
                    ParseI parseImpl,
                    DownloadI downloadImpl,
                    String url,String path){
        spider =  spiderImpl;
        parse = parseImpl;
        download = downloadImpl;
    }

    public static void main(String[] args) {
        //具體的循環(huán)爬取、異步協(xié)程管理、解析、下載流程
    }
}
interface SpiderI(){;}
interface ParseI(){;}
interface DownloadI(){;}

例如上面的代碼,我們在定義RunSpider管理執(zhí)行類的時(shí)候,只定義需要接受相應(yīng)的接口類,表示我們將會(huì)用這一接口規(guī)格的類來進(jìn)行處理。具體的接口實(shí)例化,我們可以通過構(gòu)造函數(shù),也可以通過抽象接口的set方法,總之我們把各個(gè)類的實(shí)例化交給了客戶,而不是在代碼中寫死。

這種在使用時(shí)才賦予模塊依賴關(guān)系的方式,就是依賴注入。一般常見的依賴注入通常包括構(gòu)造函數(shù)注入、使用接口的set方法注入依賴關(guān)系、通過容器等方式管理注入等。

在上面的例子中,我們可以在main方法或其他方法中分別實(shí)現(xiàn)接口并實(shí)例化,然后將對(duì)應(yīng)的實(shí)例化對(duì)象傳入當(dāng)做參數(shù)傳入到RunSpider類的構(gòu)造函數(shù)中,實(shí)現(xiàn)注入。也可以在構(gòu)造方法中不對(duì)這些接口進(jìn)行配置,而是單獨(dú)地通過set方法將實(shí)例化的對(duì)象設(shè)置到各個(gè)接口上實(shí)現(xiàn)依賴注入。

雖說這樣寫避免了模塊定義時(shí)產(chǎn)生依賴問題,但是我們還是需要在使用時(shí)手動(dòng)的修改部分依賴注入的代碼來指定依賴關(guān)系,這是非常不利于維護(hù)和管理的。當(dāng)然也可以自己新建一個(gè)專門用于管理和配置依賴關(guān)系的類,不過還是應(yīng)當(dāng)優(yōu)先選擇使用ioc容器或框架來完成這樣的工作,避免重復(fù)造輪子。

在Python中非常有名的Scrapy爬蟲框架其實(shí)就廣泛使用了類似依賴注入的機(jī)制,用戶通過繼承指定的抽象類生成自己的爬蟲類(Spider)、解析類(Parse)、數(shù)據(jù)類(Items)、流程處理類(Pipeline)、中間件類(XXX-MiddleWare)等等,在配置文件(setting)中配置需要處理的類有哪些,并通過命令行或通過其框架提供的命令類執(zhí)行命令來指定爬蟲入口并啟動(dòng)爬蟲。

同樣比較典型的基于IOC容器的框架就是Spring,Spring中通過對(duì)Bean對(duì)象的處理,把對(duì)象的創(chuàng)建、初始化、銷毀工作交給spring容器,統(tǒng)一管理各個(gè)對(duì)象的生命周期。

Reference


IoC vs. DI by Francisco Alvarez
Inversion of Control 對(duì)IOC、DIP、DI、IOC容器相對(duì)比較完整的講解。
依賴注入那些事兒 通過一個(gè)簡單的例子深入淺出,建議閱讀
DIP vs IoC vs DI 一個(gè)相對(duì)不錯(cuò)的中文版的總結(jié)
Spring Framework Basics - What Is Inversion Of Control?
Spring框架如何加載和定義Spring Bean類?
spring02——IOC(控制反轉(zhuǎn))、依賴注入和依賴查找

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容