中国领先的IT技术网站
|
|

Scala编程指南 了解Traits功能

本文为《Scala编程指南》的第四部分,将介绍Traits功能。Scala 是一种基于JVM,集合了面向对象编程和函数式编程优点的高级程序设计语言。

作者:bbsmrdj来源:51CTO|2010-10-14 13:50

Tech Neo技术沙龙 | 11月25号,九州云/ZStack与您一起探讨云时代网络边界管理实践


近日,Scala的创始人发表了“Scala is for good programmers(Scala语言是给专家级程序员的)”这样的言论!Scala在现在这个阶段并不需要适合一般的Java程序员。要吸引的是一些专家级的程序员——优秀的程序员。目标是使他们工作起来比使用Java更有效率。只有会出现足够多的教育示范材料和足够好的开发工具时,Scala对广大的普通开发人员才具有吸引力。但是Scala语言什么呢?

Scala编程指南》系列文章将会详细介绍Scala语言。本文为《Scala编程指南》系列的第四章,将介绍Traits功能。

Traits 介绍

在我们深入面向对象编程之前,我们还需要了解Scala 一个特性:Traits。要了解这个功能需要一点历史知识。

在Java 中,一个类可以实现任意数量的接口。这个模型在声明一个类实现多个抽象的时候非常有用。不幸的是,它也有一个主要缺点。

对于许多接口,大多数功能都可以用对于所有使用这个接口的类都有效的“样板”代码来实现。Java 没有提供一个内置机制来定义和使用这些可重用代码。相反的,Java 程序员必须使用一个特别的转换来重用一个已知接口的实现。在最坏的情况下,程序员必须复制粘贴同样的代码到不同的类中去。

通常,一个接口的实现拥有和该实例的其它成员无关(正交)的成员。术语mixin (混合)通常被用来指实例中这些专注的,潜在可被重用的,并且可以独立维护的代码。

来看一下下面这个图形用户接口的按钮的代码,它对单击事件使用了回调。

  1. // code-examples/Traits/ui/button-callbacks.scala  
  2. package ui  
  3. class ButtonWithCallbacks(val label: String,  
  4.     val clickedCallbacks: List[() => Unit]) extends Widget {  
  5.  
  6.   require(clickedCallbacks != null, "Callback list can't be null!")  
  7.  
  8.   def this(label: String, clickedCallback: () => Unit) =  
  9.     this(label, List(clickedCallback))  
  10.  
  11.   def this(label: String) = {  
  12.     this(label, Nil)  
  13.     println("Warning: button has no click callbacks!")  
  14.   }  
  15.  
  16.   def click() = {  
  17.     // ... logic to give the appearance of clicking a physical button ...  
  18.     clickedCallbacks.foreach(f => f())  
  19.   }  
  20. }  

这里发生了很多事情。主构造函数接受一个label(标签)参数和一个callbacks(回调)的list(列表),这些回调函数会在按钮的click 方法被调用时被调用。我们会在《第5章 - Scala 基础面向对象编程》中来探索这个类的更多细节。现在,我们希望专注在一个特别的问题上。ButtonWithCallbacks 不仅处理按钮的一些本质行为(比如单击),它同时还通过调用回调函数处理单击事件的通知。这违反了单职原则[Martin2003],这是分隔职能的一种设计方法。我们可以把按钮类的逻辑从回调逻辑中分离出来,这样每一个逻辑组件变得更加简单,更模块化,更可重用化。这个回调逻辑就是mixin 的一个不错例子。

这样的分离在Java 中很难做,即使我们定义了具有回调行为的接口,我们仍然需要在类中集成实现代码,降低模块性。唯一的其它方法则是使用特定的工具比如面向方面编程(Aspect-Oriented Programming,AOP,参见[AOSD]),一个实现是AspectJ,Java 的一个扩展。AOP 主要被设计用来分离在应用程序中重复出现的普遍问题的实现。它设法模块化这些关键点,但是也允许设计良好的和其它关键点行为的“混合”,包括应用程序的核心域逻辑,不管是在编译时还是运行时。

作为混合体的Traits

