我們知道緊密耦合的代碼不是個好現象,因此要在設計中儘量避免它 —— 但問題是如何才能避免緊密耦合呢。這個月,我們將學習如何識別一個系統是否有緊密耦合的問題,然後使用依賴性倒置原則解開這種緊密耦合。
雖然代碼度量和開發人員測試對於在整個開發過程中確保代碼質量非常重要(就像我經常所說的,要及時並經常進行測試),但是它們基本上只能對代碼質量做出反應。您通過測試和度量代碼來確定和量化代碼的質量,但是代碼本身都已經寫好了。不論您做出何等努力,都會受困於最初的設計。
|
當然,不同的方法所設計出來的軟件系統會有好有壞,良莠不齊。優秀設計的關鍵因素之一就是注意保持系統的可維護性。粗劣設計的並可執行的系統可能易於編寫,但是要對它們提供支持確實是一個挑戰。這些系統往往脆弱不堪,也就是說對系統中某個區域的修改將會影響到其它看上去毫不相干的區域,因此要對它們進行重構也相當的困難和耗時。向代碼庫中添加開發人員測試可以爲我們提供工作的規劃,但是其進展本身仍然是一個艱苦和緩慢的過程。
我們可以通過重構來改進已經編寫好的代碼,但是通常來說在代碼已完成之後再進行改動花費巨大。而如果在一開始就把代碼編寫得 盡善盡美 會不會更加方便和輕鬆呢? 這個月,我將介紹一種非常主動的技巧,可以確保軟件系統的質量和可維護性。依賴性倒置原則 被證明是編寫可維護和可測試的高質量代碼的必要條件。依賴性倒置原則的基本思想就是對象應該依賴於抽象 而不是實現。
|
您可能至少聽說過面向對象編程中所使用的術語耦合(coupling)。耦合即應用程序中各組件(或各對象)間的相互關係。鬆散耦合的應用程序要比緊密耦合的應用程序更具模塊化。鬆散耦合應用程序中的組件依賴於各種接口和抽象類,而緊密耦合的系統則與之相反,其組件依賴於各種具體的類。在鬆散耦合的系統中,其組件是使用抽象而不是 實現來相互關連的。
如果有圖解的話,可以很輕鬆地理解緊密耦合的問題。舉例說明,圖 1 中的軟件系統的 GUI 與它的數據庫相耦合:
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,那麼要考慮修改系統的話會使情況變得更糟。
|
現在在腦海中考慮一下使用依賴性倒置原則設計的相同的系統。如圖 2 所示,通過嚮應用程序添加兩個組件來解除應用程序中的耦合是可能的:這兩個組件分別是一個接口和一個實現:
在圖 2 所示的應用程序中,GUI 依賴於一個抽象 —— 一個數據訪問對象或 DAO。DAO 的執行直接依賴於數據庫,但是 GUI 本身並沒有陷入其中。以 DAO 的形式添加一個抽象可以從 GUI 實現將數據庫實現解耦。一個接口會替代數據庫與 GUI 代碼相耦合。清單 3 顯示了該接口。
public interface WidgetDAO {
public String getOrderStatus(String widget);
//....
}
GUI 的 ActionListener
代碼引用接口類型 WidgetDAO
(定義在清單 3 中)而不是接口的實際實現。在清單 4 中,GUI 的 getOrderStatus()
方法在本質上指定的是 WidgetDAO
接口:
private String getOrderStatus(String value) {
return dao.getOrderStatus(value);
}
對 GUI 完全隱藏了這個接口的實際實現,因爲它是通過一個工廠來請求實現類型的,如清單 5 所示:
private WidgetDAO dao;
//...
private void initializeDAO() {
this.dao = WidgetDAOFactory.manufacture();
}
注意,清單 5 中的 GUI 中的代碼只引用接口類型 —— GUI 中的任何地方都沒有使用(或導入)接口的實現。這種對實現細節的抽象是靈活性的關鍵:它使您能夠更換實現類型,而完全不會影響到 GUI。
還要注意,清單 5 中的 WidgetDAOFactory
是如何使 GUI 避開 WidgetDAO
類型的創建細節的。這些是工廠的任務,如清單 6 所示:
public class WidgetDAOFactory {
public static WidgetDAO manufacture(){
//..
}
}
使 GUI 引用對某個接口類型的數據檢索可以爲創建不同的實現提供靈活性。在這種情況下,部件信息保存在數據庫中,因此可以創建一個 WidgetDAOImpl
類與數據庫直接通信,如清單 7 所示:
public class WidgetDAOImpl implements WidgetDAO {
public String getOrderStatus(String value) {
//...
}
}
注意,實現代碼並未包含在這些例子中。這些代碼並不重要,真正有價值的是原理。您不應該關心 WidgetDAOImpl
的 getOrderStatus()
方法是如何運作的。它可以從數據庫或者從某個文件系統中獲得狀態信息,但重點是這不會對您產生什麼影響!
因爲 GUI 現在依賴於某個抽象並且通過一個工廠來獲得該抽象的實現,所以我們可以輕易地創建一個沒有與數據庫或者文件系統相耦合的模仿類。模仿類用於分離 GUI,如清單 8 所示:
public class MockWidgetDAOImpl implements WidgetDAO {
public String getOrderStatus(String value) {
//..
}
}
添加一個模仿類是設計可維護性的系統的最後一個步驟。把 GUI 與 數據庫分離開來意味着我們可以測試 GUI 而無需關心特定的數據。我們還可以測試數據訪問邏輯而無需關心 GUI。
您可能沒有過多地考慮這些,但是您如今所設計和構建的應用程序使用壽命可能非常長久。您將繼續開發其它的項目,或者在其它的公司工作,但是您的代碼(如 COBOL)將會留下來,甚至有可能使用幾十年。
開發人員所贊同的一點是:編寫良好的代碼易於維護,依賴性倒置原則是進行可維護性設計的可靠方法。依賴性倒置注重依賴於抽象(而非實現),這樣可以在同一個代碼庫中創建大量的靈活性。藉助一個 DAO 來應用這個技巧,就如您這個月所看到的,不僅可以確保您能夠在需要的時候修改代碼庫,還可以使其它的開發人員修改代碼庫。