控制反轉,依賴注入,依賴倒置傻傻分不清楚?

語言: CN / TW / HK

通過這篇文章,你將瞭解到

  • 控制反轉(IoC)是什麼?「反轉」到底反轉了什麼?
  • Spring和IOC之間是什麼關係?
  • 依賴注入(DI)和依賴倒置原則(DIP)又是什麼?
  • IOC、DI和DIP有什麼關係?

1. 控制反轉(IoC)

1.1 一個典型案例

介紹「控制反轉」之前,我們先看一段代碼

public class UserServiceTest {
    public static boolean doTest() {
        //此處編寫自己的判斷邏輯
        return false;
    }

    public static void main(String[] args) {

        if (doTest()) {
            System.out.println("Test succeed.");
        } else {
            System.out.println("Test failed.");
        }
    }
}

如上,我們為一個方法寫了一個測試用例,包括main方法的創建,所有的流程都是我們自己來控制的。

現在有這麼一個框架,代碼如下:

public abstract class TestCase {
    public void run() {
        if (doTest()) {
            System.out.println("Test succeed.");
        } else {
            System.out.println("Test failed.");
        }
    }

    public abstract boolean doTest();
}


public class JunitApplication {
    private static final List<TestCase> cases = new ArrayList();

    public static void register(TestCase testCase){
        cases.add(testCase);
    }

    public static void main(String[] args) {
        for(TestCase testCase : cases){
            testCase.run();
        }
    }
}

利用這麼框架,我們如果再為UserServiceTest寫一個測試用例,只需要繼承TestCase,並重寫其中的doTest方法即可。

public class UserServiceTestCase extends TestCase{
    @Override
    public boolean doTest() {
        //此處編寫自己的判斷邏輯
        return false;
    }
    
}

//註冊測試用例
JunitApplication.register();

看完這裏例子,相信讀者朋友已經明白了這個框架給我們帶來了怎樣的便利。一開始我們需要為每個測試方法添加一個main方法,一旦待測試的方法多起來會非常的不方便。現在框架給我們制定了程序運行的基本骨架,併為我們預設了埋點,我們只需要設置好框架的埋點,剩下的執行流程就交給框架來完成就可以了。

這就是「框架實現控制反轉」的典型例子。這裏的「控制」指的是對執行流程的控制,「反轉」指的是在框架產生之前我們需要手動控制全部流程的執行,而框架產生之後,有框架來執行整個大流程的執行,流程控制由我們「反轉」給了框架。

1.2 IoC概念的提出

早在1988年,Ralph E. Johnson與Brian Foote在文章Designing Reusable Classes中提出了inversion of control的概念,他們怎麼也沒想到,這幾個單詞會在未來給中國的編程者造成多大的麻煩!

image.png

雖然Spring框架把IoC的概念發揚光大,但IoC的誕生遠遠早於Spring,並且IoC的概念正是在討論框架設計的時候被提出來的。至於框架和IoC是先有雞還是先有蛋,這個問題對我們並沒有什麼意義。​

當IoC概念模糊不清的時候,追本溯源或許是讓我們徹底理解這個概念的好想法。至於概念之外的延伸不過是細枝末節罷了。接下來我們體會一下文章中比較重要兩段話,我進行了意譯。

One important characteristic of a framework is that the methods defined by the user to tailor the framework will often be called from within the framework itself, rather than from the user's application code.

「框架」的一個重要特徵是,框架本身定義的方法常常由框架自己調用,而非用户的應用程序代碼調用。

This inversion of control gives frameworks the power to serve as extensible skeletons. The methods supplied by the user tailor the generic algorithms defined in the framework for a particular application. ​這種「控制反轉」使框架作為一個程序運行的骨架,具有了可擴展的能力。用户可以自定義框架中預設好的埋點。

IoC就是一種思想,而不是某種具體編程技術的落地。應用了「控制反轉」思想的框架允許用户在一定程度上「填空」即可,其餘的運行都交給框架。

1.3 為什麼提出IoC

幾乎所有編程思想的提出都是基於一個目的——解耦。Ioc是怎麼解決耦合問題的呢?

假設我們有四個對象,彼此之間的依賴關係如圖 image.png 翻譯成代碼大致如下:

class A{
    Object b = new B();
    ...
}

class B{
    Object c = new C();
    Object d = new D();
    ...
}

class C{
    Object d = new D();
}

