Pages

Wednesday, October 26, 2011

关注性能: 确定更改的风险 耦合度量如何影响代码的质量

Jack Shirazi (jack@JavaPerformanceTuning.com), 主管, JavaPerformanceTuning.com
Kirk Pepperdine (kirk@JavaPerformanceTuning.com), 技术总监, JavaPerformanceTuning.com
简介: 在性能调优时,不可避免地会在应用程序中产生一些 bug,这些 bug 可能会让团队无法继续前进,而且可能显著地影响项目的进度。如果计划很紧(它们什么时候不紧呢?),那么性能调优工作很有可能会使项目落后、延期甚至取消。幸运的是,软件度量(software metrics)可以提供帮助。问题是:如何管理一个合理的时间框架,使系统摆脱已知的瓶颈?假定您理解改进性能需要的所有更改,那么该问题的答案取决于及时进行更改的能力。当代码中遇到未预料到的问题时,在工作过程中,必需进行的改变或者需要考虑的改变的数量常常会不断增加。您也许认为这是一项不太可能完成的任务,您是对的——精确地计划任何形式的代码重构实际上是不可能的,除非有某种可以对风险进行评估的方法。幸运的是,软件度量(software metrics)可以为您提供帮助。

编写代码时会下留您的指纹。例如,一些开发人员可能决定将所有实例变量声明为 public,而另一些开发人员则可能选择将它们声明为 private。如果搜索代码,并统计遇到多少个声明为 private 、 protected 、 package 和 public 的实例变量,那么您就测量、或者说 度量(metric)了各种选择的流行程度(prevalence )。
假定您已经确定,某一给定应用程序中,声明为 public 的实例变量的流行度为 20%。那么对于代码而言,这个数字说明了什么呢?不幸的是,如果没有其他一些信息,这个数字说明不了什么。不过,也许您已经知道,已经成功部署了的应用程序 一般 只有不到 10% 的实例变量声明为 public 。有了这条额外信息,您现在就可以说,该应用程序 也许有 太多的 public 实例变量了。
那么,在使用像“一般”和“也许有”这样的短语时,您想要表达的是什么呢?在讨论度量时(它是一种人口统计),不能采用绝对的方式。度量值可能表明一种趋势或者模式,但是统计只能提供相互关系,却无法成为因果关系的证据。换句话说,可能有一个 100% 的实例变量都声明为 public 的成功项目,但是,如果标准定在 10% 左右,那么这种声明就是一种最例外的情况。
对软件度量有了更好的理解后,让我们继续讨论它们如何帮助评估重新构建代码的风险。因为重新构建实际上是维护编码的一种形式,维护中遇到的那些麻烦事在重新构建时同样会遇到。在重新构建之后,最常见的随机 bug 是 不当耦合
从最基本形式方面,可以将耦合看成是一个对象需要的所有引用。通过使用 Java 编译器,您可以非常容易地看到耦合的结果。选择一个应用程序、清除其类目录、然后选定一个源代码文件并用命令行编译它。您会看到,在目标目录中出现一组类文件。您可能会说“这又如何?”,假定每一个类都表示原来的类所依赖的一个实体,那么其中任何一个类的改变都会在原来的类中产生一个 bug。
您应该已经理解了编译所涉及的问题,现在,就让我们用一组示例结果来集中讨论耦合。假定有一个简单的系统,它包含 5 个类:A、B、C、D 和 E。表 1 显示了试图编译一个类的结果。
表 1. 编译一个类的结果 
要编译的类被编译的其他类传入耦合传出耦合不稳定因素
AB、C、D、E041
BC、D、E130.75
C-200
DE310.25
ED310.25
首先让我们看一下该表的前两列,来查看一个具体情况——类 C。尽管其他类依赖于类 C 的存在,但是它不依赖于任何其他类。这个结果表明 C 是完全自包含的,并且在应用程序的类依赖图中表示为叶节点。
A 的情况更有趣一些,因为它依赖于系统中的所有其他类,同时没有类依赖于它。除了不依赖于类 A,B 与 A 有同样的依赖特性。D 和 E 彼此依赖,这种依赖关系称为 循环依赖。图 1 表现了可以用来描述表 1 中的信息的耦合图。

