今天要講的是一個老到不能再老的老題目:『依賴注入』。依賴注入,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!!!");
}
}
簡單,對吧!那麼你思考以下兩個問題:
這時你怎麼辦?啊,簡單,你會這樣做:
『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