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

語言: 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. 參考資料