控制反轉IoC簡介
在實際的應用開發中,我們需要盡量避免和降低對象間的依賴關系,即降低耦合度。通常的業務對象之間都是互相依賴的,業務對象與業務對象、業務對象與持久層、業務對象與各種資源之間都存在這樣或那樣的依賴關系。但是如何才能做到降低類之間的依賴關系呢?這就是本文核心IoC需要解決的問題,下面從兩大點具體介紹IoC:
(1)IoC與DI的基本概念
IoC(Inversion Of Control)即控制反轉,其具體就是由容器來控制業務對象之間的依賴關系,而不是像傳統方式中由代碼來直接控制。控制反轉的本質,是控制權由應用代碼轉到了外部容器,控制權的轉移即是所謂的反轉。控制權的轉移帶來的好處就是降低了業務對象之間的依賴程度,即實現了解耦。
IoC的實現策略有兩種:
1)依賴查找:容器中的受控對象通過容器的API來查找自己所依賴的資源和協作對象。這種方式雖然降低了對象間的依賴,但是同時也使用到了容器的API,造成了我們無法在容器外使用和測試對象;
2)依賴注入(又稱DI:Dependency Injection):對象只提供普通的方法讓容器去決定依賴關系,容器全權負責組建的裝配,它會把符合依賴關系的對象通過屬性或者是構造函數傳遞給需要的對象。通過屬性注射依賴關系的做法稱為設值方法注入,將構造子參數傳入的做法稱為構造子注入。
依賴注入的好處如下:
查詢依賴操作和應用代碼分離;
受控對象不會使用到容器的特定的API,這樣我們的受控對象可以搬出容器單獨使用。
(2)IoC模式的實例講解
IoC代表的是一種思想,也是一種開發模式,但它不是什麼具體的開發方法。要理解IoC的概念,最簡單的方式就是看它的實際應用,下面將著重介紹幾個實例來講解IoC的內涵。
我們在開發一個應用系統時,會需要開發大量的Java類,系統將會通過這些Java類之間的相互調用來產生作用。類與類之間的調用關系是系統類之間最直接的關系。因此,我們可以將系統中的類分為兩類:調用者和被調用者。具體如下圖一所示:
圖一:調用方法問題
軟件設計方法及設計模式的發展,共產生了三種類調用的方法:自己創建(new)、工廠模式(get)、外部注入(set),其中外部注入即為IoC/DI的模式。
無論是哪一種方法,都存在兩個角色——調用者和被調用者。下面我們通過實例來講解這三種方法的具體含義。首先,我們設定調用者對象為學生對象Student,被調用者對象為圖書對象Book,要設計的代碼功能是學生學習圖書知識。
從GoF設計模式中,我們已經習慣一種思維編程方式:Interface Driven Design接口驅動,接口驅動有很多好處,可以提供不同靈活的子類實現,增加代碼穩定和健壯性等。為了演示不同的方法在Student取得不同Book對象時的區別,我們采用接口來設計被調用者,實現的代碼如下面三個類所示:
//Book接口類 public interface IBook{ public void learn(); } //BookA實現類 public class BookA implements IBook{ public void learn(){ System.out.println("學習BookA"); } } //BookB實現類 public class BookB implements IBook{ public void learn(){ System.out.println("學習BookB"); } }
其中IBook為圖書的接口,它定義了一個學習接口learn(),並定義了兩個圖書類BookA和BookB來實現該接口,表示是兩本不同的圖書,其中learn()方法分別表示不同圖書學習過程。
下面將從這三種方法講解如何調用圖書類:
1)new——自己創建
Student要學習BookA,就要定義一個learnBookA()的方法,並自己來創建BookA的對象;同樣,要學習BookB,就要定義一個learnBookB()的方法,並自己來創建BookB的對象。然後我們建立一個測試類Test.java來創建一個Student對象,可以分別調用learnBookA()和learnBookB()方法來分別執行兩本書的學習過程。具體實現代碼如下:
//學生類 public class Student{ public void learnBookA(){ IBook book = new BookA(); book.learn(); } public void learnBookB(){ IBook book = new BookB(); book.learn(); } } //測試運行 public class Test{ public static void main(){ Student student = new Student(); student.learnBookA(); student.learnBookB(); } }
該方法在調用者Student需要調用被調用者IBook時,需要由自己創建一個IBook對象。這種做法的缺點是,無法更換被調用者,並且要負責被調用者的整個生命周期。具體形式如下圖二所示:
圖二:自己創建方式
2)get——工廠模式
一切對象都由自己創建的缺點是,每一次調用都需要自己來負責創建對象,創建的對象會到處分散,造成管理上的麻煩,比如異常處理等。因此,我們可以將對象創建的過程提取出來,由一個工廠(Factory)統一來創建,需要什麼對象都可以從工廠中取得。
例如下例中,我們創建了一個工廠類BookFactory,為該類添加兩個函數getBookA()和getBookB(),分別用於創建BookA和BookB的對象。然後再創建Student中learnBookA()和learnBookB()中的方法,改為分別在該工廠類中取得這兩個對象。具體實現代碼如下;
//圖書工廠 public class BookFactory{ public static IBook getBookA(){ IBook book = new BookA(); } public static IBook getBookB(){ IBook book = new BookB(); } } //學生類 public class Student{ public void learnBookA(){ IBook book = BookFactory.getBookA(); book.learn(); } public void learnBookB(){ IBook book = BookFactory.getBookB(); book.learn(); } } //測試運行 public class Test{ public static void main(){ Student student = new Student(); student.learnBookA(); student.learnBookB(); } }
此時與第一種方法的區別是,多了一個工廠類,並將Student中創建對象的代碼提取到了工廠類,Student直接從工廠類中取得要創建的對象。這種方法的優點是,實現了對象的統一創建,調用者無須關心對象創建的過程,只管從工廠中取得即可。具體形式如下圖三所示:
圖三:工廠模式
這種方法實現了一定程度的優化,使得代碼的邏輯也更趨向於統一。但是,對象的創建依然不靈活,因為對象的取得完成取決於工廠,又多了中間一道工序。
3)set——外部注入
顯然,第一種方式依賴於被調用者對象,第二種方式依賴於工廠,都存在依賴性。為了徹底解決依賴性的問題,我們又取消了工廠類,並僅僅為Student添加一個學習的方法learnBook(),輸入的參數是接口類型IBook。在使用Student的方法時,我們先創建IBook的具體對象,然後再把該對象作為learnBook()的輸入參數注入到Student,調用接口IBook的統一方法learn()即可完成學習過程。具體實現代碼如下所示:
//學生類 public class Student{ public void learnBook(IBook book){ book.learn(); } } //測試運行 public class Test{ public static void main(){ IBook bookA = new BookA(); IBook bookB = new BookB(); Student student = new Student(); student.learnBook(bookA); student.learnBook(bookB); } }
這樣我們完全簡化了Student類的方法,learnBook()的方法不再依賴於某一個特定的Book,而是使用了接口類IBook,這樣只要在外部創建任意IBook的實現對象輸入到該方法即可,使得Student類完全解脫了與具體某一種Book的依賴關系。上例中的Test.java,分別創建了bookA和bookB對象,同樣都可以調用Student的learnBook()方法,使得Student變得完全通用。具體形式如下圖四所示:
圖四:外部注入方式
可見,set——外部注入方式完全拋開了依賴關系的枷鎖,可以自由的由外部注入,這就是IoC,將對象的創建個獲取提前到外部,由外部容器提供需要的組件。