图 1. 耦合图
依赖关系图
该图显示了三种不同类型的耦合——直接、间接和循环。A 直接依赖于 B,并间接地依赖于 C、D 和 E。正如已经介绍的,D 和 E 存在循环依赖。
如果某个类依赖于其他类,那么就称它对那个类具有 传入耦合 关系。传入耦合的统计数要回答的问题是:有多少个类依赖于我?如果分析编译 B、C、D 和 E 的结果 (如表 1 所示),那么在 “被编译的其他类” 栏中,您不会找到任何一个包含 A 的项。因此,该编译器表明了 A 没有传入耦合。
与传入耦合相反, 传出耦合定义为特定类所依赖的那些类。它要回答的问题是:我依赖于多少个类?与传入耦合的情况相同,只要统计在可以编译特定类之前需要编译的类,就可以得到答案。例如,编译 A 会使每一个其他类都编译。我们可以看到,在表 1 中,A 没有直接依赖关系,却有三个间接依赖关系。
不稳定因素是传出依赖与所有依赖之间的比率(即传出/[传出 + 传入])。该度量值的范围是从 0 到 1,得分 1 表示最不稳定,得分 0 表示最稳定。得分所测量的是另一个类的改变会对正讨论的类造成不稳定的可能性。因此,某个类的得分为 1 表明应用程序任何地方的改变都会影响该类。这个术语可能有些令人迷惑,所以我们将从不同的角度分析它。
考虑表 1 所示的系统。类 A 的不稳定因素为 1,这意味着系统中的任何改变都可能使 A 不稳定。而且,它的传入耦合数为 0,这表明该类的改变不会反过来影响应用程序的其他部分。另一方面,类 C 的不稳定因素为 0,这意味着系统中的改变对它的实现没有任何影响。不过,它的传入耦合数表明,它的改变很有可能会使应用程序的其他部分不稳定 (或者产生 bug)。换句话说,改变 C 可能会在 B 和/或者 A 中产生 bug。因为 C 和 B 密切相关,所以在 B 中预计也会出现 bug,但是因为 A 和 C 的关系更远 (检查时可能看不出来),在 A 中出现 bug 则会让人感到有些意外。
霰弹式修改(Shotgun surgery)是用来描述进行更改时,在应用程序中随机出现几处 bug 这种情况的术语。通常,当一个对象不恰当地违反了抽象界限或者向其他类开放其内部状态时,就会出现这种未预料到的 bug。这些是不当 (或者紧密) 耦合的例子。
如果编写过一定数量的 Java 代码,那么您很可能遇到过 java.io.Serializable ,这是序列化框架的标志性接口。序列化的一个有趣的地方在于,即使在编写应用程序之前已经编写好框架,该框架仍然有效!更有意思的是,框架经历了您可能从不曾注意的改变。
双重分派 (Double dispatch) 是将序列化代码与应用程序代码分离的设计模式。它的工作原理是这样的:如果将一个对象编写到ObjectOutputStream ,那么它会回调 (用它自己作为参数) 原来的对象。它这样做是在说“我不知道您的内部状态,因此我把编写内部状态的责任回派给您”。然后,该对象将其实例变量编写给流。
用这种方法来回分派可以使 ObjectOutputStream 与域对象合作完成序列化过程,同时还可以维持松散的耦合。接口java.io.Serializable 使这成为可能,它将序列化与域解耦。
让我们回到 图 1,查看 D 与 E 之间存在的循环依赖。如果耦合的目标是维护依赖的单向性,那么由于彼此依赖,D 与 E 违背了这一原则。在某些条件下,这种违背是可以接受的。当同一组件/包中的两个或者多个对象相互合作来实现某些目标时,这种依赖关系会变好,这也是关系变好的最常见的一种情况。我们希望类羞于开放自己、但是勇于命令其他类。所以,如果 D 告诉 E 做某些事,而 E 增加了一些信息并告诉 D 继续这项任务,那么这种依赖性是可以接受的。但是我们可以做得更好!
在 Java 编程中,我们有一个管理一组依赖关系的很好的机制:接口。如果我们引入接口 F,让 D 依赖于它,并让 E 实现它,那么它会打破这两个类之间的循环依赖性。引入这种接口的效果如图 2 所示。

