近來總是接觸到IoC(Inversion of Control,控制反轉)、DI(Dependency Injection,依賴注入)等編程原則或者模式,而這些是著名Java 框架Spring、Struts 等的核心所在。針對此查了Wikipedia 中各個條目,並從圖書館借來相關書籍,閱讀後有些理解,現結合書中的講解以及自己的加工整理如下:
eg1
問題描述:
開發一個能夠按照不同要求生成Excel或PDF 格式的報表的系統,例如日報表、月報表等等。
解決方案:
根據“面向接口編程”的原則,應該分離接口與實現,即將生成報表的功能提取為一個通用接口ReportGenerator,並提供生成Excel 和PDF格式報表的兩個實現類ExcelGenerator 和PDFGenerator,而客戶Client 再通過服務提供者ReportService 獲取相應的報表打印功能。
實現方法:
根據上面所述,得到如下類圖:
代碼實現:
interface ReportGenerator { public void generate(Table table); } class ExcelGenerator implements ReportGenerator { public void generate(Table table) { System.out.println("generate an Excel report ..."); } } class PDFGenerator implements ReportGenerator { public void generate(Table table) { System.out.println("generate an PDF report ..."); } } class ReportService { // 負責創建具體需要的報表生成器private ReportGenerator generator = new PDFGenerator(); // private static ReportGenerator generator = new ExcelGenerator(); public void getDailyReport(Date date) { table.setDate(date); // ... generator.generate(table); } public void getMonthlyReport(Month month) { table.setMonth(month); // ... generator.generate(table); } } public class Client { public static void main(String[] args) { ReportService reportService = new ReportService(); reportService.getDailyReport(new Date()); //reportService.getMonthlyReport(new Date()); } }
eg2
問題描述:
如上面代碼中的註釋所示,具體的報表生成器由ReportService 類內部硬編碼創建,由此ReportService 已經直接依賴於PDFGenerator 或ExcelGenerator ,必須消除這一明顯的緊耦合關係。
解決方案:引入容器引入一個中間管理者,也就是容器(Container),由其統一管理報表系統所涉及的對象(在這裡是組件,我們將其稱為Bean),包括ReportService 和各個XXGenerator 。在這裡使用一個鍵-值對形式的HashMap 實例來保存這些Bean。
實現方法:
得到類圖如下:
代碼實現:
class Container { // 以鍵-值對形式保存各種所需組件Bean private static Map<String, Object> beans; public Container() { beans = new HashMap<String, Object>(); // 創建、保存具體的報表生起器ReportGenerator reportGenerator = new PDFGenerator(); beans.put("reportGenerator", reportGenerator); // 獲取、管理ReportService 的引用ReportService reportService = new ReportService(); beans.put("reportService", reportService); } public static Object getBean(String id) { return beans.get(id); } } class ReportService { // 消除緊耦合關係,由容器取而代之// private static ReportGenerator generator = new PDFGenerator(); private ReportGenerator generator = (ReportGenerator) Container.getBean("reportGenerator"); public void getDailyReport(Date date) { table.setDate(date); generator.generate(table); } public void getMonthlyReport(Month month) { table.setMonth(month); generator.generate(table); } } public class Client { public static void main(String[] args) { Container container = new Container(); ReportService reportService = (ReportService)Container.getBean("reportService"); reportService.getDailyReport(new Date()); //reportService.getMonthlyReport(new Date()); } }
時序圖大致如下:
效果:
如上面所示,ReportService 不再與具體的ReportGenerator 直接關聯,已經用容器將接口和實現隔離開來了,提高了系統組件Bean 的重用性,此時還可以使用配置文件在Container 中實時獲取具體組件的定義。
eg3
問題描述:
然而,觀察上面的類圖,很容易發現ReportService 與Container 之間存在雙向關聯,彼此互相有依賴關係。並且,如果想要重用ReportService,由於它也是直接依賴於單獨一個Container 的具體查找邏輯。若其他容器具體不同的組件查找機制(如JNDI),此時重用ReportService 意味著需要修改Container 的內部查找邏輯。
解決方案:引入Service Locator
再次引入一個間接層Service Locator,用於提供組件查找邏輯的接口,請看Wikipedia 中的描述或者Java EE 對其的描述1 、描述2 。這樣就能夠將可能變化的點隔離開來。
實現方法:
類圖如下:
代碼實現:
// 實際應用中可以是用interface 來提供統一接口class ServiceLocator { private static Container container = new Container(); public static ReportGenerator getReportGenerator() { return (ReportGenerator)container.getBean("reportGeneraator"); } } class ReportService { private ReportGenerator reportGenerator = ServiceLocator.getReportGenerator(); // ... }eg4
問題描述:
然而,不管是引入Container 還是使用Service Locator ,ReportService 對於具體組件的查找、創建的方式都是'主動'的,這意味著作為客戶的ReportService 必須清楚自己需要的是什麼、到哪裡獲取、如何獲取。一下子就因為What、Where、How 而不得不增加了具體邏輯細節。
例如,在前面'引入Container '的實現方法中,有如下代碼:
class ReportService { // 消除緊耦合關係,由容器取而代之// private static ReportGenerator generator = new PDFGenerator(); // 通過Container..getBean("reportGenerator") '主動'查找private ReportGenerator generator = (ReportGenerator) Container .getBean("reportGenerator");
在'引入Service Locator '的實現方法中,有如下代碼:
class ServiceLocator { privatestatic Container container = new Container(); publicstatic ReportGenerator getReportGenerator() { // 還是container.getBean(), 用了委託而已return (ReportGenerator) container.getBean("reportGeneraator"); }} class ReportService { // ReportService 最終還是'主動'查找,委託給ServiceLocator 而已private ReportGenerator reportGenerator = ServiceLocator.getReportGenerator(); }
解決方案:
在這種情況下,變'主動'為'被動'無疑能夠減少ReportService 的內部知識(即查找組件的邏輯)。根據控制反轉(IoC)原則,可江此種拉(Pull,主動的)轉化成推(Push,被動的)的模式。
例如,平時使用的RSS 訂閱就是Push的應用,省去了我們一天好幾次登錄自己喜愛的站點主動獲取文章更新的麻煩。
而依賴注入(DI)則是實現這種被動接收、減少客戶(在這裡即ReportService)自身包含複雜邏輯、知曉過多的弊病。
實現方法:
因為我們希望是'被動'的接收,故還是回到Container 的例子,而不使用Service Locator 模式。由此得到修改後的類圖如下:
而原來的類圖如下,可以對照著看一下,注意註釋的提示:
代碼實現:
為了使例子能夠編譯、運行,並且稍微利用跟踪代碼的運行結果來顯式整個類圖實例化、互相協作的先後順序,在各個類的構造器中加入了不少已編號的打印語句,以及兩個無關緊要的類,有點唆,具體如下:
import java.util.Date;import java.util.HashMap;import java.util.Map; // 為了能夠編譯運行,多了兩個無關緊要的類class Month { }class Table { publicvoid setDate(Date date) { } publicvoid setMonth(Month month) { }} // ------------ 以下均無甚重要改變----------------- //interface ReportGenerator { publicvoid generate(Table table);} class ExcelGenerator implements ReportGenerator { public ExcelGenerator() { System.out.println("2...開始初始化ExcelGenerator ..."); } publicvoid generate(Table table) { System.out.println("generate an Excel report ..."); }} class PDFGenerator implements ReportGenerator { public PDFGenerator() { System.out.println("2...開始初始化PDFGenerator ..."); } publicvoid generate(Table table) { System.out.println("generate an PDF report ..."); }}//------------ 以上均無甚重要改變----------------- // class Container { // 以鍵-值對形式保存各種所需組件Bean privatestatic Map<String, Object> beans; public Container() { System.out.println("1...開始初始化Container ..."); beans = new HashMap<String, Object>(); // 創建、保存具體的報表生起器ReportGenerator reportGenerator = new PDFGenerator(); beans.put("reportGenerator", reportGenerator); // 獲取、管理ReportService 的引用ReportService reportService = new ReportService(); // 注入上面已創建的具體ReportGenerator 實例reportService.setReportGenerator(reportGenerator); beans.put("reportService", reportService); System.out.println("5...結束初始化Container ..."); } publicstatic Object getBean(String id) { System.out.println("最後獲取服務組件...getBean() --> " + id + " ..."); returnbeans.get(id); }} class ReportService { // private static ReportGenerator generator = new PDFGenerator(); // 消除上面的緊耦合關係,由容器取而代之// private ReportGenerator generator = (ReportGenerator) Container // .getBean("reportGenerator"); // 去除上面的“主動”查找,提供私有字段來保存外部注入的對象private ReportGenerator generator; // 以setter 方式從外部注入publicvoid setReportGenerator(ReportGenerator generator) { System.out.println("4...開始注入ReportGenerator ..."); this.generator = generator; } private Table table = new Table(); public ReportService() { System.out.println("3...開始初始化ReportService ..."); } publicvoid getDailyReport(Date date) { table.setDate(date); generator.generate(table); } publicvoid getMonthlyReport(Month month) { table.setMonth(month); generator.generate(table); }} publicclass Client { publicstaticvoid main(String[] args) { // 初始化容器new Container(); ReportService reportService = (ReportService) Container .getBean("reportService"); reportService.getDailyReport(new Date()); // reportService.getMonthlyReport(new Date()); }}
運行結果:
1...開始初始化Container ...2...開始初始化PDFGenerator ...3...開始初始化ReportService ...4...開始注入ReportGenerator ...5...結束初始化Container ...最後獲取服務組件...getBean() --> reportService ...generate an PDF report ...
注意:
1、根據上面運行結果的打印順序,可見代碼中加入的具體編號是合理的,模擬了程序執行的流程,於是也就不再畫序列圖了。
2、注意該例子中對IoC、DI的使用,是以ReportService為客戶端(即組件需求者)為基點的,而代碼中的Client 類main()中的測試代碼才是服務組件的最終用戶,但它需要的不是組件,而是組件所具有的服務。
3、實際在Spring框剪中,初始化Container顯然不是最終用戶Client應該做的事情,它應該由服務提供方事先啟動就緒。
4、在最終用戶Client中,我們還是用到Container.getBean("reportService")來獲取事先已在Container的構造函數中實例化好的服務組件。而在具體應用中,通常是用XML等配置文件將可用的服務組件部署到服務器中,再由Container讀取該配置文件結合反射技術得以創建、注入具體的服務組件。
分析:
之前是由ReportService主動從Container中請求獲取服務組件,而現在是被動地等待Container注入(Inject,也就是Push)服務組件。控制權明顯地由底層模塊(ReportService 是組件需求者)轉移給高層模塊(Container 是組件提供者),也就是控制反轉了。