程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 演化架構與緊急設計: 積累慣用模式

演化架構與緊急設計: 積累慣用模式

編輯:關於JAVA

簡介: 本期將之前的 演化架構與緊急設計 文章中的緊急設計概念與一個案例研究相結合,展示如何 發現、積累和利用代碼中意料之外的設計元素。一旦理解了如何識別設計元素,便可以使用該知識改進代 碼的設計。緊急設計使您可以發現代碼中意料之外但是已成為代碼庫重要部分的那些方面。

在本系列第一期 “研究架構和設計” 中,我曾斷言每個較大的項目都包括超出所有人意料的設計元 素。詳細考慮一個問題時,常常會發現有些本以為困難的事情實際上卻更容易,有些本以為容易的事情實 際上卻更困難。隨後的幾期則演示了發現隱藏的有趣的設計元素的一些方法。在本文中,我將那些思想相 結合,並提供一個擴展後的案例研究,在該案例研究中,使用一些工具和方法來發現代碼庫中被忽視但是 同樣重要的部分。

在 “組合方法和 SLAP” 中,我介紹了慣用模式(idiomatic patterns) 的概念。與四人組撰寫的 Design Patterns一書普及的常規設計模式(Design Patterns)相比,慣用模式並不適用於所有項目。但 是,它們是代碼中普遍存在的、有代表性的常見設計慣例。慣用模式的范圍很廣,從純技術模式(例如項 目處理事務的方式),到問題域模式(例如 “處理訂單前總是檢查客戶的信用”),都可以是慣用模式 。發現這些模式是緊急設計的關鍵。

Big Design Up Front 設計方法學的支持者在開始編寫代碼前,要花費大量的時間確定當前應用程序 的所有必需的設計元素。編制的文檔中的大多數內容對於解決方案的總體設計仍是重要的。但是,在實現 軟件的過程中,會不斷遇見意外。實現的每個設計元素與其他設計元素相互聯系,形成極端復雜的依賴和 關系網絡。代碼中有些方面本以為平常不過,但是一旦要實現系統中其他所有必需的部分時,復雜度又隨 之放大。由於不能理解代碼中不同設計元素之間復雜的相互作用,導致在估算完成解決方案所需的努力時 困難重重。在軟件方面,估算仍是一種玄妙的 “黑色藝術”,因為對於如此復雜的耦合和交互網絡,實 在是難以理解,因而也難以分析。

依賴於緊急設計的敏捷方法學則嘗試一種不同的方法。敏捷架構和設計在編寫代碼前也不會避開設計 ,但是它們的實際工作者已經知道,只有等到實現了重要的部分後,才能徹底地理解整個問題。緊急設計 中的開發技巧使您可以推遲做決定,直到掌握了更多的上下文。精益軟件運動有一個很好的概念叫做 最 後可靠時刻(last responsible moment):不是將決定推遲到最後時刻,而是最後可靠時刻。越是往後 推遲設計決定,就能掌握越多的信息,從而可以做出更精妙、更符合實際的決定。

積累慣用模式

緊急設計要求在已有代碼中發現設計元素。可以將那些元素看作有復用潛力的有效的抽象。積累那些 慣用模式的一種技巧是使用指標組合。為了演示這種技巧,我將(像之前幾期那樣)使用 Apache Struts 代碼庫。之所以使用 Struts,並不是因為我認為它存在缺陷(實際上恰恰相反),而是因為它比較出名 ,並且是開源的。我認為每個代碼庫都包括慣用模式,所以可以使用任何項目。

使用指標

在 “通過指標進行緊急設計” 中,我討論了使用指標來發現不熟悉的代碼庫中有趣的部分,作為重 構的目標,以改進設計。我使用了兩個指標:圈復雜度(cyclomatic complexity) 和 傳入耦合 (afferent coupling)。圈復雜度是衡量一個方法相對於另一個方法的相對復雜度的指標。因此,該指 標只有與其他圈復雜度指標相比較才有用。但是,可以說,具有較低圈復雜度的方法通常更簡單。而傳入 耦合則表示其他類通過字段或參數引用當前類的次數。我使用 CJKM 指標工具收集 Struts 代碼庫上的這 些數字。