图 2. 打破循环依赖性
依赖图
现在。F 可能需要引用 D,但是这种依赖性并不是那么地不可接受,因为它没有将 E 的表示或者实现绑定到 D 或者相反。让我们重新分析进行这种重新构建之后,不稳定因素发生了怎样的改变。
表 2. 引入接口的耦合效果 
要编译的类被编译的其他类传入耦合传出耦合不稳定因素
AB、C、D、F041
BC、D、F130.75
C-200
DF21.33
EF011
F-400
从度量中可以看到,改变 A、B、C 或者 D 现在不再会影响 E 的实现。而且,改变 F 的实现会对所有依赖它的对象的稳定性产生可怕影响。最后,改变 E 现在也不会对整个框架造成影响。
让我们重提序列化,并试着为示例增加一些真实性。假定在类 A、B、C 和 D 中实现了序列化框架,并且接口 F 是java.io.Serializable 。同时还假定 E 是域对象。在这种情况下,上面所做的分析依然成立。序列化过程的改变不会影响域的稳定性。也就是说, java.io.Serializable 中的改变对所有事物(包括类)都会产生戏剧性的影响。最后,我们可以对 E 做任何事 (除了没有实现接口这一显然步骤之外),而它对序列化框架没有任何影响。
坏的依赖(或增加了代码脆弱性的依赖)是指危及封装或者倒转依赖方向的依赖。例如,考虑当两个或者更多对象开始彼此询问内部状态、而这些对象又不在同一包或组件中时可能出现的后果。在这种情况下,询问者可能问被询问者某些实例变量的值 (通过getXxx 调用)。在某些情况下,该值可以作为其他计算的基础。而在另一些情况下,该值可能会改变。不管是哪种情况,被调用的对象都将被解除责任,而且在后一种情况下,它甚至不能维持其内部状态。
考虑代码片段 obj.getX().getY() 。在该实例中,坏的耦合 (对 getX 的调用) 在将对象的内部状态传递给第三者时变成了丑耦合。在这种情况下,对 Y 的改变会立刻成为 obj 中的 bug。如果这个链足够长或者足够复杂,那么将出现想像不到的各种 bug。
这种形式的耦合的坏处在于,只是假定了调用者对所请求的状态的责任。要对请求的状态负责,就必须了解与该状态相关的所有业务规则。此外,如果排除了包含对象的责任,那么该责任很有可能会被分派到整个应用程序。不管是业务规则、数据发生了改变,还是表示发生了改变,都会迫使您搜索源代码,并按新规则重新实现代码。
考虑以下这种情况,银行决定摆脱讨厌的出纳,想让所有顾客学会他们的业务规则。现在,如果银行需要改变规则,那么它必须搜索人群,找出其顾客,并重新教给他们新的规则。如果他们错过一位顾客,并且这位顾客决定使用这项服务,那么就会出现一些问题。这听起来很荒谬,但在很多代码中,程序员认为从系统中取消 “出纳员”是好主意。
说坏的耦合是好的是不对的,但是有时坏的耦合是可以容忍的。最常见的例子是 GUI 代码。通常,不能将普通度量应用于用户界面代码。
因为性能原因,有时也会需要使用坏的耦合。在这种情况下,应当小心记录做出该决定的原因,以帮助以后使用该代码的用户了解决定不按好的实践编写代码的原因。和平时一样,要对未证实的或者不成熟的优化有所了解。
如果在开始性能调优过程时,就对代码集的复杂性和耦合进行了测量,那么您可以感受在规定的时间框架内完成手头任务的风险。这些数字可以鼓励您继续、促使您要求更多时间完成任务,或者做出取消工作的决定。不管是哪种情况,在着手改进性能时,软件度量都会提供更多的内幕信息。

Jack Shirazi 是 JavaPerformanceTuning.com的董事,也是 Java Performance Tuning, 2nd Edition (O'Reilly)一书的作者。
Kirk Pepperdine 是 JavaPerformanceTuning.com 的技术总监,最近15年来,他主要致力于对象技术和性能调优方面的研究。Kirk 还是 Ant Developer's Handbook (MacMillan)一书的合著者之一。


原文: http://www.ibm.com/developerworks/cn/java/j-perf07304/index.html

No comments:

Post a Comment