关于特征和行为(代码学习第四天)
著名科学家、研究学者艾萨克.牛顿爵士有这样一句名言:“如果说我看得比别人远一些,那是因为我站在巨人的肩膀上”。作为一名热心的历史和政治学家,我想对这位伟人的名言略加修改:“如果说我看得比别人远一些,那是因为我站在历史的肩膀上”。而这句话又体现出另一位历史学家 George Santayana 的名言:“忘记历史必将重蹈覆辙”。换句话说,如果我们不能回顾历史,从过去的错误(包括我们自己过去的经验)中吸取教训,就没有机会做出改进。
您可能会疑惑,这样的哲学与 Scala 有什么关系?继承就是我们要讨论的内容之一。考虑这样一个事实,Java 语言的创建已经是近 20 年前的事情,当时是 “面向对象” 的全盛时期。它设计用于模仿当时的主流语言 C++,尝试将使用这种语言的开发人员吸引到 Java 平台上来。毫无疑问,在当时看来,这样的决策是明智而且必要的,但回顾一下,就会发现其中有些地方并不像创建者设想的那样有益。
例如,在二十年前,对于 Java 语言的创建者来说,反映 C++ 风格的私有继承和多重继承是必要的。自那之后,许多 Java 开发人开始为这些决策而后悔。在这一期的 Scala 指南中,我回顾了 Java 语言中多重继承和私有继承的历史。随后,您将看到 Scala 是怎样改写了历史,为所有人带来更大收益。
C++ 和 Java 语言中的继承
历史是人们愿意记录下来的事实。
—拿破仑.波拿巴
从事 C++ 工作的人们能够回忆起,私有继承是从基类中获取行为的一种方法,不必显式地接受 IS-A 关系。将基类标记为 “私有” 允许派生类从该基类继承而来,而无需实际成为 一个基类。但对自身的私有继承是未得到广泛应用的特性之一。继承一个基类而无法将它向下或向上转换到基类的理念是不明智的。
关于本系列Ted Neward 将和您一起深入探讨 Scala 编程语言。在这个新的 developerWorks 系列 中,您将深入了解 Sacla,并在实践中看到 Scala 的语言功能。进行比较时,Scala 代码和 Java 代码将放在一起展示,但(您将发现)Scala 中的许多内容与您在 Java 编程中发现的任何内容都没有直接关联,而这正是 Scala 的魅力所在!如果用 Java 代码就能够实现的话,又何必再学习 Scala 呢?
.另一方面,多重继承往往被视为面向对象编程的必备要素。在建模交通工具的层次结构时, SeaPlane 无疑需要继承 Boat(使用其 startEngine() 和 sail() 方法)以及 Plane(使用其 startEngine() 和 fly() 方法)。SeaPlane 既是 Boat,也是 Plane,难道不是吗?
无论如何,这是在 C++ 鼎盛时期的想法。在快速转向 Java 语言时,我们认为多重继承与私有继承一样存在缺陷。所有 Java 开发人员都会告诉您,SeaPlane 应该继承 Floatable 和 Flyable 接口(或许还包括 EnginePowered 接口或基类)。继承接口意味着能够实现该类需要的所有方法,而不会遇到 虚拟多重继承 的难题(遇到这种难题时,要弄清楚在调用 SeaPlane 的 startEngine() 方法时应调用哪个基类的 startEngine())。
遗憾的是,彻底放弃私有继承和多重继承会使我们在代码重用方面付出昂贵的代价。Java 开发人员可能会因从虚拟多重继承中解放出来而高兴,但代价是程序员往往要完成辛苦而易于出错的工作。
--------------------------------------------
回页首
回顾可重用行为
事情大致可以分为可能永远不会发生的和不重要的。
—William Ralph Inge
JavaBeans 规范是 Java 平台的基础,它带来了众多 Java 生态系统作为依据的 POJO。我们都明白一点,Java 代码中的属性由 get()/set() 对管理,如清单 1 所示:
清单 1. Person POJO
//This is Java
注意,如何使用清单 5 中的 object 实现将静态方法注册为监听器 — 而在 Java 代码中,除非显式创建并实例化 Singleton 类,否则永远无法实现。这进一步证明了一个理论:Scala 从 Java 开发的历史 痛苦 中吸取了教训。
Person 的下一步是提供 addPropertyChangeListener() 方法,并在属性更改时对各监听器触发 propertyChange() 方法调用。在 Scala 中,以可重用的方式完成此任务与定义和使用特征一样简单,如清单 6 所示。我将此特征称为 BoundPropertyBean,因为在 JavaBeans 规范中,“已通知” 的属性称为绑定属性。
清单 6. 神圣的行为重用!
//This is Scala
在编译后,简单查看 Person 定义即可发现它有公共方法 addPropertyChangeListener()、removePropertyChangeListener() 和 firePropertyChange(),就像 Java 版本的 Person 一样。实际上,Scala 的 Person 版本仅通过一行附加的代码即获得了这些新方法:类声明中的 with 子句将 Person 类标记为继承 BoundPropertyBean 特征。
遗憾的是,我还没有完全实现;Person 类现在支持接收、移除和通知监听器,但 Scala 为 firstName 成员生成的默认方法并没有利用它们。同样遗憾的是,这样编写的 Scala 没有很好的注释以自动地 生成利用 PropertyChangeSupport 实例的 get/set 方法,因此我必须自行编写,如清单 8 所示:
清单 8. Scala 的 Person POJO,第 3 种形式
//This is Scala
应该具备的出色特征
特征不是一种函数编程 概念,而是十多年来反思对象编程的结果。实际上,您很有可能正在简单的 Scala 程序中使用以下特征,只是没有意识到而已:
清单 9. 再见,糟糕的 main()!
//This is Scala
嵌套类 StudentImpl 是 Student 特征的实现,因而提供了必需的 get()/set() 方法对。切记,尽管特征可以具有行为,但它根据 JVM 作为接口建模这一事实意味着尝试实例化特征将产生错误 —— 表明 Student 是抽象的。
当然,这个简单示例的目的在于编写出一个 Java 应用程序,使之可以利用这些由 Scala 创建的新对象:
清单 17. 学生 Neo
//This is Javapublic class App{ public static void main(String[] args) { Student s = StudentFactory.getStudent("Neo", "Anderson"); s.teach("Kung fu"); }}
运行此代码,您将看到:“I know Kung fu”。(我知道,我们经过了漫长的设置过程,只是得到了一部廉价电影的推介)。
--------------------------------------------
回页首
结束语
人们不喜欢思考。思考总是要得出结论。而结论并非总是令人愉快。
— Helen Keller
特征提供了在 Scala 中分类和定义的强大机制,目的在于定义一种接口,供客户端使用,按照 传统 Java 接口的形式定义;同时提供一种机制,根据特征内定义的其他行为来继承行为。或许我们需要的是一种全新的继承术语,用于 描述特征和实现类之间的关系。