最后讨论一下无法继承多种类的装配问题。该问题的原因在于Java不允许多层继承。
Java不能进行多层继承。这一点大多经常作为其优点而不是缺点而被提出来。C++允许多层继承,但结果却致使程序变得非常复杂,而且还产生了难以理解的错误。
多层继承在装配时会很复杂
多层继承会在如下几个方面导致混乱。第一是名称的冲突。在继承具有相同名称的其他类时,不知道是哪个类的方法将会被调用(图5)。这一点是在一个类继承两个已经继承了某一个类的类时经常发生的问题。这种继承称为菱形继承(图6)。在分别改写(override)最上层的类的方法后,就会引起被最下层的类所调用的问题。
图5●在需要继承两个具有相同名称的方法的超级类时,其名称就会发生冲突。这一点是经常被作为多层继承的问题而指出来的地方
图6●菱形继承的一个例子。在同一个人具有社会人和学生两种性质的时候,就必然会引起此类问题
第二,类的层次也会变得很复杂。如果只有单继承,类的结构仅仅只会分支成树状。而如果是多层继承,一下子就会变得很复杂。如果多次反复地进行多层继承,掌握类的层次关系几乎就将变得不可能。
由于Java只允许单继承,因此就不会引起这种问题。不过,用户当然也希望通过将多个不同的类组合起来,生成新的类。为了解决这一问题,Java就提供了被称为接口的多层继承的功能。
日本产业技术综合研究所信息处理研究部的主任研究员一杉裕志说:“Java只允许单继承从编程语言的装配的角度来讲是一种很不错的作法”。甚至有人表示:“Java利用接口来取代多层继承给人的感觉非常不错。此前Java之所以如此普及就是因为Java的语言标准采取了适当的折衷”(东京大学研究生院处理理工学系研究专业专门从事计算机科学的萩谷昌己教授)。
接口不能继承装配
图7●Java只允许继承一个装配。因此就必须完整地复制代码
不过,基于接口的伪多层继承存在致命的缺点。这就是指接口无法继承装配。Ruby语言的开发者--日本的松本幸弘指出:“使用接口取代多层继承是Java的高明之处。不过放弃装配的继承则令人非常痛心”。
继承2个类的功能时,Java将会复制某一个类的源代码。结果,相同的代码就会分散在各种不同的位置,可维护性就会显著降低(图7)。将相同的处理过程集中到名为类的单位中来提高面向对象的优点没有得到充分的利用。“从市场销售上来讲,Java是一种取得了巨大成功的语言。但是从编程语言的装配角度来看,我认为它是一个失败的作品”(松本)。
Ruby语言利用Mix-in实现装配的继承
实际上松本开发的Ruby语言也和Java一样只允许单继承。不过,Ruby则利用相当于Java接口的方法实现了装配的继承。这就是被称为Mix-in的方法。
Mix-in是指仅将程序中具有再利用性的功能部分集中到被称为“模块”的单位中,以供其他的类来使用。模块与类一样,其本身无法利用new运算符来生成。只有“include”即嵌入到其他的类中才能使用(图)。
LIST 6就是使用了Mix-in方法的简单Ruby代码。定义了一个可以输出“test”字符串的、名为TestModule的模块。TestClass类嵌入了该模块。这样一来,TestClass类就可以像调用自身的方法一样调用TestModule模块中的方法。
不过,Ruby的Mix-in所解决的只是由多层继承所造成的继承关系的复杂性。在多个模块存在同名的方法时,就会产生与多层继承相同的问题。Ruby优先处理由后面嵌入的方法(注3)。
图8●Ruby中的Mix-in功能。将集中了各种功能的模块嵌入到类中来使用
LIST 6●使用Ruby中Mix-in功能的代码。TestClass类可以像使用自己的方法一样使用TestModule模块所装配的方法
利用与类不同的单位进行程序再利用的MixJuice语言
目前一种利用与Ruby不同的方法来提高程序的再利用性的名为MixJuice的语言也在开发之中。这是一种基于Java的独立语言。
LIST 7●MixJuice的代码。程序以“module(模块)”为单位进行描述。而类则由module来分割
为了程序的再利用,即便MixJuice也使用了与类不同的编程单位。这种单位与Ruby一样被称为“模块”。MixJuice语言的开发者--日本产业技术综合研究所的一杉表示:“在基于Java等语言中常见的类的程序设计中,通过多个类的协作实现某一种功能时,类就无法进行再利用。因此应该有一个与类不同的、可以再利用的编程单位”。
MixJuice语言中当然也有类。但是编程时所处理的并不是一个类的源代码,而是以包含了各种可协调工作的类的“模块”为单位来描述程序。重新利用模块进行扩展时,描述的是模块之间的“区别”。
LIST 7就是实际的源代码。程序是以“模块”为单位来组织的。类则在模块中使用“define”关键词进行定义。如本例所示,通过追加模块来描述与现有类的区别。把使用多个类而实现的功能集中到了一起。
必然引起装配缺陷
不过,这种方法必然会产生其他问题。就是说要使用MixJuice中的模块来扩展类时,就会产生装配缺陷。
图9就是一个具体的例子。模块m1和m2均继承了模块m,m1定义了在m中定义的类S的派生类B。而m2则向S类追加了方法。如果是普通的方法,就不会产生问题,但是这里却定义了一个利用派生类来强制装配的抽象方法。实际上,它的派生类A添写了装配。
如要同时使用这两个模块,连接时就会产生错误。因为类B没有装配由m2追加到类S中的方法m()。为了解决这个问题,无论是谁拥有多么丰富的知识都必须对方法进行装配。这种问题是因模块具有方法追加和子类(派生类)追加等2种扩展的方向性而引起的(图10)。
图9●装配缺陷的例子。由m2追加到类S中的抽象方法的存在与m1没有任何关系。如果同时使用这2个模块时就会产生编译错误
图10●存在二个以上的扩展方向性时就会产生装配缺陷。拿MixJuice来说,就是子类和方法的扩展性
提出了新的分割单位的、面向侧面编程(Aspect-Oriented programming)
像MixJuice语言那样,利用横跨多个对象的单位来把握系统的观点称为“关注分隔(Separation of Concerns,SoC)”。类的相互作用也属于关注的一种。这种观点并不是仅仅单纯以对象为单位,还要由其他侧面来分割系统。
最近这种观点已经开始受到越来越广泛的关注。基于该观点的代表性编程范式(paradigm)就是面向侧面编程。它就是以“关注”为分隔单位的。如果存在多个与类相同的关注,就通过将这些关注组合起来,实现一种功能。这种功能就是侧面(Aspect)。
图11就是具体的例子。把移动图形来刷新画面的处理过程定义为一个侧面。图形是由点和线组成的。每一个点和线中均存在移动的方法和用于设置位置的方法。这些动作相互协作,就可实现称为画面刷新的侧面。
LIST 8就是使用基于Java的面向侧面语言“AspectJ”,来描述侧面的源代码。在一个被称为移动图形的程序段中定义了多个类的多个方法。图形的移动结束后,最后执行重新刷新画面的处理过程。
图11●利用“关注”分割“类”的图示。点和线以及他们各自所具有的定位和移动的方法被划分成了相同的关注。该图摘自面向侧面编程的倡导者Gregor Kiczales在面向侧面编程技术研讨会上发表的演讲资料
LIST 8●利用AspectJ描述侧面的源代码。将用于实现图像移动的各种类中的方法归纳为“move”。“move”结束以后,执行由“after”描述的画面刷新。摘自Gregor Kiczales在面向侧面编程技术研讨会上发表的演讲资料
与面向对象并不矛盾
面向侧面编程在对象以外导入了分割系统的单位,提高了程序的再利用性和扩展性。如果只有对象单位有分割的轴,也许确实就会存在一定的限制吧。
不过,面向对象本身就是公认难度很高的编程范式。尽管如此面向对象语言能够普及到如此程序恐怕是因为“只有对象单位有分割的轴”吧。正因为轴只有一个好歹才能够理解。如果还要加上被称为侧面的轴,也许有很多程序员就会产生混乱。
另外,面向侧面则是自由度很高的编程范式。正是因为自由度高,程序员才会感到苦恼。因为他们不知道应该如何分割或组织程序。也就是说,在某种程度上缩小自由度,并给程序员提供编程指南也可以说是编程语言的任务。
面向侧面是一种扩展了面向对象的编程范式。如果认为面向对象的自由度合适,就可以继续使用它。就连面向侧面的倡导者加拿大英属哥伦比亚大学的Gregor Kiczales本人也表示:“没有必要非要去勉强地使用侧面来编程”。程序员要根据自己的开发风格,在追求一种易用性和扩展性适当平衡的同时,使用这种编程范式。