Scala 提供了完整的混合(mixin)解决方案,称为Traits。在我们的例子里,我们可以定义回调的抽象为一个Trait,就像一个Java 接口一样,但是我们可以实现这些Trait (或者继承的Trait)的抽象。我们可以定义混合了Trait 的类,大致上很像实现Java 的一个接口。不过,在Scala 你甚至可以在我们创建实例的时候混合Traits。也就是说,我们不必首先声明一个混合了所有我们所需要的Trait 的类。所以,Scala Traits 在保留分离关键点的同时给了我们按需整合行为的能力。

如果你来自于Java 编程世界,你可以认为Traits 是有选择地实现了的接口。其它语言提供了类似Trait 的结构,比如Ruby 中的模块(modules)。

让我们对按钮的逻辑来使用Trait,从而分离回调的处理。我们会推广一下我们的实现。回调其实是观察者模式[GOF1995] 的一个特例。所以,让我们创建一个Trait 来实现这个模式,然后用它来处理回调行为。为了简单化,我们从一个单独的计算按钮被按次数的回调开始。

首先,让我们定义一个简单的Button 类。

  1. // code-examples/Traits/ui/button.scala  
  2. package ui  
  3. class Button(val label: String) extends Widget {  
  4.   def click() = {  
  5.     // Logic to give the appearance of clicking a button...  
  6.   }  
  7. }  
  8.  

这里是它的父类,Widget。

  1. // code-examples/Traits/ui/widget.scala  
  2. package ui  
  3. abstract class Widget  
  4.  

管理回调的逻辑(比如,clickedCallbacks 列表)被省略了,两个主要构造函数也是。只有按钮的label 字段和click 方法被保留了下来。这个click 方法现在只关心一个“物理上的” 按钮被单击时候的可见表现。按钮只有一个关心的东西,就是处理作为一个按钮的本质行为。

这里是一个实现了观察者模式逻辑的Trait。

  1. // code-examples/Traits/observer/observer.scala  
  2. package observer  
  3. Trait Subject {  
  4.   type Observer = { def receiveUpdate(subject: Any) }  
  5.   private var observers = List[Observer]()  
  6.   def addObserver(observer:Observer) = observers ::observer 
  7.   def notifyObservers = observers foreach (_.receiveUpdate(this))  
  8. }  
  9.  

除了Trait 关键字,Subject 看起来就像一个普通的类。Subject 定义了所有它声明的成员。Traits 可以声明抽象成员,具体成员,或者两者皆有,就像类所能做的一样(参见《第6章 - Scala 高级面向对象编程》的“重写类和Traits 的成员”章节获取更多信息)。而且,Traits 能包含嵌套的Trait 和类定义,类也能包含嵌套的Trait 定义。

第一行定义了Observer 类型。这是一个结构类型,形式是 { def receiveUpdate(subject:Any) }。结构类型仅制定了子类型必须支持的结构;你可以把它们看作是“匿名”类型。

在这个例子里,这个结构类型由一个有着特定签名的方法定义。任何有这个签名的方法的类型都可以被用作为一个observer(观察者)。我们会在《第12章 - Scala 类型系统》学习更多有关结构类型的内容。如果你想知道为什么我们不把Subject 作为参数,而是Any。我们会在《第13章 - 应用程序设计》的“自我类型注解和抽象类型成员”章节来重习这个问题。

我们所需要注意的最主要的是这样的结构类型如何最小化了Subject Trait 和任何潜在的Trait 用户之间的耦合。

注意

Subject 仍然通过结构类型和Observer 中的方法名称耦合在一起,例如,名为receiveUpdate 的方法。我们有几种方法来省去这剩下的耦合。我们会在《第6章 - Scala 高级面向对象编程》中的“重写抽象类型”章节看到如何做到这一点。

下面,我们声明了一系列观察者。我们定义了一个var,而不是val,因为List 是不可变的。所以我们必须在一个观察者通过addObserver 方法被添加时创建一个新的列表。

我们会在《第7章 - Scala 对象系统》的“Scala 类型结构”章节和《第8章 - Scala 函数式编程》中讨论更多有关List 的细节。现在,注意addObserver 使用了列表的cons “操作符”方法(::)来在一个列表前面加入一个观察者。Scala 编译器会聪明地把下面的语句,

  1. observers ::observer 
  2.  

转换成如下语句,

  1. observerobservers = observer :: observers  
  2.  