對 Struts 2 代碼庫計算這兩個指標可得到圖 1 所示的表,其中只顯示關心的兩個指標:

圖 1. ckjm 指標結果表

圖 2 顯示相同的表,按 Weight Methods per Class(WMC)排序:

圖 2. ckjm 指標,按 WMC 排序

單看這個結果,可以認為 DoubleListUIBean 類是 Struts 代碼庫中最復雜的類。這意味著可以將這 個類作為重構的候選目標,試著減少一些復雜性,並看看是否能發現可抽象的、重復的模式。然而,WMC 數字並不能告訴您是否值得花時間重構這個類,以改進設計。注意這個類的 Ca(傳入耦合)指標,它的 值為 3。這意味著只有 3 個其他的類使用這個類。花費大量的時間改進這個類的設計也許並不值得。

圖 3 顯示相同的 CKJM 結果,這一次按 Ca 排序:

圖 3. ckjm 結果,按 Ca(傳入耦合)排序

這個組合的視圖表明,Struts 中最常用的類是 Component(這並不奇怪,因為 Struts 是一個 Web 框架)。雖然 Component 不如 DoubleListUIBean 復雜,但是有 177 個其他的類使用它,因此很適合作 為改進設計的目標。使 Component 的設計變得更好,可以在很多其他的類上取得良好的連鎖反應。

通過 圖 3 所示的視圖,可以逐個查看復雜度和引用次數。要發現有設計挑戰的類,可以看看兩個數 字都比較高的組合(即被很多其他類使用的復雜的類)。我首先選擇的用於研究的類是 UIBean 類,它的 圈復雜度是 53,傳入耦合是 22。這是一個被很多其他類使用的復雜的類,所以我將對它作進一步的研究 。

ckjm 報告的圈復雜度數字表示這個類中所有方法的復雜度之和。我想確定是什麼使這個類如此復雜, 所以需要各個方法的復雜度數字。對這個類運行開源圈復雜度工具 JavaNCSS,可得到圖 4 所示的結果:

圖 4. UIBean 類中各個方法的復雜度數字

到目前為止,最復雜的方法是 evaluateParams(),其復雜度為 43(也是代碼行數最多的)。該方法 顯然是用於處理常見的作為請求的一部分傳遞給 Struts 控制器的額外參數,將參數類型發送到實際的 Struts 類和組件。該代碼中存在很多結構性重復,如清單 1 所示:

清單 1. evaluateParams() 方法的部分內容,其中有結構性重復

if (label != null) {
   addParameter("label", findString(label));
}

if (labelPosition != null) {
   addParameter("labelposition", findString(labelPosition));
}

if (requiredposition != null) {
   addParameter("requiredposition", findString(requiredposition));
}

if (required != null) {
   addParameter("required", findValue(required, Boolean.class));
}

if (disabled != null) {
   addParameter("disabled", findValue(disabled, Boolean.class));
}

if (tabindex != null) {
   addParameter("tabindex", findString(tabindex));
}

if (onclick != null) {
   addParameter("onclick", findString(onclick));
}
// much more code elided for space considerations

該代碼可作為改進的候選目標(見下一小節 改進代碼,第 1 部分),但是我想再多看一下,該代碼 存在的原因 是什麼,為什麼它包含如此多的復雜性。

放眼其他圈復雜度和傳入耦合值都比較高的 組合,我發現了 WebTable,它的那兩個值分別為 33 和 12。對它運行 JavaNCSS,可以肯定我的懷疑: 它的第二復雜的方法是 evaluateExtraParams()。在這裡我看到一個模式!看到這個重復的復雜元素出現 在很多不同的類中,我懷疑有很多偶然的與參數有關的復雜性,所以我做一個實驗。通過使用一點 UNIX® 命令行魔術,我觀察 Struts 中有多少類中包含名為 evaluateParams() 或 evaluateExtraParams() 的方法:

