1. Trait
Trait是指可以混入或融入一个类层次结构的行为。比如说,先对Friend建模,然后将其混入任何类:Men、Women、Dog等,而不用让它们都从一个公共的基类继承下来。
假定我们已经建模出Human,现在,想让它成为朋友。朋友是能够倾听你说话的人。所以,我们要给Human类增加一个listen方法,下面是:
class Human(val name: String) {
def listen() = println("Your friend " + name + " is listenning")
}
class Man(override val name: String) extends Human(name)
class Woman(override val name: String) extends Human(name)
上面代码的一个不足之处在于,朋友这方面的特性不太突出,而且它被并到Human类里。另外,开发几周后,我们意识到我们忘记了人类最好的朋友——狗是伟大的朋友——当我们有太多无法释怀时,它们会安静地听我们叙述。但是,怎样才能让够成为我们的朋友呢?我们不能为此就让Dog从Human继承下来。Java解决这个问题的方式是创建一个接口Friend,让Human和Dog都实现这个接口。我们不得不在这两个类里提供不同实现,不管是不是真的不同。
这就是Scala的trait介入的地方了。Trait像一个拥有部分实现的接口。trait里定义和初始化的val和var会在混入trait的类的内部得到实现。定义过而未初始化的val和var则认为是抽象的,需要由混入这些trait的类实现。下面将Friend这个概念重新实现为trait:
trait Friend {
val name: String
def listen() = println("Your friend " + name + " is listening")
}
class Human(val name: String) extends Friend
class Man(override val name: String) extends Human(name) // 使用override是因为重写了基类的name字段
class Woman(override val name: String) extends Human(name)
}
这里,把Friend定义为trait。它有一个名叫name的val,被当作abstract对待。此外还有一个listen()方法。name实际的定义或实现由混入这个trait的类提供。
Human类混入了Friend trait。如果类并不继承其他任何类的话,那么可以使用extends关键字混入trait。Human类及其派生类Man和Woman简单的使用了trait提供的listen()方法的实现。如果需要的话,也可以改写这个实现。
混入trait的数量可以是任意的。用关键字with就可以混入更多的trait。如果类已经继承了另一个类,就像下面这个例子里的Dog,还可以用关键字with混入第一个trait。除了混入trait之外,下面在Dog里改写listen()方法。
class Animal
class Dog(val name: String) extends Animal with Friend {
override def listen(): Unit = println(name + "'s listening quietly")
}
一个类被混入trait之后,通过它的实例可以调用到trait的方法,也可以把它的引用当做trait的应用。
def helpAsFriend(friend: Friend) = friend listen()
def main(args: Array[String]): Unit = {
val john = new Man("John")
val sara = new Woman("Sara")
val comet = new Dog("Comet")
john.listen() // Your friend John is listening
sara.listen() // Your friend Sara is listening
comet.listen() // Comet's listening quietly
val mansBestFriend: Friend = comet
mansBestFriend.listen() // Comet's listening quietly
helpAsFriend(sara) // Your friend Sara is listening
helpAsFriend(comet) // Comet's listening quietly
}
trait看上去很像类,但是还有一些很大的差别。首先,它们需要混入类去实现那些已经声明的而未初始化的(即抽象的)变量和值。其次,它们的构造器不能有任何参数。trait会编译成Java的接口,还有对应的实现类,里面包含了trait实现的方法。
多重继承通常会带来方法冲突的问题,trait并不会为这个问题所扰。通过延迟绑定混入类的方法,它们有效的回避了这一点。如此一来,在trait里调用super可能解析成另一个trait的方法,也可能会解析成混入类的方法。
2.选择性混入
在上面的例子里,Friend trait混入到了Dog类里。这样就可以将Dog的任意实例当作Friend;也就是说,所有Dog都是Friend。
此外,还可以在实例一级对trait进行选择性混入,这样的话,就可以把特定的类的实例当作trait。看下面例子:
class Cat(val name:String) extends Animal
Cat并没有混入Friend trait,所以,不能把Cat的实例当作Friend。
def useFriend(friend: Friend)=friend listen
val alf = new Cat("Alf")
// val friend:Friend = alf // error
// useFriend(alf) // error
然而,Scala确实可以为爱猫人提供帮助,需要的话,我们可以专门把特殊的宠物当作Friend。创建实例时,只要简单的用with关键字标记一下即可:
def useFriend(friend:Friend)=friend listen
val snowy = new Cat("Snowy") with Friend
val friend:Friend = snowy
friend.listen // Your friend Snowy is listening
useFriend(snowy) // Your friend Snowy is listening
Scala给予了我们极大的灵活性:把类的所有实例当作trait,或是只选择需要的实例当作trait。如果想把trait用于事先存在的类上,后者就显得有用了。
3. 以trait进行装饰
Trait可用于装饰对象,使器具备一些能力。假设我们要对申请者进行不同的检查——信贷、犯罪记录、雇佣记录等。我们并不总是对所有的检查项感兴趣。公寓申请人需要检查信贷和犯罪记录,而就业申请人则需要检查犯罪记录和之前的雇佣记录。如果依靠创建特定的类对这些组合进行检查的话,最终,会为所需要检查的各种排列组合都创建一个类。而且,如果决定进行额外的检查,就不得不改变处理这组检查的类。不,我们要避免这种类的激增。我们可以更具有成效一些,对每种情况,只混入特定的检查。
abstract class Check {
def check(): String = "Checked Application Details..."
}
对不同类型的检查,比如信贷、犯罪记录和雇佣记录,我们都会创建向下面这样的trait:
trait CreditCheck extends Check {
override def check(): String = "Checked Credit..." + super.check()
}
trait EmploymentCheck extends Check{
override def check(): String = "Checked Employment..." + super.check()
}
trait CriminalRecordCheck extends Check {
override def check(): String = "Check Criminal Records..." + super.check()
}
这些trait都继承自Check,因为我们只想把它们混入继承自Check的类。继承这个类给予了我们两个能力。首先,这些trait只能混入继承自Check的类。其次,在这些trait里可以使用Check的方法。
我们感兴趣的使增强或是修饰check()方法的实现,所以,需要将其标记为override。这里check()实现调用了super.check()。在trait里,通过super调用的方法会经历一个延迟的过程。
这个调用并不是对基类的调用,而是对其左边混入的trait的调用
——如果这个trait已经是混入的最左边trait,那么这个调用就会解析成混入这个trait的类的方法。
目前为止,在这个例子里有一个抽象类,三个trait,没有任何具体类——因为根本不需要。检查公寓申请时,可以用一个实例把上面的trait和类放在一起:
val apartmentApplication = new Check with CreditCheck with CriminalRecordCheck
// Check Criminal Records...Checked Credit...Checked Application Details...
println(apartmentApplication check)
val employmentApplication = new Check with CriminalRecordCheck with EmploymentCheck
// Checked Employment...Check Criminal Records...Checked Application Details...
println(employmentApplication check)
最右的trait开始调用check()。然后,顺着super.check(),将调用传递到其左边的trait。最左边的trait调用的时真正实例的check()。
在Scala中,trait是一个强有力的工具,可以用它混入横切关注点。使用它们可以较低的成本创建出高度可扩展的代码。无需创建一个拥有大量类和接口的层次结构,就可以快速地把必要的代码投入使用。
4. Trait方法的延迟绑定
上面的例子里,Check类的check()方法是具体的,trait都是从这个类继承的。我们见识到了,在trait里对super.check()的调用时如何绑定到其左边的trait或是其混入的类的。但如果基类的方法时抽象的,就会变得有点复杂。下面进一步探索一番。
先写一个抽象类Writer,它有一个抽象方法writeMessage():
abstract class Writer {
def writeMessage(message: String)
}
任何继承这个类的类都要实现writeMessage()方法。如果有一个trait继承了这个抽象类,并且用super调用了这个抽象方法,Scala会要求将方法声明为abstract override。将这两个关键字组合到一起看上去有些奇怪。关键字override告诉Scala,要为基类的一个已知方法提供一个实现。同时,还表示,这个方法实际最后的"终极"实现由混入这个trait的类提供。下面是一个例子,这个trait继承了上面的那个类:
trait UpperCaseWriter extends Writer {
abstract override def writeMessage(message: String) = super.writeMessage(message.toUpperCase())
}
trait ProfanityFileredWriter extends Writer {
abstract override def writeMessage(message: String): Unit = super.writeMessage(message.replace("stupid", "s-----"))
}
在这段代码里,为了调用super.writeMessage,Scala做了两件事。首先,它对这个调用进行了延迟绑定。其次,它会要求混入这些trait的类提供该方法的实现。ProfanityFilteredWriter只负责处理有些粗鲁的单词——且仅当它以小写形式出现。这是为了体现混入的顺序。
现在来用一下这些trait。先来写个类StringWriterDelegate,继承自抽象类Writer,将写消息的操作委托给一个StringWriter实例:
class StringWriterDelegate extends Writer {
val writer = new StringWriter
override def writeMessage(message: String): Unit = writer.write(message)
override def toString(): String = writer.toString
}
在上面StringWriterDelegate的定义里可以混入一个或多个trait,不过,在这里,我们选择的事在创建这个类的实例时混入trait
val myWriterProfanityFirst = new StringWriterDelegate with UpperCaseWriter with ProfanityFileredWriter
val myWriterProfanityLast = new StringWriterDelegate with ProfanityFileredWriter with UpperCaseWriter
myWriterProfanityFirst writeMessage "There is no sin except stupidity"
myWriterProfanityLast writeMessage "There is no sin except stupidity"
println(myWriterProfanityFirst) // THERE IS NO SIN EXCEPT S-----ITY
println(myWriterProfanityLast) // THERE IS NO SIN EXCEPT STUPIDITY
在第一个语句里,ProfanityFilteredWriter是最右的trait,所以,它会先起作用。然而,在第二个语句中,它会后起作用。
5. 隐式类型转换
假设我们要创建一个应用,其中包含了几种日期和时间的操作。如果代码可以写成下面这样,就会相当方便,更加可读:
2 days ago
5 day from_now
上面的代码看起来不像代码,更像是数据输入——这是DSL的特征之一。可选的点和括号在这里起到了作用。在第一个语句里,我们调用了2的days()方法,传入一个变量ago。在第二个语句里,调用了5的方法,传的变量是from_now。
如果编译上面的代码,Scala会提示days()不是Int的方法。是的,Int没有提供这个方法,但是这并不能阻止我们写出这样的代码。就让Scala安静地把Int转换成什么东西,帮我们完成上面这个操作——进入隐式类型转换的世界吧!
隐式类型转换可以帮助我们扩展语言,创建“专用于特定应用和领域”的词汇或语法,也可以帮助我们创建属于自己的领域专用语言。
为了先理解这些概念,我们从一个恶心的代码开始,然后把它重构成一个漂亮的类。
我们需要定义变量ago和from_now,让Scala接收days()方法。定义变量很简单,接收方法却不容易。我们创建一个类DateHelper,其构造函数可以以一个Int为参数:
import java.util._
class DateHelper(number: Int) {
def days(when: String): Date = {
var date = Calendar.getInstance()
when match {
case "ago" => date.add(Calendar.DAY_OF_MONTH, -number)
case "from_now" => date.add(Calendar.DAY_OF_MONTH, number)
case _ => date
}
date.getTime
}
}
DateHelper类提供我们想要的days()方法。现在,我们所需要做的就是把Int转化成DateHelper。可以用一个方法来做这件事,接收一个Int,返回一个DateHelper的实例。简单的把方法标记为implicit,只要它在当前范围内存在(通过当前import可见,或是位于当前文件),Scala就会自动调用它。
代码如下:
def main(args: Array[String]): Unit = {
implicit def convertInt2DateHelper(number: Int) = new DateHelper(number)
val ago = "ago"
val from_now = "from_now"
val past = 2 days ago
val appointment = 5 days from_now
println(past)
println(appointment)
}
如果把上面的代码同DateHelper的定义一起运行,Scala就会自动把给定的数字转换为一个DateHelper实例,然后,调用days()方法。
现在,代码已经可以工作了,是时候稍作清理了。我们并不想在每次需要转换时都去写隐式转换器。把这个转换器放到一个单独的单独的单例对象里,可以获得更好的重用性,也更加易用。可以把转换器挪到DateHelper的伴生对象用。
import java.util._
class DateHelper(number: Int) {
def days(when: String): Date = {
var date = Calendar.getInstance()
when match {
case "ago" => date.add(Calendar.DAY_OF_MONTH, -number)
case "from_now" => date.add(Calendar.DAY_OF_MONTH, number)
case _ => date
}
date.getTime
}
}
object DateHelper {
val ago = "ago"
val from_now = "from_now"
implicit def convertInt2DateHelper(number: Int) = new DateHelper(number)
}
导入DateHelper时,Scala会自动的找到转换器。这是因为Scala会在当前范围和导入的范围内进行转换。
下面是一个例子,用到了在DateHelper里写的隐式转换。
import DateHelper._
val past = 2 days ago
val appointment = 5 days from_now
println(past)
println(appointment)
在Predef对象里,Scala已经定义了一些隐式转换,Scala会默认导入它们。这样的话,比如说,当我们写 1 to 3时,Scala就会隐式的将1从Int转换为其富封装器RichInt,然后,调用to()方法。
Scala一次至多应用一个隐式转换。当前范围内,如果发现通过类型转换有助于操作、方法调用或类型转换的成功完成,就会进行转换。