但是A對象就是實實在在地需要B對象啊,這種依賴關係無法被抹除,就意味着耦合關係不可能完全解除,但是可以減弱!IoC的思想是引入一個IoC容器來處理對象之間的依賴關係,由主動依賴轉為被動依賴,減輕耦合關係,從強耦合變為弱耦合。 image.png 關於IoC容器的作用,給大家舉個生活中的例子。

假設有3個顧客分別從4個店鋪購買了商品,好巧不巧,所有人都碰到了質量問題,在第三方購物平台誕生之前,每個顧客都只能分別與每家店鋪協商理賠問題,此時顧客和店鋪之間是強耦合關係。 image.png 有了第三方購物平台之後,顧客可以直接和平台投訴,讓平台和各個店鋪進行協商,平台對每位顧客進行統一理賠,此時顧客和店鋪之間就是鬆耦合的關係,因為最累的工作被平台承擔了,此時平台的作用就類似IoC容器。 image.png 最後拿Spring再舉個例子。

從大粒度上看,使用Spring之後我們不需要再寫Servlet,其中調用Servlet的流程全部交給Spring處理,這是IoC。

從小粒度上看,在Spring中我們可以用以下兩種方式創建對象

// 方式1
private MySQLDao dao = new MySQLDaoImpl();

// 方式2
private MySQLDao dao = (MySQLDao) BeanFactory.getBean("mySQLDao");

使用方式1,dao對象的調用者和dao對象之間就是強耦合關係,一旦MySQLDaoImpl源碼丟失,整個項目就會在編譯時期報錯。

使用方式2,如果我們在xml文件中配置了mySQLDao這個bean,如果源碼丟失,最多報一個運行時異常(ClassNotFound錯誤),不至於影響項目的啟動。

Spring提供了方式2這樣的方式,自動給你查找對象,這也是IoC,而且這是IoC的常用實現方法之一,依賴查找。另一種是依賴注入,我們一會兒再介紹。

1.4 Spring和IoC的關係

Spring是將IoC思想落地的框架之一,並將之發揚光大的最著名的框架(沒有之一)。

1.5 面試中被問到IoC怎麼回答

「控制反轉」是應用於軟件工程領域的,在運行時被裝配器對象用來綁定耦合對象的一種編程思想,對象之間的耦合關係在編譯時通常是未知的。

在傳統的編程方式中,業務邏輯的流程是由應用程序中早已設定好關聯關係的對象來決定的。在使用「控制反轉」的情況下,業務邏輯的流程是由對象關係圖來決定的,該對象關係圖由IoC容器來負責實例化,這種實現方式還可以將對象之間關聯關係的定義抽象化。綁定的過程是由“依賴注入”實現的。

控制反轉是一種以給予應用程序中目標組件更多控制為目的的設計範式,並在實際工作中起到了有效作用。

2. 依賴注入(DI)

依賴注入的英文翻譯是 Dependency Injection,縮寫為 DI。

依賴注入不等於控制反轉!依賴注入只是實現控制反轉的一種方式! 依賴注入不等於控制反轉!依賴注入只是實現控制反轉的一種方式! 依賴注入不等於控制反轉!依賴注入只是實現控制反轉的一種方式!

這個概念披着“高大上”的外衣,但是實質卻非常單純。用人話解釋就是:不通過new() 的方式在類內部創建依賴類對象,而是將依賴的類對象在外部創建好之後,通過構造函數、函數參數等方式傳遞(或注入)給類使用。

舉一個平時編碼常用的一個例子,我們在Controller中調用Service服務的時候一般會這麼寫

@Api(tags = {"報警聯繫人接口"})
@RestController
@RequestMapping("/iot/contact")
public class AlarmContactController extends BaseController {
    
    // 這就是大名鼎鼎的DI啊,是不是非常簡單!
    @Autowired
    private IAlarmContactService alarmContactService;

    ...

}

這就是大名鼎鼎的DI啊,是不是非常簡單!

2.1 面試中被問到「依賴注入」怎麼回答

依賴注入是在編譯階段尚不知道所需功能是來自哪個類的情況下,將其他對象所依賴的功能對象實例化的手段。有三種實現方式:構造器注入、setter方法注入、接口注入。

3. 依賴倒置原則(DIP)

3.1 定義

「依賴倒置」原則的英文翻譯是 Dependency Inversion Principle,縮寫為 DIP。中文翻譯有時候也叫「依賴反轉」原則。