注意我们写了observer:: observers,把已存的observers 列表放到了右边。回忆一下,所有的以: 结尾的方法是右绑定的。所以,前一个语句和下面的语句等价。

  1. observersobservers = observers.::(observer)  
  2.  

notifyObservers 方法遍历所有的观察者,使用foreach 方法,然后对每一个观察者调用receiveUpdate 方法。(注意我们使用了“插入”操作符标记法而不是observers.foreach。)我们使用占位符'_' 来缩短下面的表达式,

  1. (obs) => obs.receiveUpdate(this)  
  2.  

为这样的表达式,

  1. _.receiveUpdate(this)  
  2.  

这个表达式实际上是一个“匿名函数”的函数体,在Scala 中称为字面函数。这和其它语言中的Lambda 表达式或类似结构相似。字面函数和闭包相关的概念会在《第8章 - Scala 函数式编程》的“字面函数和闭包”章节中被讨论。

在Java 中,foreach 方法很可能会接受一个接口,你可能会传递一个实现了该接口的类的实例。(比如,典型的Comparable 使用的方法)。

在Scala 中,List[A].foreach 方法期待的参数类型为(A)=>Unit,这是一个函数,接受一个A 类型的参数,而A 标识了列表的元素的类型(在这个例子中是Observer),然后返回Unit(和Java 的void 一样)。

注意

我们在这个例子里选择使用一个var 来表示不可变的观察者的List。我们也可以使用val 和一个可变的类型,比如ListBuffer。这个选择会在一个真实的应用程序中显得更加合理,但是我们希望避免介绍新的类来分散我们的注意。

再一次的,我们从一个小例子里学习了许多Scala 的知识。现在,让我们来用一用我们的Subject Trait。这里有一个ObservableButton,它继承了Button,混合了Subject。

  1. // code-examples/Traits/ui/observable-button.scala  
  2. package ui  
  3. import observer._  
  4. class ObservableButton(name: String) extends Button(name) with Subject {  
  5.   override def click() = {  
  6.     super.click()  
  7.     notifyObservers  
  8.   }  
  9. }  
  10.  

我们从导入observer 包的所有东西开始,使用'_' 通配符。实际上,我们在这个包中只定义了Subject Trait。

新的类使用了with 关键字把Subject Trait 加到类中。ObserverButton 重写了click 方法。使用super 关键字(参见《第6章 - Scala 高级面向对象编程》的“重写抽象和具体方法”章节),它首先调用了“父类”的方法,Button.click,然后它通知观察者。因为新的click 方法重写了Button 的具体实现,必须加上override 关键字。

with 关键字和Java 的对接口使用的implement 关键字类似。你可以是顶任意多的Traits,每一个都必须有with 关键字。

一个类可以继承一个Trait,一个Trait 也可以继承一个类。实际上,我们的Widget 类也可以被声明为一个Trait。

注意

如果你定义一个类使用一个或多个Traits,而它又不继承任何类,你必须对第一个列出的Trait 使用extends 关键字。

如果你不对第一个Trait 使用extends,例如写成这样。

  1. // ERROR:  
  2. class ObservableButton(name: String) with Button(name) with Subject {...}  
  3.  

你会获得如下错误。

  1. ... error: ';' expected but 'with' found.  
  2.        class ObservableButton(name: String) with Button(name) with Subject {...}  
  3.                                             ^  
  4.  

这个错误实际上应该说“with found,but extends expected。”(发现with 关键字,但是期望一个extends。)

要演示这部分代码,让我们从一个观察按钮点击和记录点击数目的类开始。

  1. // code-examples/Traits/ui/button-count-observer.scala  
  2. package ui  
  3. import observer._  
  4. class ButtonCountObserver {  
  5.   var count = 0 
  6.   def receiveUpdate(subject: Any) = count += 1  
  7. }  
  8.  

左后,让我们写一个测试来运用所有的类。我们会使用Specs 库(在《第14章 - Scala 工具,库和IDE 支持》的“Specs” 章节讨论) 来写一个行为驱动(【BDD】)的“规范”来测试组合后的Button 和Subject 类型。

  1. // code-examples/Traits/ui/button-observer-spec.scala  
  2. package ui  
  3. import org.specs._  
  4. import observer._  
  5. object ButtonObserverSpec extends Specification {  
  6.   "A Button Observer" should {  
  7.     "observe button clicks" in {  
  8.       val observableButton = new ObservableButton("Okay")  
  9.       val buttonObserver = new ButtonCountObserver  
  10.       observableButton.addObserver(buttonObserver)  
  11.       for (i <- 1 to 3) observableButton.click()  
  12.       buttonObserver.count mustEqual 3  
  13.     }  
  14.   }  
  15. }  
  16.  