find . -name "*.java" | xargs grep -l "void evaluate.*Params" > pbcopy

這個命令查找當前目錄以下的所有 Java™ 源文件,對於每個找到的文件,它在文件中搜索以 evaluate 開頭,以 Params 結尾的方法定義。最後的重定向(>)將結果文件粘貼到剪貼板上(至少 在 Mac 上是如此)。當我粘貼結果時,看到了令我驚訝的事:

AbstractRemoteCallUIBean.java
Anchor.java
Autocompleter.java
Checkbox.java
ComboBox.java
DateTimePicker.java
Div.java
DoubleListUIBean.java
DoubleSelect.java
 
File.java
Form.java
FormButton.java
Head.java
InputTransferSelect.java
Label.java
ListUIBean.java
OptionTransferSelect.java
Password.java
Reset.java
Select.java
Submit.java
TabbedPanel.java
table/WebTable.java
TextArea.java
TextField.java
Token.java
Tree.java
UIBean.java
UpDownSelect.java

所有這些類中都包含以上兩個方法中的一個或兩個!我發現了一個慣用模式。顯然,Struts 中的很多 類需要覆蓋和定制處理參數的行為,所有這些類各自負責定制。現在的問題是:如何使之變得更好?

改進代碼,第 1 部分

在 UIBean 的 evaluateParams() 方法中,可以看到很多不同的結構性重復,我的一個同事稱之為 “ 相同的空格,不同的值”。換句話說,結構相同,但是代入不同的類或變量名。這代表著一種代碼味道, 因為應用程序中前後出現實際上可以復制-粘貼的代碼,這些代碼差別很小。

修復結構性重復的一種常見的技巧是使用元編程將重復的結構封裝到一個地方。清單 2 顯示一個新的 方法,以及 evaluateParams() 方法中經過改進的前一部分,這裡使用反射提供所需的不同的值:

清單 2. 通過元編程消除結構性重復

protected void handleDefaultParameters(final String paramName) {
  try {
    Field f = UIBean.class.getField(paramName);
    if (f.get(this) != null) 
      addParameter(paramName, findString(paramName));
  } catch (Exception e) {
    throw new RuntimeException(e.getMessage());
  }
}

