Scala 混入trait特质

Scala  混入trait特质(特征)

通常,接口中定义了一些与实现类中的其他成员无关联的成员(这些成员具有“正交性”)。而混入(mixin)一词便适用于这类聚焦的、可重用的状态和行为。理想情况下,我们应单独维护这些公用的行为,而不应该依赖于任何使用这些行为的具体类型。
在Java 8 诞生之前,Java 未提供用于定义和使用这类可重用代码的内置机制。为此,Java程序员必须使用特定的方法进行复用某一接口的实现代码。最坏的情况下,开发人员必须将相同的代码复制粘贴到所有需要这一功能的类中。这里存在一个略好一些但谈不上完美的解决方案:单独编写一个实现了该行为的类,原始类中会保存一份该类的示例并负责为支持类提供代理方法调用。这一方案能够解决代码复用的问题,但也添加了一些没有必要的额外开销,同时容易导致错误的样板代码。

为了能够理解trait 在模块化Scala 代码的作用,我们首先学习一下下面的这段代码。图形用户界面(GUI)中的按钮会使用这段代码,当点击事件发生时,这段代码会通过回调机制通知客户:

package cn.com.tengen.test.withtrait

abstract class Widget

class ButtonWithCallbacks(val label: String, val callbacks: List[() => Unit] = Nil) extends Widget {
  def click(): Unit = {
    updateUI()
    callbacks.foreach(f => f())
  }
  protected def updateUI(): Unit = { /* 修改GUI页面样式 */ }
}
object ButtonWithCallbacks {
  def apply(label: String, callback: () => Unit) = new ButtonWithCallbacks(label, List(callback))
  def apply(label: String) = new ButtonWithCallbacks(label, Nil)
}

点击按钮之后将触发一组类型为() => Unit(这类函数完全通过副作用产生作用)的回调函数。

ButtonWithCallbacks 类有两大职责:更新可视界面样式(我们省略了相关代码)和处理回调行为。处理回调行为包括了管理一组回调函数以及点击按钮后调用这组函数。
我们在定义类型时应尽量做到职责分离,这样才能体现单一职责原则。单一职责原则认为每个类型都应该只做一件事,不应在一个类型中混杂多个职责。
我们希望能将按钮相关的逻辑从回调逻辑中抽离出来,这样一来这两类逻辑都会变得更加简单、更加模块化,也更易于测试和修改,可用性也更强。回调逻辑则很适用于实现成混入结构(mixin)。
我们将使用trait 将回调逻辑从按钮逻辑中抽离出来。除此之外,我们会对逻辑抽离方式进行简单的概括。实际上,回调是观察者设计模式的一类特殊应用。因此,我们创建了两个trait,分别用于声明观察者模式中的主体(Subject)和观察者(Observer),这两个trait 同时还实现了主体和观察者的部分功能。之后我们会运用这两个trait 处理回调行为。为了简化起见,我们首先使用一个简单的回调方法,统计按钮的点击次数:

trait Observer[-State] {
  // 那些希望能在状态发生变化时得到通知的客户将运用这一trait。这些客户代码必须实现receiveUpdate 方法。
  def receiveUpdate(state: State): Unit
}

// 那些需要向观察者发送通知的主体应使用该trait。
trait Subject[State] {
  //一组待通知的观察者列表。由于该列表为可变列表,因此非线程安全。
  private var observers: List[Observer[State]] = Nil

  //用于添加观察者的方法。该表达式的意思是,把observer 对象安放到observers 列表的最前面,并将新生成的列表赋给observers 列表。
  def addObserver(observer:Observer[State]): Unit =  observers ::= observer
  
  //该方法会向观察者通知状态变更。
  def notifyObservers(state: State): Unit =  observers foreach (_.receiveUpdate(state))
}

通常,将混入了Subject 特征的类直接设置为状态类型参数是最便捷的做法。因此,一旦某一对象的notifyObservers 方法被调用了,该实例直接将自己作为参数传入即可,例如:直接传入this 对象。
需要注意的是,由于Observer 特征既未实现它所声明的方法,也未定义其他任何成员,在字节码级别,该trait 实际上与Java 8 之前的接口是完全等同的。由于缺少方法实现体明显是一个抽象对象,因此我们不需要使用abstract 关键字。不过在声明那些包含了未实现方法体的抽象类时,我们必须使用abstract 关键字,例如:


trait PureAbstractTrait {
  def abstractMember(str: String): Int
}
abstract class AbstractClass {
  def concreteMember(str: String): Int = str.length
  def abstractMember(str: String): Int
}

定义一个简化的Button 类:


class Button(val label: String) extends Widget {
  def click(): Unit = updateUI()
  def updateUI(): Unit = { /* 本方法包含了GUI样式的修改逻辑 */ }
}

Button 类非常简单,它只负责一件事情——处理点击事件。

下面我们将使用Subject 特征。ObservableButton 类是Button 类的子类,我们在该类中混入了Subject 特征:

class ObservableButton(name: String) extends Button(name) with Subject[Button] {
  override def click(): Unit = {
    super.click()
    notifyObservers(this)
  }
}

运行脚本:

class ButtonCountObserver extends Observer[Button] {
  var count = 0
  def receiveUpdate(state: Button): Unit = count += 1
}

object ButtonMain extends App{
  val button = new ObservableButton("Click Me!")
  val bco1 = new ButtonCountObserver
  val bco2 = new ButtonCountObserver
  button addObserver bco1
  button addObserver bco2
  (1 to 5) foreach (_ => button.click())
  println("bco1:"+bco1.count+"\tbco2:"+bco2.count)
}

输出结果:
bco1:5	bco2:5

该脚本声明了一个观察者类型ButtonCountObserver,该类型负责统计点击的次数。之后我们创建了两个ButtonCountObserver 实例以及一个ObservableButton 对象,这两个实例都被注册为按钮的观察者。之后该脚本点击了五次按钮,打印按钮点击次数。
假设我们只需要一个ObservableButton 实例,那么我们并不需要单独声明一个混入了我们所需trait 的类,只需要声明一个Button 对象,并“凭空”为这个对象注入Subject 特征即可:

object ButtonMain extends App{
  val button = new Button("Click Me!") with Subject[Button] {
    override def click(): Unit = {
      super.click()
      notifyObservers(this)
    }
  }

  val bco1 = new ButtonCountObserver
  val bco2 = new ButtonCountObserver
  button addObserver bco1
  button addObserver bco2
  (1 to 5) foreach (_ => button.click())
  println("bco1:"+bco1.count+"\tbco2:"+bco2.count)
}

与之前的实现不同, 我们在声明Button() 对象时未声明新类, 而是直接捎带上了Subject[Button] 实例。这与Java 中初始化某一实现了接口的匿名类的做法较为相似,但Scala 提供了更多的灵活性。

如果待声明的类未扩展其他类,而只是混入了一些trait,那么无论如何你必须使用extend 关键字,并将其用于第一个trait,而在其他的trait 之前使用with 关键字。但是,如果你想在实例化某一类型时混入trait 声明,请在所有的trait 之前添加with 关键字。

猜你喜欢

转载自blog.csdn.net/u014646662/article/details/83861527