如果你从O'Reilly 网站下载了代码例子,你可以按照README 文件的指示来编译和运行这个章节的例子。specs “目标”的输出应该包含如下的内容。

  1. Specification "ButtonCountObserverSpec"  
  2.   A Button Observer should  
  3.   + observe button clicks  
  4. Total for specification "ButtonCountObserverSpec":  
  5. Finished in 0 second, 10 ms  
  6. 1 example, 1 expectation, 0 failure, 0 error  
  7.  

注意字符串“A Button Observer Should”和“observe button clicks” 对应了例子中的字符串。Specs 的输出提供了一个漂亮的被测试的项目的需求,并假设为这些字符串做了合适的决定。

测试的主题创建了一个“Okay” ObservableButton 和一个ButtonCountObserver,把观察者给了这个button。按钮通过for 循环被按了3次。最后一行要求observer 的计数等于3。如果你习惯使用XUnit 风格的TDD (测试驱动开发)工具,例如JUnit 或者ScalaTest(参见《第14章 - Scala 工具,库和IDE 支持》的“ScalaTest” 章节),那么最后一行等效于下面的JUnit 断言。

  1. assertEquals(3, buttonObserver.count)  
  2.  

注意

Specs 库(参见“Specs” 章节) 和ScalaTest 库(参见“ScalaTest”章节)都支持行为驱动开发[BDD],测试驱动开发[TDD] 的一种风格,强调了测试的“规范”角色。

假设我们只需要一个ObservableButton 实例呢?我们实际上不用声明一个继承Subject 的Button 的子类。我们可以在创建实例的时候加上Trait。

下一个例子展示了一个修订的Specs 文件,它实例化了一个Button,混合了Subject 作为声明的一部分。

  1. // code-examples/Traits/ui/button-observer-anon-spec.scala  
  2. package ui  
  3. import org.specs._  
  4. import observer._  
  5. object ButtonObserverAnonSpec extends Specification {  
  6.   "A Button Observer" should {  
  7.     "observe button clicks" in {  
  8.       val observableButton = new Button("Okay") with Subject {  
  9.         override def click() = {  
  10.           super.click()  
  11.           notifyObservers  
  12.         }  
  13.       }  
  14.       val buttonObserver = new ButtonCountObserver  
  15.       observableButton.addObserver(buttonObserver)  
  16.       for (i <- 1 to 3) observableButton.click()  
  17.       buttonObserver.count mustEqual 3  
  18.     }  
  19.   }  
  20. }  
  21.  

修订过的observableButton 的声明实际上创建了一个匿名类,并且像之前那样重写了click 方法。和在Java 中创建匿名类的主要区别是我们可以在过程中引入Trait。Java 不允许你在实例化一个类的时候实现一个新的接口。

最后,注意一个实例的继承结构会因为混合了继承自其它Traits 的Traits 而变得复杂。我们会在《第7章 - Scala 对象系统》的“对象层次结构的线性化”章节来讨论这些层次结构的细节。

可堆叠Traits

我们可以通过一系列精炼来提高我们工作的可重用性,使得我们可以更容易地同时使用一个以上的Trait,例如,“堆叠”它们。

首先,让我们来引入一个新的Trait,Clickable,一个任意构件响应点击事件的抽象。

  1. // code-examples/Traits/ui2/clickable.scala  
  2. package ui2  
  3. Trait Clickable {  
  4.   def click()  
  5. }  
  6.  

注意

我们由一个新的包,ui2 开始,这样我们可以更容易的区分开下载下来的新旧代码。

Clickable Trait 看上去就像一个Java 接口;它是完全抽象的。它定义了一个单独的抽象的方法,click。因为它没有函数体,所以称之为抽象。如果Clickable 是一个类的话,我们则应该在class 关键字前面加上abstract 关键字。但是这对于Trait 来说不是必须的。

