11月阅读周·编写可测试的JavaScript代码:复杂度之耦合篇
背景
去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。
没有计划的阅读,收效甚微。
新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。
这个“玩法”虽然常见且板正,但是有效,已经坚持阅读十个月。
已读完书籍:《架构简洁之道》、《深入浅出的Node.js》、《你不知道的JavaScript(上卷)》、《你不知道的JavaScript(中卷)》、《你不知道的JavaScript(下卷)》、《数据结构与算法JavaScript描述》、《WebKit技术内幕》、《前端架构:从入门到微前端》、《秒懂算法:用常识解读数据结构与算法》、《JavaScript权威指南》、《JavaScript异步编程设计快速响应的网络应用》。
当前阅读周书籍:《编写可测试的JavaScript代码》。
耦合
在计算一个模块或函数所依赖模块和对象的扇出数量时,耦合则是关注这些依赖模块是如何组合在一起的。子模块或许可以减少扇出计数,但它们不会减少原始模块和最初依赖之间的耦合度。原始模块仍然需要依赖,只不过依赖方式变成了间接方式,而不是显式依赖。
一些指标试图尝试用一个数字计算耦合度。这些指标基于Norman Fenton和Shari Lawrence Pfleeger在1996年发表的Software Metrics: A Rigorous & Practical Approach, 2nd Edition(Course Technology)中定义的六级耦合。每一级给定一个分数,分数越高耦合越紧。
内容耦合
内容耦合是最紧的耦合形式,包括在外部对象上调用方法或函数,或通过修改外部对象的属性直接改变对象状态。下面任何一个例子,与外部对象O之间都是一种内容耦合:
o.property = 'blah'; // changing O's state directly
// Changing O's internals
o.method = function () {
/* something else */
};
// changing all os!
o.prototype.method = function () {
/* switcheroo*/
};
所有这些语句与对象O都是内容耦合,这种类型的耦合分数为5。
公共耦合
在耦合类型中,略低一点的耦合是公共耦合。如果两个对象都共享另外一个全局变量,则这两个对象就有公共耦合了。
var Global = 'global';
function A() {
Global = 'A';
}
function B() {
Global = 'B';
}
在这里,对象A和对象B就是公共耦合,这种类型的耦合分数为4。
控制耦合
接下来的是控制耦合,比公共耦合的耦合度稍低一些。该耦合基于标记或参数设置来控制外部对象。例如,在代码开头,创建一个单例抽象工厂,接着传入一个env标记告知该抽象工厂如何操作,这种做法就是控制耦合的一个形式:
var absFactory = new AbstractFactory({ env: 'TEST' });
印记耦合
印记耦合是通过向外部对象传递一个记录,而只使用该记录的一部分,示例如下:
// this object is stamp coupled to O
O.makebread({ type: wheat, size: 99, name: 'foo' });
// Elsewhere within the definition of O:
O.prototype.makeBread = function (args) {
return new Bread(args.type, args.size);
};
上述代码,向makeBread函数传递一个记录,但该函数只使用了该条记录三个属性中的两个,这就是印记耦合。印记耦合的分数为2。
数据耦合
印记耦合是通过向外部对象传递一个记录,而只使用该记录的一部分,示例如下:
耦合类型中最松散的耦合是数据耦合。这种类型的耦合发生在一个对象传递给另一个对象消息数据,而没有传递控制外部对象的参数时。数据耦合的分数仅为1。
无耦合
最后一种耦合形式是任意两个对象之间的绝对零耦合,这就是著名的“无耦合”。这种类型的耦合分数为完美的0分。
实例化
虽然不是正式耦合的一部分,实例化一个非单例全局对象的行为也是一种非常紧密的耦合,其耦合程度接近于内容耦合,但比公共耦合紧密。使用new或Object.create创建的对象是单向的,且对象之间的关系是紧密耦合的。构造器对对象的处理方式决定着其是否为双向关系。
实例化一个对象,让代码负责对象的生命周期。具体来说,对于刚刚创建的对象,代码后期也要负责销毁它。在函数结束或关闭时,虽然该对象可以脱离作用域,但该对象依赖的其他所有资源和依赖项仍然会占用内存——甚至被执行。实例化一个对象时,必须要知道赋予对象创造者的责任。当然,实例化的对象越少,思路就会越清晰。最小化的对象实例化就会有最小化的代码复杂度;这是一个值得追求的好目标。如果我们发现自己正在创建很多对象,那么是时候后退一步,重新考虑一下我们的架构了。
耦合性度量
对于每种类型的耦合进行命名和得分计算的要点,除了给大家一个共同的参考框架以外,还要基于函数、对象、模块来生成耦合性度量。早期计算两个模块或对象之间的耦合性度量时,只是添加模块或对象之间互连的数目,计算最大耦合得分而已。
需要指出的是,一个或一组数字可以决定一个系统或一组模块的耦合松散程度。这意味着系统之上的人试图确定其状态。对此,我们就是每天看这些东西的程序员。一旦知道要找什么,我们就可以找到它,必要时对此进行重构。再重述一下,代码检查和代码审查是查找代码耦合的一个非常好的方法,而不是依靠工具来发现耦合性度量。
现实中的耦合
让我们看看一些JavaScript中的耦合例子。理解松耦合的最好方式是了解紧耦合:
function setTable() {
var cloth = new Tablecloth(),
dishes = new Dishes();
this.placeTableCloth(cloth);
this.placeDishes(dishes);
}
上述辅助方法,可能属于Table类,其试图更好地设置table。然而,这种方式将会导致Table类与TableCloth和Dishes这两个对象产生紧耦合(tightly coupled)。使用这两个对象创建新对象导致了紧耦合。
由于受紧耦合影响,这种方法不具独立性——测试时,还必须得准备TableCloth和Dishes对象。单元测试确实是要独立于外部依赖而测试设置方法(settable method),但上述代码将导致测试变得困难。
在上述烹饪示例中,我们可以看到,全局模拟TableCloth和Dishes对象是很困难的。这使得测试更加困难,尽管利用JavaScript的动态特性是有可能的。稍后我们将会看到,桩(stub)与模(mock)可以进行动态注入,以便处理该问题。然而要做到可维护,这种情况并不是很理想。
可以从静态类型语言中借鉴一些想法,像如下示例这样使用注入,从而达到松耦合:
function setTable(cloth, dishes) {
this.placeTableCloth(cloth);
this.placeDishes(dishes);
}
此时的测试就会变得更简单,因为我们的测试代码可以直接向方法传递桩与模参数。这样的方法更容易隔离,因此更容易测试。
然而,在某些情况下,这种方法只会使问题更加严重。我们需要在某个地方、某个时机进行这些参数对象的实例化工作。难道这些方法不会导致紧耦合吗?
一般来说,初始化对象尽量在程序调用堆栈之前进行。如下示例,展示了该方法在应用程序中是如何被调用的:
function dinnerParty(guests) {
var table = new Table(),
invitations = new Invitations(),
food = new Ingredients(),
chef = new Chef(),
staff = new Staff(),
cloth = new FancyTableClothWithFringes(),
dishes = new ChinaWithBlueBorders(),
dinner;
invitations.invite(guests);
table.setTable(cloth, dishes);
dinner = chef.cook(ingredients);
staff.serve(dinner);
}
工厂允许我们在底层堆栈调用时实例化对象,但依然保持松耦合。我们现在只能依赖于工厂,而不是显式地依赖于实际对象。工厂依赖于其所创建的对象,然而,为了进行测试,我们引入工厂来创建桩与模对象,而不是真实的对象。沿着这个思路走到最后的结果是:抽象工厂。
这里我们要做的是参数化一个工厂,以便返回一个能够生成TableCloth对象的实际工厂,我们称之为抽象工厂。为了进行测试,我们只需要一个mock对象;而其他的情况,则是要得到真实的对象。
现在,没有和对象紧耦合,也有办法实例化对象了。我们已经从非常紧的内容耦合转移到了宽松得多的控制耦合了(两个耦合度的规模,很棒!)。现在测试变得更简单了,我们可以创建测试版本的mock工厂了,而不是“真实”版本,这使得我们不用担心代码的任何依赖项,就能够测试我们的代码了。
总结
根据不同耦合度的代码,测试的类型和所需要的测试数量是有关系的。不必感到惊讶的是,代码耦合越紧,需要测试的资源就越多,让我们深入分析一下。
内容耦合的代码很难测试,是因为单元测试要在隔离环境中进行测试,但是根据定义,内容耦合代码至少与一个其他外部对象紧密耦合。需要利用所有的单元测试技巧来测试这段代码;通常必须使用模对象和桩对象来模拟代码所需要的运行环境。由于存在紧密耦合,集成测试也是必要的,以确保多个对象在一起可以正常工作。
公共耦合代码更易于进行单元测试,因为其共享的全局变量可以很容易地模拟(mock或stub)并检查,以确保被测对象可以正常地读取、写入,并对变量做出正确的响应。
控制耦合代码需要模拟出外部控制对象,并验证其可以正常控制,这种测试使用mock对象很容易完成。
印记耦合代码也可以通过外部mock对象,很容易地进行单元测试,并验证传入的参数是否正确。
数据耦合代码和无耦合代码很容易利用单元测试进行测试。很少或者不需要模拟对象(mock或stub),并且该方法可以直接进行测试。
将耦合最小化,测试将会变得更容易。
作者介绍
非职业「传道授业解惑」的开发者叶一一。
《趣学前端》、《CSS畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏⭐️ | 留言📝。
- 点赞
- 收藏
- 关注作者
评论(0)