「依賴倒置」是本文要講述的主要內容,是七大設計原則之二,在生產實際中應用的非常廣泛,主要內容為

  1. 高層模塊(high-level modules)不要直接依賴低層模塊(low-level);
  2. 高層模塊和低層模塊應該通過抽象(abstractions)來互相依賴
  3. 抽象(abstractions)不要依賴具體實現細節(details),具體實現細節(details)依賴抽象(abstractions)。

暫時看不懂沒關係,我們先看個代碼案例。

3.2 代碼示例

陀螺研發了一套自動駕駛系統,在積極談判之下和本田以及福特達成了合作協議,兩個廠商各自提供汽車啟動、轉彎和停止的api供自動駕駛調用,系統就能實現自動駕駛,代碼如下

/**
 * @author 公眾號【蟬沐風】
 * @desc 福特汽車廠商提供的接口
 */
public class FordCar{
    public void run(){
        System.out.println("福特開始啟動了");
    }

    public void turn(){
        System.out.println("福特開始轉彎了");
    }

    public void stop(){
        System.out.println("福特開始停車了");
    }
}

/**
 * @author 公眾號【蟬沐風】
 * @desc 本田汽車廠商提供的接口
 */
public class HondaCar {
    public void run() {
        System.out.println("本田開始啟動了");
    }

    public void turn() {
        System.out.println("本田開始轉彎了");
    }

    public void stop() {
        System.out.println("本田開始停車了");
    }
}

/**
 * @author 公眾號【蟬沐風】
 * @desc 自動駕駛系統
 */
public class AutoDriver {
    public enum CarType {
        Ford, Honda
    }

    private CarType type;
    
    private HondaCar hcar = new HondaCar();
    private FordCar fcar = new FordCar();

    public AutoDriver(CarType type) {
        this.type = type;
    }

    public void runCar() {
        if (type == CarType.Ford) {
            fcar.run();
        } else {
            hcar.run();
        }
    }

    public void turnCar() {
        if (type == CarType.Ford) {
            fcar.turn();
        } else {
            hcar.turn();
        }
    }

    public void stopCar() {
        if (type == CarType.Ford) {
            fcar.stop();
        } else {
            hcar.stop();
        }
    }

}

自動駕駛系統運轉良好,很快,奧迪和奔馳以及寶馬紛紛找到陀螺尋求合作,陀螺不得不把代碼改成這個樣子。

/**
 * @author 公眾號【蟬沐風】
 * @desc 自動駕駛系統
 */
public class AutoDriver {
    public enum CarType {
        Ford, Honda, Audi, Benz, Bmw
    }

    private CarType type;

    private HondaCar hcar = new HondaCar();
    private FordCar fcar = new FordCar();
    private AudiCar audicar = new AudiCar();
    private BenzCar benzcar = new BenzCar();
    private BmwCar bmwcar = new BmwCar();

    public AutoDriver(CarType type) {
        this.type = type;
    }

    public void runCar() {
        if (type == CarType.Ford) {
            fcar.run();
        } else if (type == CarType.Honda) {
            hcar.run();
        } else if (type == CarType.Audi) {
            audicar.run();
        } else if (type == CarType.Benz) {
            benzcar.run();
        } else {
            bmwcar.run();
        }
    }

    public void turnCar() {
        if (type == CarType.Ford) {
            fcar.turn();
        } else if (type == CarType.Honda) {
            hcar.turn();
        } else if (type == CarType.Audi) {
            audicar.turn();
        } else if (type == CarType.Benz) {
            benzcar.turn();
        } else {
            bmwcar.turn();
        }
    }

    public void stopCar() {
        if (type == CarType.Ford) {
            fcar.stop();
        } else if (type == CarType.Honda) {
            hcar.stop();
        } else if (type == CarType.Audi) {
            audicar.stop();
        } else if (type == CarType.Benz) {
            benzcar.stop();
        } else {
            bmwcar.stop();
        }
    }

}

如果看過我上一篇開閉原則的文章,你會馬上意識到這段代碼不符合開閉原則。沒錯,一段代碼可能同時不符合多種設計原則,那針對今天的「依賴倒置」原則,這段代碼問題出現在哪裏呢?

