缘起
设计模式是建立在编程语言层面之上的,从某种角度上看,模式是以应用场景为导向,在编程语言的基础设施之上构建的最佳设计的范本,其价值在于可以作为模版应用于同类场景中。而反过来,语言基础设施的改变必然会影响到上层的设计模式,设计模式和编程语言相互影响,新的编程范式会催生新的设计模式,而成熟的设计模式会引导在语言在基础层面上进行进化从而直接支持!这类的案例是非常多的,比如:Java语言原生支持原型模式、观察者模式,Scala里的Object是原生的单态,等等。本文将要探讨的是在Scala语言中存在的一种语言特性:类型类。同时,本文还将会讨论一个与类型类在用意上非常接近的设计模式:桥接模式,讨论两者的异同,但是请不要被我开篇的立论所误导,两者是否互为等价物,需要我们抽丝剥茧慢慢分析。本文原文出处: http://blog.csdn.net/bluishglc/article/details/50894843 严禁任何形式的转载,否则将委托CSDN官方维护权益!
认识“类型类”
类型类是Scala中的一项语言特新,为了能更好的说明它的设计初衷,在介绍它之前我们先来看一个传统的面向对象的设计,你一定非常熟悉:
一个糟糕的例子
package com.github.scala.typeclasses
import scala.math.{Pi, sqrt}
object PolymorphicDemo {
trait Shape {
def area:Double
}
case class Circle(radius:Double) extends Shape {
def area:Double = 2*Pi*radius
}
case class EQLTriangle(side:Double) extends Shape {
def area:Double = (sqrt(3)/4)*side*side
}
val shapes = Vector(Circle(2.2), EQLTriangle(3.9), Circle(4.5))
def a(s:Shape) = f"$s area: ${s.area}%.2f"
val result = for(s <- shapes) yield a(s)
def main(args: Array[String]) {
println(result)
}
}
上述代码设计了一个Trait: Shape,所有的Shape都有一个计算面积的方法,所以这个方法被定义在了Trait中。在它的两个派生子类:Circle和EQLTriangle中都实现了计算面积的方法:area. 这个例子来自《Atomic Scala》一书,书中解释说:想把计算面积的方法抽离出来独立的演变,于是引入的“类型类”的实现版本:
package com.github.scala.typeclasses
import scala.math.{Pi, sqrt}
object TypeClassDemo {
trait Calc[S]{
def area(shape:S):Double
}
case class Circle(radius:Double)
implicit object CircleCalc extends Calc[Circle] {
def area(shape:Circle) = 2*shape.radius*Pi;
}
case class EQLTriangle(side:Double)
implicit object EQLTriangleCalc extends Calc[EQLTriangle] {
def area(shape:EQLTriangle) = (sqrt(3)/4)*shape.side*shape.side
}
def a[S](shape:S)(implicit calc: Calc[S])= f"$shape area: ${calc.area(shape)}%2.2f"
def main(args: Array[String]) {
//you can only use a(shape) to get area not shape.area
//this is not so "OO"
println(a(Circle(2.2)))
println(a(EQLTriangle(3.9)))
println(a(Circle(4.5)))
}
}
如果仅从学习语法的角度看,这个例子是OK的,但是这个例子会严重误导读者对于类型类用意的理解。首先,你会发现,在这个示例中没有了Circle和EQLTriangle的共同基类:Shape,实际上它们也不能有共同基类Shape,因为Shape的area是虚函数,一旦继承,Circle和EQLTriangle就必须要实现,而这个例子要演示的是让类型类去实现。这个例子没能很好地演示“在两个维度上独立演化”,反而把原类族的继承关系给摸去了。实际上,作为一个反面教材,这个案例很好的证明了一个事实:类型类不是用来重写一个或多个类的已有方法的,而只能是给它们添加新的面向这个类型自身的某种具有共性的操作。
一个“稍好”的例子
让我们接着看一个稍好的应用类型类的例子:
package com.github.scala.typeclasses
import scala.math.{Pi, sqrt}
object TypeClassDemo2 {
trait Car
trait Transmissive[C]{
def shift(Car:C)
}
case class Bus(seats:Int) extends Car
implicit object ATBus extends Transmissive[Bus] {
def shift(car:Bus) = println(s"This is a Bus, seats: ${car.seats} , now shift automatically!");
}
// //Error: ambiguous implicit values:
// implicit object MTBus extends Transmissive[Bus] {
// def shift(car:Bus) = println(s"This is a Bus, seats: ${car.seats} , now shift manually!");
// }
case class Truck(load:Double) extends Car
implicit object ATTruck extends Transmissive[Truck] {
def shift(car:Truck) = println(s"This is a Truck, load: ${car.load} T, now shift automatically!");
}
// //Error: ambiguous implicit values:
// implicit object MTTruck extends Transmissive[Truck] {
// def shift(car:Truck) = println(s"This is a Truck, load: ${car.load} T, now shift manually!");
// }
def shift_func[C](car:C)(implicit t: Transmissive[C]) = t.shift(car)
def main(args: Array[String]) {
shift_func(Bus(10))
shift_func(Truck(1))
}
}
在这个例子中, 我们试图来展示:类型类可以将一个已存在的类族向另一个维度上演变的能力。Car和Bus/Truck是主类族,类型类Transmissive试图从变速方式上对Car类族进行独立的演化,而附加上去的行为就是:shift(换挡)。之所以说这是一个“稍好”的例子,一方面它能够印证类型类将一个已存在的类族向另一个维度上演变的能力,但同时我们也看到另一个尴尬的地方,那就是代码中注释掉的部分,因为如果不注释掉,会出现编译错误,因为针对同一个类型引入了两个隐式对象,编译器在隐式解析时不知道要使用那一个。注释掉的代码试图将Car类族向另一个维度上进去进行演变,但是它失败了,在后续的桥接模式的探讨中,我们会继续探讨这个问题。
桥接模式?
类型类很容易让人联想到桥接模式,因为两者都有能让一组类型在另一种维度上演化的能力。但是两者的差异其实是很大的。在对比之前我们还是有必要来简单回顾一下桥接模式。
上图是《设计模式》一书中给出的桥接模式的案例,该书对于桥接模式的解释是将抽象部分与它的实现部分分离,使它们都可以独立地演化,在现实世界里,很多事物根据不同的属性和行为特征都有可以按照不同的维度进行分类,一般分两种典型的场景:
- 一种是多个子类之间的概念是平行的,比如咖啡有大杯、小杯之分,又有摩卡、卡布奇诺之分。
- 一种是多个子类之间有内容和概念上的重叠, 那么需要我们抽象共同的部分,把变化的部分抽象成类族独立的去演化
以上两种场景中的任何一种都可以使用桥接模式轻松地解决。那么问题来了,“类型类”能解决上述问题吗?在回答问题之前,让我们先定义两个概念,我们知道既然类型类和桥接都有让两个类族独立演化的能力,为了准确的描述,我们把两个独立演化的维度分别称为主维度和副维度(在Bridge的示意图中,Window是主维度,WindowImp是副维度,在我们的类型类示例代码中,Car是主维度,Transmissive是副维度)
现在来回答前面的问题:
- 针对第一种场景
- 如果副维度是一个单一概念(不存在两层以上的继承,也不存在并行的第二种实现),则可以使用类型类,而且使用类型类在语法实现更多加简洁优雅,比如我们前面例子中,仅仅只针对汽车类族在“自动档”维度上做的延展工作一样(当然如前所说这不是一个最好的例子)。
- 但一旦副维度上出现了两种以上的实现,就会出现二意性的隐式解析错误,所以就不能使用类型类了。
- 针对第二种场景,类型类更无能为力了。
对决
以下我们把这两者放到一起,做一个细致的比较:
- “用意”不同
类型类可以针对一个类族进行嫁接,但并不限于一个类族,它可以针对任何类嫁接,这一点很重要,类型类的立意是围绕一个主题或者说特性,为所有涉及到这些主题或特性的类“添加”或者说“绑定”面向这个主题或特性的“实现”!而桥接模式总是针对 一个类族在另一个维度上进行演变。
- “粒度”不同
类型类并不是在目标类型的目标方法上添加或修改逻辑,它也没有这样的能力,它的目标是针对目标类型“附加”新的操作!这是类型类区别于桥接模式的一个显著特点。
- “维度”不同
桥接模式必定有两个独立演化的类族,如果两个类族各有两个实现类的话,那么理论上会产生2X2=4种对象组合,而对于类型类来说,它自身(副维度)是没有自主的演化体系的,就像我们的关于Car的实例代码一样,你只能针对一个具体的汽车类施加一种换挡操作,在“变速模式”这个维度上,它只能是单一的,如果有两个以上的变种,你就不能再使用类型类了。
- “嫁接”方式不同。
对桥接模式而言,主维度中的具体类一定会依赖到(作为一个字段被引入)副维度上的具体类,这是桥接模式将两种维度嫁接在一起的方式。而对于类型类来说,是通过将副维度的基类泛型化,在副维度的具体类继承时指定了要嫁接的主维度的对应类型,两个维度的嫁接需要通过一个使用了隐式参数的“嫁接函数”(如本例中的a)来实现的。虽然从语法上看,像println(a(Circle(2.2)))
这样在求图像面积时没有出现第二维度的具体类,直观上完成了嫁接,但是不完美的地方在于:你不能使用println(Circle(2.2).area)这样更自然更OO的方式去调用,这是非常让人遗憾的。但尽管如此,类型类的定义和声明方式还是非常优雅的。
小结
跳出主/副维度的圈子,类型类设计的初衷不是和桥接模式进行PK,类型类的立意更加倾向于创建一个独立于任何其他类族的功能系(只针对某项功能或特征进行建模),然后可能有多种类型都需要这个功能或特征,此时是类型类的典型应用场景。实际上,类型类并没有真得把一组类型在另一个维度上演化,它所做的就是针对某个类型,附加了一些行为,然后利用隐式参数,使得调用时看上去像是被嫁接到目标类型上一样。