这里是重构过的按钮类,使用了这个Trait。

  1. // code-examples/Traits/ui2/button.scala  
  2. package ui2  
  3. import ui.Widget  
  4. class Button(val label: String) extends Widget with Clickable {  
  5.   def click() = {  
  6.     // Logic to give the appearance of clicking a button...  
  7.   }  
  8. }  
  9.  

这段代码就像Java 实现一个Clickable 接口一样。

当我们在前面定义ObservableButton 的时候(在“混合Traits”章节),我们重写了Button.click 来通知观察者。而我们在声明observableButton 为一个按钮实例的时候,我们直接混合了Subject Trait,它重复了ButtonObserverAnonSpec 的逻辑。让我们来消除这个重复。

当我们开始用这种方式重构代码的时候,我们意识到我们实际上不关心“观察”按钮;我们关心的是“观察”点击。这里是一个单一的专注于观察点击的Trait。

  1. // code-examples/Traits/ui2/observable-clicks.scala  
  2. package ui2  
  3. import observer._  
  4. Trait ObservableClicks extends Clickable with Subject {  
  5.   abstract override def click() = {  
  6.     super.click()  
  7.     notifyObservers  
  8.   }  
  9. }  
  10.  

ObservableClick Trait 继承自Clickable,并且混合了Subject。然后它重写了click 方法,像在“混合Traits”章节重写的方法几乎一样的实现。最重要的区别就是abstract 关键字。

仔细看这个方法。它调用了super.click(),但是这里super 是什么意思?在这里,它看上去只能是声明了但是没有定义click 方法的Clickable,或者Subject,它并没有click 方法。所以,super 的身份还不一定,至少现在还不一定。

实际上,super 会在这个Trait 混入一个定义了具体click 方法的实例的时候被绑定。这样,我们需要在ObservableClicks.click 前加上abstract 关键字来告诉编译器(或者读者)click 还没有被完全实现,即使ObservableClicks.click 有一个函数体。

注意

除了声明抽象类,abstract 关键字只在Trait 的方法有函数体,但是调用了在父类没有具体实现的super 的方法的时候需要。

让我们在Specs 测试中和Button 类以及它的具体click 方法一起使用这个Trait。

  1. // code-examples/Traits/ui2/button-clickable-observer-spec.scala  
  2. package ui2  
  3. import org.specs._  
  4. import observer._  
  5. import ui.ButtonCountObserver  
  6. object ButtonClickableObserverSpec extends Specification {  
  7.   "A Button Observer" should {  
  8.     "observe button clicks" in {  
  9.       val observableButton = new Button("Okay") with ObservableClicks  
  10.       val buttonClickCountObserver = new ButtonCountObserver  
  11.       observableButton.addObserver(buttonClickCountObserver)  
  12.       for (i <- 1 to 3) observableButton.click()  
  13.       buttonClickCountObserver.count mustEqual 3  
  14.     }  
  15.   }  
  16. }  
  17.  

把这段代码和ButtonObserverAnonSpec 比较。我们初始化了一个Button,混入ObservableClicks Trait,但是这次我们不需要重写click 方法。所以,这个Button 的使用者不用惦记着重写一个合适的click。这部分工作已经由ObservableClicks 完成。想要的行为在我们需要的时候被声明性地组合到代码里去。

让我们再来加入一个Trait。JavaBeans 规范[JavaBeanSpec] 有“可否决”事件的概念,是说JavaBean 修改的监听者可以否决修改。让我们来用Trait 来实现一个类似的机制,用来否决一系列的点击。

  1. // code-examples/Traits/ui2/vetoable-clicks.scala  
  2. package ui2  
  3. import observer._  
  4. Trait VetoableClicks extends Clickable {  
  5.   val maxAllowed = 1  // default  
  6.   private var count = 0 
  7.   abstract override def click() = {  
  8.     if (count < maxAllowed) {  
  9.       count += 1  
  10.       super.click()  
  11.     }  
  12.   }  
  13. }  
  14.  
  15.     
  16.  

再一次,我们重写了click 方法。和以前一样,必须声明这个重写是抽象的。允许点击的数目默认值是1。你可能想知道这里的默认是什么?这个字段不是被声明为val 吗? 我们没有声明构造函数用其它值来初始化它。我们会在《第6章 - Scala 高级面向对象编程》的”重写类和Traits 的成员“ 重温这个问题。

