20.2 鬆散耦合
只要應用的某個部分過分依賴於另一部分,代碼就是耦合過緊,難於維護。典型的問題如: 對象直接引用另一個對象,並且當修改其中一個的同時需要修改另外一個。緊密耦合的軟件難於維護並且需要經常重寫。
因爲 Web 應用所涉及的技術,有多種情況會使它變得耦合過緊。必須小心這些情況,並儘可能維護弱耦合的代碼。
1.解耦 HTML/JavaScript
一種最常見的耦合類型是 HTML/JavaScript 耦合。在 Web 上,HTML 和 JavaScript 各自代表瞭解決方案中的不同層次: HTML 是數據,JavaScript 是行爲。因爲它們天生就需要交互,所以有多種不同的方式將這兩個技術關聯起來。但是,有一些方法會將 HTML 和 JavaScript 過於緊密地耦合在一起。
直接寫在 HTML 中的 JavaScript ,使用包含內聯代碼的 <script> 元素或者是使用 HTML 屬性來分配事件處理程序,都是過於緊密的耦合。請看一下代碼:
<!-- 使用了 <script> 的緊密耦合的 HTML/JavaScript -->
<script type="text/javascript">
document.write("Hello world!");
</script>
<!-- 使用事件處理程序屬性值的緊密耦合的 HTML/JavaScript -->
<input type="button" value="Click Me" οnclick="doSomething()" />
雖然這些從技術上來說都是正確的,但是實踐中,它們將表示數據的 HTML 和定義行爲的 JavaScript 緊密耦合在了一起。理想情況是,HTML 和 JavaScript 應該完全分離,並通過外部文件和使用 DOM 附加行爲來包含 JavaScript 。
當 HTML 和 JavaScript 過於緊密的耦合在一起時,出現 JavaScript 錯誤時就要先判斷錯誤是出現在 HTML 部分還是在 JavaScript 文件中。它還會引入和代碼是否可用的相關新問題。在這個例子中,可能在 "doSomething()" 函數可用之前,就已經按下了按鈕,引發一個 JavaScript 錯誤。因爲任何對按鈕行爲的更改要同時觸及 HTML 和 JavaScript ,因此影響了可維護性。而這些更改本應該只在 JavaScript 中進行。
HTML 和 JavaScript 的緊密耦合也可以在相反的關係上成立: JavaScript 包含了 HTML 。這通常會出現在使用 innerHTML 來插入一段 HTML 文本到頁面上這種情況中,如下面的例子:
// 將 HTML 緊密耦合到 JavaScript
function insertMessage(msg){
var container = document.getElementById("container");
container.innerHTML = "<div class=\"msg\"><p class=\"post\">" + msg + "</p>" + "<p><em>Latest message above.</em></p></div>";
}
一般來說,你應該避免在 JavaScript 中創建大量 HTML 。再一次重申要保存層次的分離,這樣可以很容易的確定錯誤來源。當使用上面這個例子的時候,有一個頁面佈局的問題,可能和動態創建的 HTML 沒有被正確格式化有關。不過,要定位這個錯誤可能非常困難,因爲你可能一般先看頁面的源代碼來查找那段煩人的 HTML ,但是卻沒能找到,因爲它是動態生成的。對數據或者佈局的更改也會要求更改 JavaScript,這也表明了這兩個層次過於緊密地耦合了。
HTML 呈現應該儘可能與 JavaScript 保持分離。當 JavaScript 用於插入數據時,儘量不要直接插入標記。一般可以在頁面中直接包含並隱藏標記,然後等到整個頁面渲染好之後,就可以用 JavaScript 顯示該標記,而非生成它。另一種方法是進行 Ajax 請求並獲取更多要顯示的 HTML ,這個方法可以讓同樣的渲染層 (PHP、JSP、Ruby 等等) 來輸出標記,而不是直接嵌在 JavaScript 中。
將 HTML 和 JavaScript 解耦可以在調用過程中節省時間,更加容易確定錯誤的來源,也減輕維護的難度:更改行爲只需要在 JavaScript 文件中進行,而更改標記則只要在渲染文件中。
2.解耦 CSS/JavaScript
另一個 web 層則是 CSS,它主要負責頁面的顯示。JavaScript 和 CSS 也是非常緊密相關的:他們都是 HTML 之上的層次,因此常常一起使用。但是,和 HTML 與 JavaScript 的情況一樣,CSS 和 JavaScript 也可能會過於緊密地耦合在一起。最常見的緊密耦合的例子是使用 JavaScript 來更改某些樣式,如下所示:
// CSS 對 JavaScript 的緊密耦合
element.style.color = "red";
element.style.backgroundColor = "blue";
由於 CSS 負責頁面的顯示,當顯示出現任何問題時都應該只是查看 CSS 文件來解決。然而,當使用了 JavaScript 來更改某些樣式的時候,比如顏色,就出現了第二個可能已更改和必須檢查的地方。結果是 JavaScript 也在某種程度上負責了頁面的顯示,並與 CSS 緊密耦合了。如果未來需要更改樣式表,CSS 和 JavaScript 文件可能都需要修改。這就給開發人員造成了維護上的噩夢。所以在這兩個層次之間必須有清晰的劃分。
現代 web 應用常常要使用 JavaScript 來更改樣式,所以雖然不可能完全將 CSS 和 JavaScript 解耦,但是還是能讓耦合更鬆散的。這是通過動態更改樣式類而非特定樣式來實現的,如下面的例子:
// CSS 對 JavaScript 的鬆散耦合
element.className = "edit";
通過只修改某個元素的 CSS 類,就可以讓大部分樣式信息嚴格保留在 CSS 中。JavaScript 可以更改樣式類,但並不會直接影響到元素的樣式。只要應用了正確的類,那麼任何顯示問題都可以直接追溯到 CSS 而非 JavaScript 。
第二類緊密耦合耦合僅會在 Internet Explorer 中出現 (但運行於標準模式下的 IE8 不會出現),它可以在 CSS 中通過表達式嵌入 JavaScript ,如下例:
div {
width: expression(document.body.offsetWidth - 10 + "px");
}
通常要避免使用表達式,因爲它們不能跨瀏覽器兼容,還因爲它們所引入的 JavaScript 和 CSS 之間的緊密耦合。如果使用了表達式,那麼可能會在 CSS 中出現 JavaScript 錯誤。由於 CSS 表達式而追蹤過 JavaScript 錯誤的開發人員,會告訴你在他們決定看一下 CSS 之前花了多長時間來查找錯誤。
再次提醒,好的層次劃分是非常重要的。顯示問題的唯一來源應該是 CSS,行爲問題的唯一來源應該是 JavaScript 。在這些層次之間保持鬆散耦合可以讓你的整個應用更加易於維護。
3.解耦應用邏輯/事件處理程序
每個 Web 應用一般都有相當多的事件處理程序,監聽着無數不同的事件。然而,很少有能仔細得將應用邏輯從事件處理程序中分離的。請看以下例子:
function handleKeyPress(event){
if (event.keyCode == 13) {
var target = EventUtil.getTarget(event);
var value = 5 * parseInt(target.value);
if (value > 10) {
document.getElementById("error-msg").style.display = "block";
}
}
}
這個事件處理程序除了包含了應用邏輯,還進行了事件的處理。這種方式的問題有其雙重性。首先,除了通過事件之外就沒有方法執行應用邏輯,這讓調試變得困難。如果沒有發生預想的結果怎麼辦?是不是表示事件處理程序沒有被調用還是指應用邏輯失敗?其次,如果一個後續的事件引發同樣的應用邏輯,那就必須複製功能代碼或者將代碼抽取到一個單獨的函數中。無論何種方式,都要作比實際所需更多的改動。
較好的方法是將應用邏輯和事件處理程序相分離,這樣兩者分別處理各自的東西。一個事件處理程序應該從事件對象中提取相關信息,並將這些信息傳送到處理應用邏輯的某個方法中。例如,前面的代碼可以被重寫爲:
function validateValue(value) {
value = 5 * parseInt(value);
if (value > 10) {
document.getElementById("error-msg").style.display = "block";
}
}
function handleKeyPress(event) {
if (event.keyCode == 13) {
var target = EventUtil.getTarget(event);
validateValue(target.value);
}
}
改動過的代碼合理將應用邏輯從事件處理程序中分離了出來。handleKeyPress() 函數確認是按下了 Enter 鍵 (event.keyCode 爲 13) ,然後取得了事件的目標並將 value 屬性傳遞給 validateValue() 函數,這個函數包含了應用邏輯。注意 validateValue() 中沒有任何東西會依賴於任何事件處理程序邏輯,它只是接收一個值,並根據該值進行其他處理。
從事件處理程序中分離應用邏輯有幾個好處。首先,可以讓你更容易更改觸發特定過程的事件。如果最開始由鼠標點擊事件觸發過程,但現在按鍵也要進行同樣處理,這種更改就很容易。其次,可以在不附加到事件的情況下測試代碼,使其更易創建單元測試或者自動化應用流程。
以下是要牢記的應用和業務邏輯之間鬆散耦合的幾條原則:
- 勿將 event 對象傳給其他方法;只傳來自 event 對象中所需的數據;
- 任何可以在應用層面的動作都應該可以在不執行任何事件處理程序的情況下進行;
- 任何事件處理程序都應該處理事件,然後將處理轉交給應用邏輯。
- 不要爲實例或原型添加屬性;
- 不要爲實例或原型添加方法;
- 不要重定義已存在的方法。
- 創建包含所需功能的新對象,並用它與相關對象進行交互;
- 創建自定義類型,繼承需要進行修改的類型。然後可以爲自定義類型添加額外功能。
這段重寫的代碼引入了一個單一的全局對象 MyApplication ,name 和 sayName() 都附加到其上。這樣做消除了一些存在於前一段代碼中的一些問題。首先,變量 name 覆蓋了 window.name 屬性,可能會與其他功能產生衝突;其次,它有助消除功能作用域之間的混淆。調用 MyApplication.sayName() 在邏輯上暗示了代碼的任何問題都可以通過檢查定義 MyApplication 的代碼來確定。
單一的全局量的延伸便是命名空間的概念,由 YUI(Yahoo! User Interface) 庫普及。命名空間包括創建一個用於放置功能的對象。在 YUI的2.x版本中,有若干用於追加功能的命名空間。比如:
- YAHOO.util.Dom -- 處理 DOM 的方法;
- YAHOO.util.Event -- 與事件交互的方法;
- YAHOO.lang -- 用於底層語音特性的方法。
- 如果值應爲一個引用類型,使用 instanceof 操作符檢查其構造函數;
- 如果值應爲一個基本類型,使用 typeof 檢查其類型;
- 如果是希望對象包含某個特定的方法名,則使用 typeof 操作符確保指定名字的方法存在於對象上。
在這段重寫過的代碼中,消息和 URL 都被定義於 Constants 對象中,然後函數引用這些值。這些設置運行數據在無須接觸使用它的函數的情況下進行變更。Constants 對象甚至可以完全在單獨的文件中進行定義,同時該文件可以由包含正確值的其他過程根據國際化設置來生成。
關鍵在於數據和使用它的邏輯進行分離。要注意的值的類型如下所示。
- 重複值 -- 任何在多處用到的值都應抽取爲一個變量。這就限制了當一個值變了而另一個沒變的時候會造成的錯誤。這也包含了 CSS 類名。
- 用戶界面字符串 -- 任何用於顯示給用戶的字符串,都應該被抽取出來以方便國際化。
- URLs -- 在 Web 應用中,資源位置很容易變更,所以推薦用一個公共地方存放所有的 URL。
- 任意可能會更改的值 -- 每當你在用到字面量值的時候,你都要問一下自己這個值在未來是不是會變化。如果答案是 "是" ,那麼這個值就應該被提取出來作爲一個常量。