我們再來看一下「依賴倒置」原則的要求:

  1. 高層模塊(high-level modules)不要直接依賴低層模塊(low-level);
  2. 高層模塊和低層模塊應該通過抽象(abstractions)來互相依賴
  3. 抽象(abstractions)不要依賴具體實現細節(details),具體實現細節(details)依賴抽象(abstractions)。

針對第1點,高層模塊AutoDriver直接依賴了底層模塊XXCar,體現就是在AutoDriver中直接new了具體的汽車對象。因此也就沒有做到第2點和第3點。UML類圖如下: image.png 那我們就在上層模塊和低層模塊之間加一層抽象吧,定義一個接口ICar,表示抽象的汽車,這樣AutoDriver直接依賴的就是抽象ICar,看代碼:

/**
 * @author 公眾號【蟬沐風】
 * @desc 汽車的抽象接口
 */
public interface ICar {
    void run();
    void turn();
    void stop();
}

public class FordCar implements ICar{
    @Override
    public void run(){
        System.out.println("福特開始啟動了");
    }
    
    @Override
    public void turn(){
        System.out.println("福特開始轉彎了");
    }
    
    @Override
    public void stop(){
        System.out.println("福特開始停車了");
    }
}

public class HondaCar implements ICar{
    @Override
    public void run() {
        System.out.println("本田開始啟動了");
    }

    @Override
    public void turn() {
        System.out.println("本田開始轉彎了");
    }

    @Override
    public void stop() {
        System.out.println("本田開始停車了");
    }
}

public class AudiCar implements ICar{
    @Override
    public void run() {
        System.out.println("奧迪開始啟動了");
    }

    @Override
    public void turn() {
        System.out.println("奧迪開始轉彎了");
    }

    @Override
    public void stop() {
        System.out.println("奧迪開始停車了");
    }
}

public class BenzCar implements ICar{
    @Override
    public void run() {
        System.out.println("奔馳開始啟動了");
    }

    @Override
    public void turn() {
        System.out.println("奔馳開始轉彎了");
    }

    @Override
    public void stop() {
        System.out.println("奔馳開始停車了");
    }
}

public class BmwCar implements ICar {
    @Override
    public void run() {
        System.out.println("寶馬開始啟動了");
    }

    @Override
    public void turn() {
        System.out.println("寶馬開始轉彎了");
    }

    @Override
    public void stop() {
        System.out.println("寶馬開始停車了");
    }
}

/**
 * @author 公眾號【蟬沐風】
 * @desc 自動駕駛系統
 */
public class AutoDriver {

    private ICar car;

    public AutoDriver(ICar car) {
        this.car = car;
    }

    public void runCar() {
        car.run();
    }

    public void turnCar() {
        car.turn();
    }

    public void stopCar() {
        car.stop();
    }

}

重構之後我們發現高層模塊AutoDriver直接依賴於抽象ICar,而不是直接依賴XXXCar,這樣即使有更多的汽車廠家加入合作也不需要修改AutoDriver。這就是高層模塊和低層模塊之間通過抽象進行依賴。

此外,ICar也不依賴於XXXCar,因為ICar是高層模塊定義的抽象,汽車廠家如果想達成合作,就必須遵循AutoDriver定義的標準,即需要實現ICar的接口,這就是第3條所説的具體細節依賴於抽象!

我們看一下重構之後的UML圖

image-20220211082552756

可以看到,原本是AutoDriver直接指向XXXCar,現在是AutoDriver直接指向抽象ICar,而各種XXXCar對象反過來指向ICar,這就是所謂的「依賴倒置(反轉)」。

看到這裏,不知道你是不是對「依賴倒置」原則有了深刻的理解。其實這種中間添加抽象層的思想應用非常廣泛,再舉兩個例子。

3.3 無所不在的抽象

3.3.1 JVM的抽象

JVM雖然被稱為Java虛擬機,但是其底層代碼的運行並不直接依賴於Java語言,而是定義了一個字節碼抽象(行業標準),只要實現字節碼的標準,任何語言都可以運行在JVM之上。

3.3.2 貨幣的誕生

回到物物交換的時代,王二想用自己多餘的雞換一雙草鞋,李四想用自己多餘的草鞋換一條褲子,趙五想用自己多餘的褲子換個帽子。。。如果用物物交換的方式進行下去,這個圈子可就繞到姥姥家了。然後人們就抽象出了中間層——貨幣,貨幣作為購買力的標準使得物物交換變得更加方便。 ​

4. 推薦閲讀

5. 參考資料