这个Trait 还声明了一个count 变量来记录我们观察到的点击。它被定义为private (私有的),所以它对于Trait 外部的域来说是不可见的(参见《第5章 - Scala 基础面向对象编程》的”可见域规则“)。被重写的click 方法会增加count。它只在count 小于等于maxAllowed 数目的时候调用super.click()。

这里是展示ObservableClicks 和VetoableClicks 一起工作的Specs 对象。注意每一个Trait 都需要一个单独的with 关键字,和Java 对于implements 指令只要一个关键字和用逗号隔开名称的实现方式不一样。

  1. // code-examples/Traits/ui2/button-clickable-observer-vetoable-spec.scala  
  2. package ui2  
  3. import org.specs._  
  4. import observer._  
  5. import ui.ButtonCountObserver  
  6. object ButtonClickableObserverVetoableSpec extends Specification {  
  7.   "A Button Observer with Vetoable Clicks" should {  
  8.     "observe only the first button click" in {  
  9.       val observableButton =  
  10.           new Button("Okay") with ObservableClicks with VetoableClicks  
  11.       val buttonClickCountObserver = new ButtonCountObserver  
  12.       observableButton.addObserver(buttonClickCountObserver)  
  13.       for (i <- 1 to 3) observableButton.click()  
  14.       buttonClickCountObserver.count mustEqual 1  
  15.     }  
  16.   }  
  17. }  
  18.  
  19.  
  20.  

观察者的计数应该是1。observableButton 有下面的语句声明,

  1. new Button("Okay") with ObservableClicks with VetoableClicks  
  2.  

我们可以推断VetoableClicks 重写的click 在ObservableClicks 重写的click 之前被调用。大概地讲,因为我们的匿名类没有定义自己的click,所以这个方法会按照声明从右到左开始寻找。实际上比这个更复杂,我们会在《第7章 - Scala 对象系统》的”对象结构的线性化“ 章节讨论。

同时,如果我们把使用Trait 的顺序反过来会发生什么呢?

  1. // code-examples/Traits/ui2/button-vetoable-clickable-observer-spec.scala  
  2. package ui2  
  3. import org.specs._  
  4. import observer._  
  5. import ui.ButtonCountObserver  
  6. object ButtonVetoableClickableObserverSpec extends Specification {  
  7.   "A Vetoable Button with Click Observer" should {  
  8.     "observe all the button clicks, even when some are vetoed" in {  
  9.       val observableButton =  
  10.           new Button("Okay") with VetoableClicks with ObservableClicks  
  11.       val buttonClickCountObserver = new ButtonCountObserver  
  12.       observableButton.addObserver(buttonClickCountObserver)  
  13.       for (i <- 1 to 3) observableButton.click()  
  14.       buttonClickCountObserver.count mustEqual 3  
  15.     }  
  16.   }  
  17. }  
  18.  

现在观察者的计数应该是3。ObservableClicks 现在比VetoableClicks 拥有更高的优先级,所以点击的计数会增加,即使有些点击在接下来的动作中被否决!

所以,为了防止Trait 之间互相影响导致不可预料的后果,声明的顺序很重要。也许另外一个教训是,把对象分拆成太多细密的Traits 可能会值得你代码的执行变得复杂费解。

把你的程序分割成小的,个所有长的Trait 是个创建可重用,可伸缩的抽象和”组件“的强大方式。复杂的行为可以通过声明Trait 的组合来完成。我们会在《第13章 - 应用程序设计》的“可伸缩的抽象”中更多地探索这个概念。

构造Traits

Traits 不支持辅助构造函数,它们也不支持在主构造函数,Trait 的主体里的参数列表。Traits 可以继承类或者其它Trait。然而,因为它们不能给父类的构造函数传递参数(哪怕是字面值),所以Traits 只能继承有无参数的主/副构造函数的类。

