iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 13
0
自我挑戰組

再戰軟體工程系列 第 12

『依賴注入(DI)』 -- DI做啥小?你才DI,你全家都DI!

今天要講的是一個老到不能再老的老題目:『依賴注入』。依賴注入,Dependence Injection,也很常被簡寫成DI。很多時候,我們都是在學Spring架構時,才講到DI的。的確,他也是Spring架構設計中非常重要的一環。然而,他卻不一定要用Spring架構才能使用,一般程式裏面也可以被廣泛用到。使用依賴注入,在你程式還很小的時候是看不出來好處的。

喔不,我應該這麼說,如果你不注重軟體工程與程式解耦的話,DI跟你一點關係都沒有。為什麼?因為不影響功能啊!有沒有DI,程式都會跑,功能都會對,只是耦合性高了點,自動測試難寫了點而已呀!與其說破嘴,我們直接來看看例子吧!

我們今天要做一個冷血殺手類別,這個殺手手上有一把手槍作為武器,每當要殺人時,他就會開槍,發出砰然巨響。

聰明如你,光聽完這句話,腦中一定立馬有了基本架構,並且心想:『什麼嘛,簡單!我5分鐘咻咻寫完了,你看!』

// CoolKiller.java
public class CoolKiller {
    Gun weapen;

    public CoolKiller(){
        weapen = new Gun();
    }
    public void kill(){
        weapen.act();
    }
}
// Gun.java
public class Gun {
    public void act(){
        System.out.println("BANG!!!");
    }
}

簡單,對吧!那麼你思考以下兩個問題:

  1. 如果我今天要做一個『搞笑殺手』,手上的武器是喇叭,每次叫他殺人,他就把喇叭拿起來吹,發出『吧』的一聲。
  2. 我同時想聘請另一個冷血殺手,但是這次手上武器變成衝鋒槍,殺人時變成發出『答答答~~~』的聲音。

這時你怎麼辦?啊,簡單,你會這樣做:
『copy冷血殺手過來改一改,另創一個武器叫做喇叭,喇叭的內容也是copy手槍的內容過來改一改,很快!』
『需求二更簡單,我在冷血殺手的constructor開一個boolean接口,true就new一個槍,false就new一個衝鋒槍,改一改,很快!』

你會這樣做嗎?

你會這樣做,代表你總是能夠很快完成客戶要的需求,很棒,但是,你不是一個好的軟體工程師。不只不是,你製造的技術債簡直經典到可以直接拿來當成教材!

不要再跟我說什麼co過來改一改很快之類的話了!我不想聽!!!
醒醒!你是專業工程師,不再是大學生了!!!

上面的問題只有一個因素:對依賴類的強耦合。耦合一強,你的殺手類跟槍類就被綁得死死的了,要增加彈性就只能靠開接口或是複製貼上大法了,然後產生一大堆長得很像的『樣板式程式』。這實在是結構性的問題。我們看看『依賴注入』怎麼解決這個問題吧!(喔對了,說到樣板式程式,我們還有另外一個方法可以解決,不過不在本篇範圍內,下回再說。)

我們首先,看到上面的需求,就要先把『做什麼』跟『什麼時候做』分開來。這麼說吧,這時候我們直接就來重組這個需求,首先我們要有一個殺手,殺手手上必須有一個武器,他在殺人時要觸發武器,武器被觸發時會做一些事情。

於是我們先不要管細節,先把兩大類別的動作給定義起來先。要定義動作,那就使用interface嚕:

// Killer.java
public interface Killer {
    void kill();
}
// Weapon.java
public interface Weapon {
    void act();
}

有了動作不夠,我們還得實際定義動作內容,於是我們實際創立一個冷酷的殺手:

// CoolKiller.java
public class CoolKiller implements Killer{
    Weapon weapon;

    public CoolKiller(Weapon weapon){
        this.weapon = weapon;
    }
    
    public void kill() {
        weapon.act();
    }
}

這裡才是關鍵!我們透過建構子,把原本對槍的強依賴減弱了,現在你只要在主程式裡面創一把槍,喔對了,這個槍必需要實作Weapon喔!就像這樣:

//Gun.java
public class Gun implements Weapon {
    public void act() {
        System.out.println("BANG!!!");
    }
}

你也許會想,『啊!Kuma老師你唬爛,這樣寫程式變超長的耶!哪有比較快!』
我沒騙人啊!我沒有說這樣寫比較快啊!我剛剛說的是,這樣耦合度比較低,比較容易擴展。不信?你試著創一個衝鋒槍,一樣implements Weapon,並且實作act()函式,讓他發出答答答的聲音,接著在new CoolKiller時把他set進去。是不是做完了?

於是你這個CoolKiller不管拿到任何武器,他就可以拿那個武器來執行任務,是不是很方便!

而DI最強的,其實在他的測試。

我們看看這個測試:

public class CoolKillerTest {

    @Test
    public void kill() throws Exception {
        Weapon mockedWeapon = Mockito.mock(Weapon.class);
        CoolKiller coolKiller = new CoolKiller(mockedWeapon);

        coolKiller.kill();
        verify(mockedWeapon, times(1)).act();
    }

}

你可以看到我只驗證一件事,就是這個CoolKiller被呼叫kill()之後,是不是確實把weapon的act()觸發了一次,其他的我都不管。為什麼我敢只驗這件事?很簡單,因為解耦合

邏輯上,我們每次new出一個CoolKiller,只要交給他一把武器就可以了。這把武器是槍,是刀,還是手榴彈,我通通都不管,因為對CoolKiller來說,我只在乎weapon.act()這件事有沒有被執行,至於會造成什麼影響,那是weapon該負責的事,我要連這都要測,那就超越我的測試範圍了。因為我只管邏輯,邏輯對,程式就會對。

於是,我們用了一個實際的例子,示範了依賴注入怎麼幫我們同時降低耦合,提高延展性,以及降低測試難度,即便是在Spring框架之外。

其實,依賴注入也不是萬靈丹,就像其他解耦合技巧一樣,他只是適合特定的狀況。依賴注入要用得好,我個人的經驗是,設計模式要挑得好。至於設計模式要怎麼挑,那就有賴多練習與多解題了。

本文範例程式碼在此:https://github.com/bearhsu2/ithelp


上一篇
『你是在回顧,還是在驗屍?』 -- 談回顧會議的產出
下一篇
『有點像又不會太一樣』 -- 慎選設計模式 之 模板模式
系列文
再戰軟體工程30

尚未有邦友留言

立即登入留言