诊断Java代码:“杀手组合”—mixin、Jam和单元测试

在 Java 语言中获得单继承编程的安全性需要付出极大的代价:有时必须沿着继承层次结构中的多条路径复制代码。要重新获得单继承 Java 代码中所失去的大多数表示,我们可以将 mixin集成为一个扩展。本月,Eric Allen 解释了 mixin(那些由它们的父类参数化的类)的概念,以及它们如何能协助单元测试。他还描述了基于 mixin 编程的工具,并讨论了将 mixin 添加到您的 Java 代码中的可能方法。

自从面向对象的编程出现以来,OO 语言设计中一直存在着一个困扰人的基本问题。一方面,我们在域分析过程中开发的本质是有意使用从多个父类继承的类。那是因为实际世界中的对象不会刚好适合一个简单的单继承层次结构。您最喜爱的啤酒或许口感既好纯度 又比较高。另一方面,在编程语言中允许多重继承的结果是语义极其复杂。

在语言中引入这样的复杂性往往会使发生错误的概率增加,因此 Java 语言已经坚持采用单继承的方法(接口继承除外,其中的语义要简单得多)。其结果是,Java 程序中的许多类结构要么包含沿着继承层次结构的多个分支复制的代码,要么包含通过使用责任链(Chain of Responsibility)设计模式、命令(Command)设计模式或策略(Strategy)设计模式而添加的各个间接级别。

例如,请考虑下面这个用于 GUI 库可滚动窗格的 UML 分析图示例:

图 1. 选择 GUI 元素的分析图

理想情况下,我们希望将这个图直接转换成 Java 编程中的类层次结构。但是,因为 Java 编程是单继承,所以我们不能这么做。就算多重接口继承允许我们构造对应的接口集,但是实现这些接口的类不能直接遵循该结构。另一种方法是,我们要么必须沿着继承层次结构中的多条路径复制代码,要么使用策略模式(或其它使用限制的一些诀窍)来避免复制代码。这两种方法都不能完全让人满意。

博采众长

但是如果多重继承太容易出错,而单继承又太局限,那么在 Java 编程中是否可以添加一些语言特性,这些语言特性会向我们提供集中这两种方法的优点呢?答案是有的 — 它就是 mixin。

mixin 是那些由它们的父类参数化的类。它们也可以被认为是将类映射到新子类的函数。根据特定上下文的要求,可以用不同的父类实例化 mixin。

例如,如下所示,通过使用 mixin 可以实现图 1 中 ScrollPane 的类层次结构(其中,存在定向的虚线代表从 mixin 到父类的实例化关系):

图 2. mixin 继承图

在图 2 中,我们已经将类 Scrollable 转换成 mixin,它可以继承不同上下文中的不同类。在这个上下文中,我们实例化 Scrollable 以继承 Pane ,来创建 ScrollPane 。我们也可以实例化 Scrollable 以继承 Dialog ,而且我们可以对它实例化以继承不同上下文所需的所有种类的其它 GUI 组件。

mixin 的简史

词语 mixin的初次使用源自 Lisp 社区。它被用于 CLOS 的主流中,实际上它在其中是一种设计模式,尝试控制这种语言的多重继承所带来的不便。mixin 设计模式也已被 C++ 社区用于同样目的。

之所以使用 mixin这个名称,是因为这样的类可以以各种方式与其它类混合在一起。尽管 mixin 只是这些语言中的一种设计模式,但在语言级别上支持它们应该是毫无问题的。对于要将 mixin 添加到 Java 语言,已经提出了许多建议,但迄今为止最受欢迎的建议是使用 Jam,这是一种使用 mixin 的 Java 扩展,它是由意大利研究人员 Davide Ancona、Giovanni Lagorio 和 Elena Zucca 提出的。

mixin、Java 代码和 Jam:不仅仅是为了早餐

Jam 是一种向后兼容的 Java 平台 V1.0 扩展(带有两个新关键字: mixin 和 inherited )。无可否认,除非您正在将 Java 程序改写成 .NET 程序,否则您可以使用这种语言相当旧的版本,但是基本设计可以延用至各个更新的版本。

所提供的实现作为 Jam 到 Java 语言转换程序。注: jamc 实现不执行完整的程序类型检查。与此相反,它转换成 Java 源代码,并依赖 Java 类型检查器来捕获类型错误。这使 Jam 实现更简单,但是这也意味着要诊断从编译器上取回的错误消息会比较困难,因为我们已在实际编写的源代码上删除了这一步骤!最后,独立的 Jam 类型检查器对于生产使用是不可或缺的。

