2009年6月28日 星期日

Dependency Injection

Dependency Injection(依賴注入,簡稱DI)是物件導向技術中常被用來降低模組間耦合度的做法,Martin Fowler首先在他的Inversion of Control Containers and the Dependency Injection pattern一文中使用了這個詞,並定義了三種型式的DI,分別是type 1:Interface Injection、type 2:Setter Injection及type 3:Constructor Injection,後來又由picocontainer定義了更多種類的DI。那些DI,其實我也不知道詳細情形為何,也懶得知道。無論如何,DI的用途就是「將模組之間的相依性從程式實作中抽離」。這樣說或許很抽象,但我們生活中其實充滿DI的影子。例如,每個MP3 Player一定需要電池才能運作,有些MP3的電池是內建的,無法更換;有些使用3號或4號鹼性電池,替換很方便。使用內建電池的MP3 Player,由於電池相依於MP3 Player的內部實作,如果之後電池壞掉,就只能買一台新的。而使用標準電池的MP3 Player,即使電池壞掉,只要到7-11買一副新的電池,馬上就能夠使用。同樣的道理也適用於軟體設計:萬一某天發現軟體中的一個類別有bug,究竟是要整個軟體都改過,還是只需要改有bug的類別?這樣我們就能歸納出一個結論:類別和MP3Player的電池一樣,最好能夠想換就換。廢話不多說,我們直接寫個Java MP3Player程式來看看。
class MP3Player{
private NormalBattery battery = new NormalBattery();
public void play(){
battery.usePower();
}
public int getBatteryPower(){
return battery.getPower();
}
public static void main(String... arg){
MP3Player player = new MP3Player();
/** 使用至沒電為止 */
while(player.getBatteryPower()>0){
player.play();
}
}
}
class NormalBattery{
private int power = 100; //預設電力100
public int getPower(){
return power;
}
public void usePower(){
power--; //每使用一次就遞減
}
}
Program A.

以上是代表MP3 Player與電池的類別。注意在MP3Player類別裡,battery欄位的初始值已經指名了要使用的電池是NormalBattery。這意味著如果哪天我們發現NormalBattery類別已經無法滿足我們的需求,想替換的話就必須連MP3Player類別一起更改。現在這麼看可能覺得只是小事,但試想若有10個類別同時使用到NormalBattery,你得花多少時間在這種猴子也能做的瑣碎工作上?
為了不讓身為人類的你退化成猴子,我們還是將上面的程式修改一下,並加入一個Battery介面。
interface Battery{
int getPower();
void usePower();
}
class MP3Player{
private Battery battery;
public MP3Player(Battery battery){
this.battery = battery;
}
public static void main(String... arg){
/** 建構 player時將NormalBattery傳入當參數 */
MP3Player player = new MP3Player(new NormalBattery());
....
}
}
class NormalBattery implements Battery{
....
}
Program B.

這Program B.裡,我們先將電池所共有的功能抽象成一個介面,並讓NormalBattery去實作它。在MP3Player類別裡,則讓battery欄位的值在建構時才由傳入的參數決定。如此一來,使用何種Battery介面的實作的決定權,便從MP3Player類別的實作者手中,轉移到MP3Player使用者的手上。就好比使用者可以隨意更換MP3 Player的電池,而不是取決於MP3 Player的製造商。
但是萬一MP3 Player用到一半,突然想把電池拆掉,又或者,一開始就不想要裝電池,該怎麼辦?如果是用建構子傳入參數的方式,因為沒有提供修改battery的方法,也強制MP3Player的使用者在建構MP3Player時就必須把battery的實體傳入。這種方式在某些情況下顯然不適用,因此我們改採另一種方法:不使用建構子傳參數,而是增加Setter方法。
class MP3Player{
private Battery battery;
public MP3Player(){}
public void setBattery(Battery battery){
this.battery = battery;
}
...
public static void main(String... arg){
MP3Player player = new MP3Player();
/** 設定電池 */
player.setBattery(new NormalBattery());
....
}
}
class NormalBattery implements Battery{
....
}
Program C.

Program C.中把改為不在建構子傳參數,而是去將參數傳入新增的setBattery()方法。這種作法在建構子參數太多的時候相當有用,特別是那些可以省略的參數。

介紹到這裡,看似頗為人滿意,終於可以快樂大結局了。但是,在這個能源耗竭的時代,我們必須思考著如果有一天,MP3 Player的價錢會跟電池差不多,只更換電池就顯得沒有意義,連MP3 Player也必須要可以替換才行。為了因應這種變態的要求,MP3 Player廠商終於決定不再販賣MP3 Player,宣布轉型為MP3 Player出租商,改成以服務的方式收取費用。
為了因應石油不足對產業結構產生的衝擊,物件導向技術也有一套作法,讓你不需要去理會程式碼寫了什麼或怎麼使用,只要改一改訂單,MP3 Player和電池就送到府上供你使用。這種做法必須仰賴介面,將各個實作類別抽象化,因此必須新增 Player介面。
class NormalBattery implements Battery{....}
interface Battery{
....
}
interface Player{
void setBattery(Battery batery);
void play();
int getBatteryPower();
}
class MP3Player implements Player{
private Battery battery;
public MP3Player(){}
public void setBattery(Battery battery){....}
public void play(){....}
public int getBatteryPower(){....}
}
class NormalBattery implements Battery{....}
import java.io.*;
import java.util.*;
class Main{
public static void main(String... arg)throws Exception{
Properties props = new Properties();
/** 從config.txt檔案中讀出屬性 */
props.load(new FileInputStream("config.txt"));
/** 動態載入類別 */
Class playerClass =
Class.forName(props.getProperty("player"));
Class batteryClass =
Class.forName(props.getProperty("battery"));
Player player = (Player)playerClass.newInstance();
Battery battery = (Battery)batteryClass.newInstance();
/** 設定電池 */
player.setBattery(battery);
/** 使用至沒電為止 */
while(player.getBatteryPower()>0){
player.play();
}
}
}

config.txt→
player:MP3Player
battery:NormalBattery
Program D.

由於Program D.要凸顯不需要知道MP3Player內部實作的特性,因此把main方法移到Main類別裡。現在整個主程式完全看不到MP3Player和NormalBattery的蹤影,但程式還是會呼叫MP3Player的setBattery()方法,並將NormalBattery的實體傳入。程式依賴於config.txt的設定,如果想要修改Player或Batter的實作,只需要將實作類別編譯好,接著修改config.txt的內容就可以了。這種方法很明顯要比前面的方法還要有彈性的多,然而如果實作是一些現成的框架提供的介面,在抽離框架時就必須大量修改,代價挺高。由於這種方法有很高的侵入性,大多框架還是使用前面兩種作法。

一開始曾經提到,Martin Fowler曾經定義出三種DI的型態。 Program B.是屬於type 3,Constructor Injection; Program C.是type 2, Setter Injection; Program D.為type 1, Interface Injection。定義得那麼複雜,其實一點也不難。

沒有留言:

張貼留言