然而,不像类,Trait 的主体在每次使用Trait 创建一个实例的时候都会被执行,正如下面的脚本所演示。

  1. // code-examples/Traits/Trait-construction-script.scala  
  2. Trait T1 {  
  3.   println( "  in T1: x = " + x )  
  4.   val x=1 
  5.   println( "  in T1: x = " + x )  
  6. }  
  7. Trait T2 {  
  8.   println( "  in T2: y = " + y )  
  9.   val y="T2" 
  10.   println( "  in T2: y = " + y )  
  11. }  
  12. class Base12 {  
  13.   println( "  in Base12: b = " + b )  
  14.   val b="Base12" 
  15.   println( "  in Base12: b = " + b )  
  16. }  
  17. class C12 extends Base12 with T1 with T2 {  
  18.   println( "  in C12: c = " + c )  
  19.   val c="C12" 
  20.   println( "  in C12: c = " + c )  
  21. }  
  22. println( "Creating C12:" )  
  23. new C12println( "After Creating C12" )  
  24.  

用scala 命令运行这段脚本会得到以下输出。

  1. Creating C12:  
  2.   in Base12: b = null 
  3.   in Base12: b = Base12 
  4.   in T1: x = 0 
  5.   in T1: x = 1 
  6.   in T2: y = null 
  7.   in T2: y = T2 
  8.   in C12: c = null 
  9.   in C12: c = C12 
  10. After Creating C12  
  11.  
  12.     
  13.  

注意类和Trait 构造函数的调用顺序。因为C12 的声明继承自Base12 with T1 with T2,这个类结构的构造顺序是从左到右的,从基类Base12 开始,接着是Traits T1 和T2,最后是C12 的构造主体。(对于构造任意复杂的结构,参见《第7章 - Scala 对象系统》的“对象结构的线性化”章节。)

所以,虽然你不能传递构造参数给Trait,你可以用默认值初始化字段,或让它们继续抽象。我们实际上在前面的Subject Trait 中见过, Subject.observers 字段被初始化为一个空列表。

如果Trait 的一个具体字段没有合适的默认值,那么就没有一个“万无一失”的方式来初始化这个值了。所有的其它方法都需要这个Trait 的用户的一些特别步骤,这很容易发生错误,因为他们可能会做错甚至忘记去做。也许这个字段应该继续作为抽象字段,这样类和其它Trait 使用它的时候会被强制定义一个合适的值。我们会在《第6章 - Scala 高级面向对象编程》中详细讨论重写抽象和具体成员。

另外一个解决方案是把这个字段转移到一个单独的类中,这样构造过程可以保证用户可以提供正确的初始化数据。这样也许应该说这整个Trait 实际上应该是一个类,这样你才能定义一个构造函数来初始化这个字段。

类还是Trait?

当我们考虑是否一个“概念”应该成为一个Trait 或者一个类的时候,记住作为混入的Trait 对于“附属”行为来说最有意义。如果你发现某一个Trait 经常作为其它类的父类来用,导致子类会有像父Trait 那样的行为,那么考虑把它定义为一个类吧,让这段逻辑关系更加清晰。(我们说像。。。的行为,而不是是。。。,因为前者是继承更精确的定义,基于Liskov Substitution Principle -- 例如参见 [Martin2003]。)

提示

在Trait 里避免不能用合适的默认值初始化的具体字段。使用抽象字段,或者把这个Trait 转换成一个有构造函数的类。当然,无状态Trait 没有初始化的问题。

一个实例应该从构造过程结束开始,永远都在一个已知的有效的状态下,这是优秀的面向对象设计的基本原则。

概括,及下章预告

在这一章,我们学习了如何使用Trait 来封装和共享类之间正交的关注点。我们也学习了何时,以及如何使用Trait,如果“堆叠”多个Trait,以及初始化Trait 的成员的规则。

在下一章,我们会探索Scala 编程中的面向对象基础。即使你是一个面向对象编程的老手,你也会希望通过阅读下面的章节来理解Scala 面向对象方法的方方面面。

51CTO推荐专题

专题:Scala编程语言

【编辑推荐】

  1. 《Scala编程指南》第一章
  2. 《Scala编程指南》第二章 
  3. 《Scala编程指南》第三章 
  4. 51CTO专访Scala创始人:Scala拒绝学术化
  5. “Scala” 一个有趣的语言
【责任编辑:立方 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

读 书 +更多

网管员必读—服务器与数据存储

《网管员必读—服务器与数据存储》全面、系统地介绍了在中、高级网络管理和网络工程实施中两个重要方面的主流技术和应用:硬件服务器和数据...

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊
× Phthon,最神奇好玩的编程语言