在 Jam 中,使用 mixin 类 def 内的声明来声明父类所需的方法,类似于: inherited 。

mixin 的实例化可以这样编写: class NAME = MIXIN extends CLASS {CONSTRUCTOR*}

CONSTRUCTOR 产品尾部的 * 意味着该产品可以不存在,也可以存在更多。如果在 mixin 实例化中没有指定任何构造器,那么就假定是缺省的不带参数的(zeroary)构造器。

例如,如下所示,编写 UML 图(图 2)中使用的 mixin(其中,我们在 Panes 中包含了 setVisible() 方法,在 mixin Scrollable 中包含了 maxScrollSize 字段):

清单 1. 在 Jam 中实例化 mixin

class Pane {  ...   void setVisible(boolean value) {   ...  }}class DialogBox {  ...}mixin Scrollable {  int maxScrollSize;  inherited void setVisible(boolean value);}class ScrollDialog = Scrollable extends DialogBox {  ScrollDialog() {   this.maxScrollSize = 10;  }}class ScrollPane = Scrollable extends Pane {  ScrollPane(int maxScrollSize) {   this.maxScrollSize = maxScrollSize;  }}

Jam 遵循著名的“用于 mixin 的复制原则”:

通过实例化父类P 上的 mixinM 而获得的类应该具有与P 的一般继承者相同的行为,其主体包含M 中定义的所有组件的副本。

尽管 mixin 的概念已经应用到了许多语言中,但是 Jam 还是很新颖,因为它在严格类型化的语言上下文中严格引入了基于 mixin 的编程。Jam 中的 mixin 与普通类相似,都定义类型;mixin 实例化拥有 mixin 的类型和父类的类型。一个 mixin 可以实现多个接口。

在 mixin 中不能声明构造器,它只适用于 mixin 实例化。就象 Jam 的设计人员所声明的,不允许构造器作为设计选择,因为它们“与它们自己类的实现紧密联系在一起,所以它们的说明往往变得非常不一般了。”

要注意这种语言的一些常规特性:

通过使用与标准 Java 语言所用的相同规则可以访问字段成员。

静态成员与 mixin 的实例化相关联;没有“可共享”的 mixin 静态成员。

另外,Jam 对 mixin 的实例化强加了五个约束:

非法覆盖/隐藏。 如果一个父类相应的“已复制”类合法,那么允许对这个父类上的方法进行意外(也称为“偶然”)覆盖(更确切地说,方法不会拥有和父类中已覆盖的方法相同的 arg类型,而是其它 return类型,或者可以拥有其它 throws子句,或诸如 静态vs. 实例那样不兼容的修饰语)。

不明确的重载。不明确的重载是个问题,因为方法参数可能是 mixin 类型,它允许两个已重载的方法可用并且这两个方法都不是比较特定的情况。如果除了某些参数具有两种不同引用类型以外,这两个方法拥有相同数目和类型的参数,那么通过禁止重载可以解决这个问题。

方法注释。用“Parent”类型来注释被继承的方法。

仅类实例化。只能根据类来实例化 Jam mixin;与 Jiazzi 中的组件不同的是,Jam 中没有 mixin 组合的概念(但是,Jam 团队有意探究这样的扩展)。有关 Jiazzi 和基于组件编程的更多信息,请参阅 参考资料。

不能传递“this”。可能的显示阻塞(show-stopper)将“this”作为参数从 mixin 内部传递给方法或构造器,这是被禁止的!这个 Jam 特性是保护类型系统的稳固性所必不可少的。没有它,就无法确保 Jam mixin 类型将在所有可能的实例化上都是有效的。然而它仍是一个非常遗憾的约束,因为它限制了适合于转换成 mixin 的类集合。

内幕:对实现的简要一瞥

要转换成 Java 语言,Jam mixin 类型就要被表示成接口,这是由实例化来实现的(所有实例化都是静态的)。要处理 mixin 中引入的字段,在接口中引入了 getter/setter 方法: M_$get$_f 和 M_$set$_f 。然后在每个实例化中将 f 声明为字段,并相应实现这两个方法(同样,对 来自外部代码的静态类型 M 的表示进行的所有字段访问都转换成调用 getter/setter)。mixin 中的静态字段不可以在各个 mixin 实例化上共享,因此只能分别将它们插入到每个实例化中。

