CSS

[Hollywood Principle] Dependency Injection Pattern

今天讀了Agile上面有關DIP的段落,乘機把它拿出來再複習一下:

首先,為什麼要用DIP呢?

書上舉了一個很好玩的例子來說明,叫"Hollywood Principle"
意思是"Don't Call me, I'll Call you"
好萊塢的經紀人不希望面試者主動聯絡他們,而是當他們決定要用你的時候才會主動聯絡。

以程式來說,就是讓物件自己決定要如何呼叫其他物件,而不是依賴於其他物件。

假設說今天我們要設計一個可以讀取Book資料的Reader

class Reader{
    private Book book = new Book("Harry Potter");
    public void read(int page){
        book.read(page);
    }
}

Reader依賴於book這個class的函式,
所以當我們要為這個程式增加可以閱讀EBook功能的時候,我們必須加入邏輯判斷來區別
第一個想到的就是新增TypeCode判斷,
於是醜陋的程式碼就產生了...

class Reader{
    static final int BOOK = 1,EBOOK = 2;
    public void read(int page, int bookType){
        if(bookType = BOOK){
            Book book = new Book("Harry Potter");
            book.read(page);
        }else if(bookType = EBOOK){
            EBook book = new EBook("Harry Potter");
            book.read(page);
        }
    }
}

使用Dependency Injection Pattern,能使Reader這個Class不會被book這個class綁死,
相反的,Reader可以先宣告自己會使用的介面,等於是告訴其他Class
"我會用這個方式來呼叫你,如果你想讓我用的話,就先實做這個介面":

class Reader{
    private IBook book;
    public void read(int page){
        book.read(page);
    }

    public Reader(IBook book){
        this.book = book;
    }
}

class Book implement IBook{...}
class EBook implement IBook{...}

使用介面,或是abstract class來宣告使用的介面,
讓Reader這個class可以不被單一的Book class所綁死
只要是實作IBook這個介面的Class,Reader都可以使用

public Reader(IBook book){
        this.book = book;
    }

而這段程式碼在呼叫Reader的建構式時傳入它要使用的Book
這就是Dependency Injection(控制反轉)的一種
是當你要使用物件的時候才傳入物件之間的依賴關係,而不是在一開始就在程式碼中指定
這樣可以保持物件之間彼此的鬆散偶合,增加擴充性.

另外,在作單元測試時,DI也是很有用的一個Pattern
如果我們要測試Reader的Read()這個函式,但是Read這個函式又會呼叫到Book,
然後Book會連接到背後的資料物件...這樣我們無法知道說測試的成功或失敗,
是因為Reader的關係,還是Book,或是背後的資料庫的問題.

這個時候我們就可以使用Dependency Injection的特性,將Book與Reader之間的關係給切開,
先寫一個測試用的Book,實作IBook的介面,並且紀錄是否被呼叫:

public class TestBook implement IBook(){
    public bool readed = false;
    public void read(int page){ 
        readed = true;
    }
}
//測試案例:
    public void ReaderTest(){

        TestBook book = new TestBook()   
        Reader reader = new Reader(book);
     
        reader.read();

        assertTrue(book.readed);
    }

利用這個方式,我們可以只測試我們想測試的邏輯.
也可以寫一個假的Data Access Class,將資料庫的部分先抽換掉,
來測試系統的介面與流程是否ok.

more:
Martin Flower提出的DI概念
良葛格對DI的詳細說明