第20章 最佳實踐 (二)

 

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 對象中所需的數據;
  • 任何可以在應用層面的動作都應該可以在不執行任何事件處理程序的情況下進行;
  • 任何事件處理程序都應該處理事件,然後將處理轉交給應用邏輯。
牢記這幾條可以在任何代碼中都獲得極大的可維護性的改進,並且爲進一步的測試和開發製造了很多可能。

編程實踐

書寫可維護的 JavaScript 並不僅僅是關於如何格式化代碼;它還關係到代碼做什麼的問題。在企業環境中創建的 Web 應用往往同時由大量人員一同創作。這種情況下的目標是確保每個人所使用的瀏覽器環境都有一致和不變的規則。因此,最好堅持以下一些編程實踐。
1.尊重對象所有權
JavaScript 的動態性質使得幾乎任何東西在任何時間都可以修改。有人說在 JavaScript 沒有什麼神聖的東西,因爲無法將某些東西標記爲最終或恆定狀態。在其他語言中,當沒有實際的源代碼的時候,對象和類是不可變的。JavaScript 可以在任何時候修改任意對象,這樣就可以以不可預計的方式覆寫默認的行爲。因爲這門語言沒有強行的限制,所以對於開發者來說,這是很重要的,也是必要的。
也許在企業環境中最重要的編程實踐就是尊重對象所有權,它的意思是你不能修改不屬於你的對象。簡單地說:如果你不負責維護某個對象、它的對象或者它的方法,那麼你就不能對它們進行修改。更具體地說:
  • 不要爲實例或原型添加屬性;
  • 不要爲實例或原型添加方法;
  • 不要重定義已存在的方法。
問題在於開發人員會假設瀏覽器環境按照某個特定方式運行,而對於多個人都用到的對象進行改動就會產生錯誤。如果某人期望叫做 stopEvent() 的函數能取消某個事件的默認行爲,但是你對其進行了更改,然後它完成了本來的任務,後來還追加了另外的事件處理程序,那肯定會出現問題了。其他開發人員會認爲函數還是按照原來的方式執行,所以他們的用法會出錯並有可能造成危害,因爲他們並不知道有副作用。
這些規則不僅僅適用於自定義類型和對象,對於諸如 Object、String、document、window 等原生類型和對象也適用。此處潛在的問題可能更加危險,因爲瀏覽器提供者可能會在不做宣佈或者是不可預期的情況下更改這些對象。著名的 Prototype JavaScript 庫就出現過這種例子:它爲 document 對象實現了 getElementsByClassName() 方法,返回一個 Array 的實例並增加了一個 each() 方法。John Resig 在他的博客上敘述了產生這個問題的一系列事件。他在帖子中說,他發現當瀏覽器開始內部實現 getElementsByClassName() 的時候就出現問題了,這個方法並不返回一個 Array 而是返回一個並不包含 each() 方法的 NodeList。使用 Prototype 庫的開發人員習慣於寫這樣的代碼:
document.getElementsByClassName("selected").each(Element.hide);
雖然在沒有原生實現 getElementsByClassName() 的瀏覽器中可以正常運行,但對於支持了的瀏覽器就會產生錯誤,因爲返回的值不同。你不能預測瀏覽器提供者在未來會怎樣更改原生對象,所以不管用任何方式修改他們,都可能會導致將來你的實現和他們的實現之間的衝突。
所以,最佳的方法便是永遠不修改不是由你所有的對象。你依然可以通過以下方式爲對象創建新的功能:
  • 創建包含所需功能的新對象,並用它與相關對象進行交互;
  • 創建自定義類型,繼承需要進行修改的類型。然後可以爲自定義類型添加額外功能。
現在很多 JavaScript 庫都贊同並遵守這條開發原理,這樣即使瀏覽器頻繁更改,庫本身也能繼續成長和適應。
2.避免全局量
與尊重對象所有權密切相關的是儘可能避免全局變量和函數。這也關係到創建一個腳本執行的一致的和可維護的環境。最多創建一個全局變量,讓其他對象和函數存在其中。請看以下例子:
// 兩個全局量 -- 避免!!
var name = "Nicholas";
function sayName(){
alert(name);
}
這段代碼包含了兩個全局量:變量 name 和函數 sayName() 。其實可以創建一個包含兩者的對象,如下例所示:
// 一個全局量 -- 推薦
var MyApplication = {
name: "Nicholas",
sayName: function(){
alert(this.name);
}
};

這段重寫的代碼引入了一個單一的全局對象 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 -- 用於底層語音特性的方法。
對於 YUI ,單一的全局對象 YAHOO 作爲一個容器,其中定義了其他對象。用這種方式將功能組合在一起的對象,叫做命名空間。整個 YUI 庫便是構建在這個概念上的,讓它能夠在同一個頁面上與其他的 JavaScript 庫共存。
命名空間很重要的一部分是確定每個人都同意使用的全局對象的名字,並且能儘可能唯一,讓其他人不太可能也使用這個名字。在大多數情況下,可以是開發代碼的公司的名字,例如 YAHOO 或者 Wrox 。你可以如下例所示開始創建命名空間來組合功能。
// 創建全局對象
var Wrox = {};
// 爲 Preofessional JavaScript 創建命名空間
Wrox.ProJS = {};
// 將書中用到的對象附件上去
Wrox.ProJS.EventUtil = { ... };
Wrox.ProJS.CookieUtil = { ... };
在這個例子中,Wrox 是全局量,其他命名空間在此之上創建。如果本書所有代碼都放在 Wrox.ProJS 命名空間,那麼其他作者也應該把自己的代碼添加到 Wrox 對象中。只要所有人都遵循這個規則,那麼就不用擔心其他人也創建叫做 EventUtil 或者 CookieUtil 的對象,因爲它會存在於不同的命名空間中。請看以下例子:
// 爲 Professional Ajax 創建命名空間
Wrox.ProAjax = {};
// 附加該書中所使用的其他對象
Wrox.ProAjax.EventUtil = { ... };
Wrox.ProAjax.CookieUtil = { ... };
// ProJS 還可以繼續分別訪問
Wrox.ProJS.EventUtil.addHandler(...);
// 以及 ProAjax
Wrox.ProAjax.EventUtil.addHandler(....);
雖然命名空間會需要多寫一些代碼,但是對於可維護的目的而言是值得的。命名空間有助於確保代碼可以在同一個頁面上與其他代碼以無害的方式一起工作。
3.避免與 null 進行比較
由於 JavaScript 不做任何自動的類型檢查,所以它就成了開發人員的責任。因此,在 JavaScript 代碼中其實很少進行類型檢測。最常見的類型檢測就是查看某個值是否爲 null。但是,直接將值與 null 比較是使用過度的,並且常常由於不充分的類型檢查導致錯誤。看以下例子:
function sortArray(values){
if (values != null){                // 避免 !
values.sort(comparator);
}
}
該函數的目的是根據給定的比較值對一個數組進行排序。爲了函數能正確執行,values 參數必需是數組,但這裏的 if 語句僅僅檢查該 values 是否爲 null。還有其他的值可以通過 if 語句,包括字符串、數字,它們會導致函數拋出錯誤。
現實中,與 null 比較很少適合情況而被使用。必須按照所期望的對值進行檢查,而非按照不被期望的那些。例如,在前面的範例中,values 參數應該是一個數組,那麼就要檢查它是不是一個數組,而不是檢查它是否非 null 。函數按照下面的方式修改會更加合適:
function sortArray(values){
if(values instanceof Array){             // 推薦
values.sort(comparator);
}
}
該函數的這個版本可以阻止所有非法值,而且完全用不着 null 。
這種驗證數組的技術在多框架的網頁中不一定正確工作,因爲每個框架都有其自己的全局對象,因此,也有自己的 Array 構造函數。如果你是從一個框架將數組傳送到另一個框架,那麼就要另外檢查是否存在 sort() 方法。
如果看到了與 null 比較的代碼,嘗試使用以下技術替換:
  • 如果值應爲一個引用類型,使用 instanceof 操作符檢查其構造函數;
  • 如果值應爲一個基本類型,使用 typeof 檢查其類型;
  • 如果是希望對象包含某個特定的方法名,則使用 typeof 操作符確保指定名字的方法存在於對象上。
代碼中的 null 比較越少,就越容易確定代碼的目的,並消除不必要的錯誤。
4.使用常量
儘管 JavaScript 沒有常量的正是概念,但它還是很有用的。這種將數據從應用邏輯分離出來的思想,可以在不冒引入錯誤的風險的同時,就改變數據。請看以下例子:
function validate(value){
if (!value) {
alert("Invalid value!");
location.href = "/errors/invalid.php";
}
}
在這個函數中有兩端數據:要顯示給用戶的信息以及 URL。顯示在用戶界面上的字符串應該以允許進行語音國際化的方式抽取出來。URL 也應被抽取出來,因爲它們有隨着應用成長而改變的傾向。基本上,有着可能由於這樣那樣原因變化的這些數據,那麼都會需要找到函數並在其中修改代碼。而每次修改應用邏輯的代碼,都可能會引入錯誤。可以通過將數據抽取出來變成單獨定義的常量的方式,將應用邏輯與數據修改隔離開來。請看以下例子:
var Constants = {
INVALID_VALUE_MSG: "Invalid value!",
INVALID_VALUE_URL: "/errors/invalid.php"
};
function validate(value) {
if (!value) {
alert(Constants.INVALID_VALUE_MSG);
location.href = Constants.INVALID_VALUE_URL;
}
}

在這段重寫過的代碼中,消息和 URL 都被定義於 Constants 對象中,然後函數引用這些值。這些設置運行數據在無須接觸使用它的函數的情況下進行變更。Constants 對象甚至可以完全在單獨的文件中進行定義,同時該文件可以由包含正確值的其他過程根據國際化設置來生成。

關鍵在於數據和使用它的邏輯進行分離。要注意的值的類型如下所示。

  • 重複值 -- 任何在多處用到的值都應抽取爲一個變量。這就限制了當一個值變了而另一個沒變的時候會造成的錯誤。這也包含了 CSS 類名。
  • 用戶界面字符串 -- 任何用於顯示給用戶的字符串,都應該被抽取出來以方便國際化。
  • URLs -- 在 Web 應用中,資源位置很容易變更,所以推薦用一個公共地方存放所有的 URL。
  • 任意可能會更改的值 -- 每當你在用到字面量值的時候,你都要問一下自己這個值在未來是不是會變化。如果答案是 "是" ,那麼這個值就應該被提取出來作爲一個常量。
對於企業級的 JavaScript 開發而言,使用常量是非常重要的技巧,因爲它能讓代碼更容易維護,並且在數據更改的同時保護代碼。

發佈了0 篇原創文章 · 獲贊 1 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章