mixin 的每个实例化被编译成独立的 Java 类;各个副本上不存在任何共享的字节码。还为 mixin 的父类构造了一个接口。这个父类接口是由 mixin 接口继承而来的(而不是由父类的实例化继承而来的)。

mixin 和单元测试

mixin 的每个实例化被编译成独立的 Java 类;各个副本上不存在任何共享的字节码。还为 mixin 的父类构造了一个接口。这个父类接口是由 mixin 接口继承而来的(而不是由父类的实例化继承而来的)。

mixin 一般作为一种重新获得一种语言中多重继承的强大功能,同时不带有任何缺陷的方法来激励程序员。但是很重要的是,要注意它们还向我们提供了测试现有类的新继承的功能强大的方法,特别是当父类的本质是很难对它直接进行测试的时候,此方法很有用(如同在 GUI 元素或 RMI 代理类中)。

事实上,就如同 Jiazzi 向我们提供了在与这些包导入的包无关的情况下测试这些包的方法,Jam(或任何其它基于 mixin 的Java 语言扩展)允许我们在与父类无关的情况下测试这些类,即使那些父类存在于同一个包中。执行清单 3 中的示例,我们可以用 Recorder 为只记录所有超级方法调用的父类实例化我们的 mixin:

清单 2. 与 mixin 实例化无关的情况下测试 mixin

class TestLog {  private StringBuffer recording = new StringBuffer("");  public void record(String message) {   recording.append(message);  }  public String toString() {   return recording.toString();  }}class WidgetRecorder {  public TestLog testLog;  public void setVisible(boolean value) {   testLog.record("setVisible(" + value + "); ");  }}class ScrollableWidgetRecorder = Scrollable extends WidgetRecorder {  public TestScrollable() {   this.maxScrollSize = 10;  }}

随后我们可以根据预期的调用序列检查这个日志:

清单 3. 用于 mixin 的 JUnit TestCase

import junit.framework.*;public class ScrollableTest extends TestCase {  public ScrollableTest(String name) {super(name);}  public void testSetVisible() {   ScrollableWidgetRecorder test = new ScrollableWidgetRecorder();   test.initialize();   assertEquals("Scrollable initialization should've called setVisible(true)",         "setVisible(true); ",         test.testLog.toString())  }  ...}

通过这种方式,我们能够测试本身很难测试的类的继承,而不用考虑这些类的父类位置在哪里。随后难以测试的核心功能被分离成一个小型的父类集合,而依赖该集合的功能就可以在完全通过测试的 mixin 类中轻松得到。

有关 mixin 和类属类型的最后几句话

最后,我疏忽了一点:在讨论 Java 编程中的 mixin 时,至少应该简要讨论一下 mixin 如何与向 Java 添加类属类型的 JSR-14 建议相关联。

因为类属类型允许由类所引用的类型来参数化这些类,Java 语言中对类属类型的真正一流支持必须支持一种 mixin 形式,因为可以定义类以继承类型变量。

遗憾的是,Sun 的 JSR-14 原型编译器所用的方法禁止这样的“一流”继承,因为在静态编译过程中会擦除类属类型;即使在运行时也不存在任何类属类型信息。在 mixin 情形中,这意味着会根据类型变量的限制而擦除 mixin 的父类,很明显,这不是我们想要的。

与此相反,类属类型的 NextGen 公式(2002 年 12 月 Rice JavaPLT 会发布 beta 测试发行版)在运行时使类属类型信息保持可用。因此可以继承它以支持一流的类属类型,包括 mixin。事实上,在首个 beta 测试发行版之后不久的扩充版本中,应该只包含这样的功能。在 参考资料中可以获得已扩展语言的设计。

正如本文及上一篇专栏文章所演示的,当前的 Java 语言不是语言设计的终结者,特别是当我们使用测试优先的编程风格时。还存在许多功能强大的、自然语言的扩展,它们允许我们更迅速更全面地测试程序。

尽管如此,但令人高兴的是,这两篇文章都说明了 Java 语言所提供的巨大灵活性和可扩展性。这个扩展性是该语言和 JVM 设计的安全性和可移植性直接带来的结果。因为最初的设计人员很有远见,所以 Java 语言将会证明,在将来很长一段时间内,它会保持是一种功能非常强大的、有意义的语言,当程序员构建日益复杂的应用程序时,它继续向程序员提供服务。

启程了,人的智慧才得以发挥。

诊断Java代码:“杀手组合”—mixin、Jam和单元测试

相关文章:

你感兴趣的文章:

标签云: