第20章 最佳实践 (二)

来源:互联网 发布:视频交友软件聊天 编辑:程序博客网 时间:2024/05/06 13:56
 

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" onclick="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 开发而言,使用常量是非常重要的技巧,因为它能让代码更容易维护,并且在数据更改的同时保护代码。

原创粉丝点击