在過去一年的時間中,我在“ 追求代碼質量 ” 專欄撰寫了大量的文章。這 些文章向大家介紹了許多可以改進代碼質量的工具和技巧。我已經向大家展示了 如何應用代碼度量來監控代碼庫的質量;如何使用 TestNG、FIT 和 Selenium 之類的測試框架來檢驗應用程序的功能;以及如何使用 XMLUnit 和 StrutsTestCase 之類的擴展框架(和一些功能強大的幫助工具,如 Cargo 和 DbUnit)來擴展測試框架的應用范圍。
雖然代碼度量和開發人員測試對於在整個開發過程中確保代碼質量非常重要 (就像我經常所說的,要及時並經常進行測試),但是它們基本上只能對代碼質 量做出反應。您通過測試和度量代碼來確定和量化代碼的質量,但是代碼本身都 已經寫好了。不論您做出何等努力,都會受困於最初的設計。
當然,不同的方法所設計出來的軟件系統會有好有壞,良莠不齊。優秀設計 的關鍵因素之一就是注意保持系統的可維護性。粗劣設計的並可執行的系統可能 易於編寫,但是要對它們提供支持確實是一個挑戰。這些系統往往脆弱不堪,也 就是說對系統中某個區域的修改將會影響到其它看上去毫不相干的區域,因此要 對它們進行重構也相當的困難和耗時。向代碼庫中添加開發人員測試可以為我們 提供工作的規劃,但是其進展本身仍然是一個艱苦和緩慢的過程。
我們可以通過重構來改進已經編寫好的代碼,但是通常來說在代碼已完成之 後再進行改動花費巨大。而如果在一開始就把代碼編寫得 盡善盡美 會不會更加 方便和輕松呢? 這個月,我將介紹一種非常主動的技巧,可以確保軟件系統的 質量和可維護性。依賴性倒置原則 被證明是編寫可維護和可測試的高質量代碼 的必要條件。依賴性倒置原則的基本思想就是對象應該依賴於抽象 而不是實現 。
是依賴性倒置 而不是依賴性注入
依賴性倒置原則與依賴性注入並沒有直 接的關系。依賴性注入,也被稱作控制反轉(inversion of control,IOC),即 使用 Spring 之類的框架在運行的時候(而不是在編譯的時候)鏈接對象的依賴 關系。雖然依賴性倒置和依賴性注入並不需要同時使用,但是它們是互補的:兩 個技巧都力爭利用抽象而不是實現。
過於緊密的耦合
您可能至少聽說過面向對象編程中所使用的術語耦合(coupling)。耦合即 應用程序中各組件(或各對象)間的相互關系。松散耦合的應用程序要比緊密耦 合的應用程序更具模塊化。松散耦合應用程序中的組件依賴於各種接口和抽象類 ,而緊密耦合的系統則與之相反,其組件依賴於各種具體的類。在松散耦合的系 統中,其組件是使用抽象而不是 實現來相互關連的。
如果有圖解的話,可以很輕松地理解緊密耦合的問題。舉例說明,圖 1 中的 軟件系統的 GUI 與它的數據庫相耦合:
圖 1. 一個緊密耦合的系統
GUI 對某個實現(而不是抽象)的依賴會對系統造成限制。在數據庫未啟動 和運行的情況下 GUI 是無法執行的。從功能的角度上看這種設計似乎並不是很 糟糕 —— 畢竟,我們一直都是這樣編寫應用程序而且也沒有出什麼問題 —— 但是測試就要另當別論了。
‘脆弱’ 的系統
圖 1 中的系統使得隔離編程格外地困難,而這對測試和維護系統各個方面又 十分必要。您將需要一個具有正確查找數據的活動數據庫來測試 GUI,和一個運 行正常的 GUI 來測試數據訪問邏輯。您可以使用 TestNG-Abbot(現在的名稱為 FEST)來測試前端,但是這樣仍然無法告訴您任何有關數據庫功能的內容。
清單 1 展示了這種糟糕的耦合。GUI 的一個特定的按鈕定義了一個 ActionListener,它通過 getOrderStatus 調用直接與底層數據庫通信。
清單 1. 把 ActionListener 定義為 GUI 中的一個按鈕
findWidgetButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
try {
String value = widgetValue.getText();
if (value == null || value.equals("")) {
dLabel.setText("Please enter a valid widgetID");
} else {
dLabel.setText(getOrderStatus(value));
}
} catch (Exception ex) {
dLabel.setText("Widget doesn't exist in system");
}
}
//more code
});
單擊 GUI 的按鈕組件後,直接從數據庫中檢索某個特定命令的狀態,如清單 2 所示:
清單 2. GUI 通過 getOrderStatus 方法直接與數據庫通信
private String getOrderStatus(String value) {
String retValue = "Widget doesn't exist in system";
Connection con = null;
Statement stmt = null;
ResultSet rs = null;
try {
con = DriverManager.getConnection ("jdbc:hsqldb:hsql://127.0.0.1", "sa", "");
stmt = con.createStatement();
rs = stmt.executeQuery("select order.status "
+ "from order, widget where widget.name = "
+ "'" + value + "' "
+ "and widget.id = order.widget_id;");
StringBuffer buff = new StringBuffer();
int x = 0;
while (rs.next()) {
buff.append(++x + ": ");
buff.append(rs.getString(1));
buff.append("\n");
}
if(buff.length() > 0){
retValue = buff.toString();
}else{
retValue = "Widget doesn't exist in system";
}
} catch (SQLException e1) {
e1.printStackTrace();
} finally {
try {rs.close();} catch (Exception e3) {}
try {stmt.close();} catch (Exception e4) {}
try {con.close();} catch (Exception e5) {}
}
return retValue;
}
清單 2 中的代碼出現了問題,尤其是它通過一個硬編碼的 SQL 語句直接與 一個硬編碼的數據庫進行通信。Yeeesh! 您能夠想像開發人員測試這種 GUI 和 相關數據庫的挑戰嗎(順便說一下,測試本應該簡單得像測試一個 Web 頁面一 樣)? 倘若對數據庫的任何改動都將 影響到 GUI,那麼要考慮修改系統的話會 使情況變得更糟。
DAO 模式
Data Access Object (DAO) 是一種設計模式,它旨在使用接口 和相關實現把低級的數據訪問操作從高級事務邏輯中分離出來。從本質上說,某 個具體的 DAO 類通過特定的數據源實現訪問數據的邏輯。DAO 模式使得只使用 一個接口為多個數據庫,或者甚至各種不同的數據源(如文件系統)定義多個具 體實現成為了可能。
轉變為松散耦合!
現在在腦海中考慮一下使用依賴性倒置原則設計的相同的系統。如圖 2 所示 ,通過向應用程序添加兩個組件來解除應用程序中的耦合是可能的:這兩個組件 分別是一個接口和一個實現:
圖 2. 一個松散耦合的系統
在圖 2 所示的應用程序中,GUI 依賴於一個抽象 —— 一個數據訪問對象或 DAO。DAO 的執行直接依賴於數據庫,但是 GUI 本身並沒有陷入其中。以 DAO 的形式添加一個抽象可以從 GUI 實現將數據庫實現解耦。一個接口會替代數據 庫與 GUI 代碼相耦合。清單 3 顯示了該接口。
清單 3. WidgetDAO 是一個能幫助解耦架構的抽象
public interface WidgetDAO {
public String getOrderStatus(String widget);
//....
}
GUI 的 ActionListener 代碼引用接口類型 WidgetDAO(定義在清單 3 中) 而不是接口的實際實現。在清單 4 中,GUI 的 getOrderStatus() 方法在本質 上指定的是 WidgetDAO 接口:
清單 4. GUI 依賴於抽象,而不是數據庫
private String getOrderStatus(String value) {
return dao.getOrderStatus(value);
}
對 GUI 完全隱藏了這個接口的實際實現,因為它是通過一個工廠來請求實現 類型的,如清單 5 所示:
清單 5. 對 GUI 隱藏了 WidgetDAO 實現
private WidgetDAO dao;
//...
private void initializeDAO() {
this.dao = WidgetDAOFactory.manufacture();
}
進展順利
注意,清單 5 中的 GUI 中的代碼只引用接口類型 —— GUI 中的任何地方 都沒有使用(或導入)接口的實現。這種對實現細節的抽象是靈活性的關鍵:它 使您能夠更換實現類型,而完全不會影響到 GUI。
還要注意,清單 5 中的 WidgetDAOFactory 是如何使 GUI 避開 WidgetDAO 類型的創建細節的。這些是工廠的任務,如清單 6 所示:
清單 6. 工廠對 GUI 隱藏了實現細節
public class WidgetDAOFactory {
public static WidgetDAO manufacture(){
//..
}
}
使 GUI 引用對某個接口類型的數據檢索可以為創建不同的實現提供靈活性。 在這種情況下,部件信息保存在數據庫中,因此可以創建一個 WidgetDAOImpl 類與數據庫直接通信,如清單 7 所示:
清單 7. WidgetDAO 類型的任務
public class WidgetDAOImpl implements WidgetDAO {
public String getOrderStatus(String value) {
//...
}
}
注意,實現代碼並未包含在這些例子中。這些代碼並不重要,真正有價值的 是原理。您不應該關心 WidgetDAOImpl 的 getOrderStatus() 方法是如何運作 的。它可以從數據庫或者從某個文件系統中獲得狀態信息,但重點是這不會對您 產生什麼影響!
現在,分離 GUI
因為 GUI 現在依賴於某個抽象並且通過一個工廠來獲得該抽象的實現,所以 我們可以輕易地創建一個沒有與數據庫或者文件系統相耦合的模仿類。模仿類用 於分離 GUI,如清單 8 所示:
清單 8. 分離變得簡單
public class MockWidgetDAOImpl implements WidgetDAO {
public String getOrderStatus(String value) {
//..
}
}
添加一個模仿類是設計可維護性的系統的最後一個步驟。把 GUI 與 數據庫 分離開來意味著我們可以測試 GUI 而無需關心特定的數據。我們還可以測試數 據訪問邏輯而無需關心 GUI。
結束語
您可能沒有過多地考慮這些,但是您如今所設計和構建的應用程序使用壽命 可能非常長久。您將繼續開發其它的項目,或者在其它的公司工作,但是您的代 碼(如 COBOL)將會留下來,甚至有可能使用幾十年。
開發人員所贊同的一點是:編寫良好的代碼易於維護,依賴性倒置原則是進 行可維護性設計的可靠方法。依賴性倒置注重依賴於抽象(而非實現),這樣可 以在同一個代碼庫中創建大量的靈活性。借助一個 DAO 來應用這個技巧,就如 您這個月所看到的,不僅可以確保您能夠在需要的時候修改代碼庫,還可以使其 它的開發人員修改代碼庫。