public void evaluateParams() {

  addParameter("templateDir", getTemplateDir());
  addParameter("theme", getTheme());

  String[] defaultParameters = new String[] {"label", "labelPosition",  "requiredPosition",
    "tabindex", "onclick", "ondoubleclick", "onmousedown", "onmouseup", "onmouseover",
    "onmousemove", "onmouseout", "onfocus", "onblur", "onkeypress", "onkeydown",
    "onkeyup", "onselect", "onchange", "accesskey", "cssClass", "cssStyle",  "title"};

  for (String s : defaultParameters) 
    handleDefaultParameters(s);

清單 2 中的 handleDefaultParameters() 方法將原有的重復結構封裝到一條 if 語句中。它接收一 個指定 Struts 參數名的參數,並通過編程方式使用反射獲取適當的字段。然後,它對原始代碼進行 null 檢查,最終調用 Struts addParameter() 方法。

有了 handleDefaultParameters 方法後,就可以大大減少原有代碼的行數(以及圈復雜度)。我為所 有適用的 Struts 參數名創建一個 String 數組,然後迭代那個數組,在每次迭代中調用 handleDefaultParameters() 方法。

將所有參數檢查合並到一個簡潔的地方,這不僅僅是消減了方法的大小。原始的方法的圈復雜度是 43 。之前每個 if 塊占用 3 行代碼(並貢獻了 1 個點的圈復雜度)。我用一個含 9 行代碼的方法(圈復 雜度為 4)消除了重復,減少了 66 行代碼(22 個參數,每個參數需要 3 行代碼)。這意味著這個簡單 的改變使這個類因這個新方法而減少了 57 行代碼,並使圈復雜度減少 18 個點(1 個 CC 點 x 22 個參 數 - 4 個 CC 點)。因為那樣小的一個改變,我大大改善了應用程序的可讀性、指標、大小和可維護性 。如果將來需要改變調用 Struts addParameter() 方法的方式,只需在一個地方做出更改。

這是一個短期的修復,這是為了演示簡單的改變如何對代碼的簡潔度產生深遠的影響。但是,如果是 我的代碼庫,我會采用一個長期的解決方案。

改進代碼,第 2 部分

如果是我的項目,我會將整個參數處理機制抽象到它自己的一組類中,構建 Struts 中的一個子框架 。處理參數的代碼的復雜性,以及它的廣泛性和數量,意味著應該將它當做 Struts 中的一等公民。這不 是一篇文章所能詳述的,但是可以看到,Struts 中有大量的復雜性是圍繞這個問題產生的。

緊急設計和慣用模式

您認為 Struts 的原設計者曾經想到過處理參數所需的代碼有多少嗎?軟件就是這樣。有時候可以根 據問題域的理論知識預測復雜性,但是在編寫代碼時,又會產生新的約束和機會,這些實際上是無法預測 的。實際上,資深的開發人員在預測那樣的 “硬骨頭” 方面並不會做得更好。他們只是更相信,那個神 秘的硬骨頭最終會出現。

緊急設計的魅力之一是有這樣的認識:我們不能可靠地預測什麼會變成硬骨頭,但是我們應該對此保 持警惕。如果帶著發現抽象和模式的期望去看代碼庫,就更容易有所發現。

最後我以一個案例研究作為結束,該案例研究基於一個 ThoughtWorks 項目,這是我間歇性地從事的 一個項目。在這個大型 Ruby on Rails 項目的早期,技術主管意識到,在一些單獨的情況下需要異步行 為(例如,上傳大量的圖像時,用戶需要能夠暫時離開頁面,之後再回到頁面)。如果我們存著 Big Design Up Front 的思維,那麼就會立即尋求一個消息隊列。但是,在項目一開始,我們還不能完全知道 有哪些地方需要異步,一般的態度是尋求最大的消息隊列,以確保能處理將來的新需求。但是我們的技術 主管非常明智地沒有那樣做。他決定我們已有的足以適合當前狀況。

時間往後推移 2 年。到現在,應用程序有 3 個不同的異步行為,當前解決方案開始成為瓶頸。現在 ,是時候使用一個消息隊列了。但是,由於技術主管推遲了如此長的時間才做決定,我們現在十分清楚這 個應用程序在消息傳遞方面的需求,因此可以采用最簡單的工具來完成該任務。由於耐心等到最後可靠時 刻才做決定,我們避免了因一個並不那麼需要的工具帶來的意外復雜性而做的數年的工作 — 從而獲得更 簡潔的代碼,使新特性具有更高的速度,也減少了相關的煩人的工作。

讓代碼引領設計,這意味著對需求有更好的理解。設計決定推遲得越久,最終做出具有長期意義的決 定時,便擁有更清晰的路線。

結束語

本系列的很多文章都是在向您灌輸一些上下文,使您明了真正的優點。本期幾乎將本系列之前的每篇 文章都貫穿起來,利用那些文章中提到的技巧、工具和態度。緊急設計要求具有看到和積累慣用模式和抽 象、工具和技巧的能力,以便在它們出現時能夠利用它們。Design up front 有助於發現重要的部分(敏 捷項目要做足夠多的預先設計,以決定那些事情),但是開始編寫代碼後,仍應該保持警惕,您將發現令 人驚訝的重要的設計元素。每個代碼庫都有慣用模式。您必須學會發現它們並采取行動。

在最近幾期,我幾乎都是在談論設計和相關的問題。下一次,我將重新深入研究演化架構,並展示將 敏捷開發技巧與架構概念相結合時出現的一些常見問題和解決方案。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved