设计模式
一说起“设计模式” (design pattern),人们总会首先想到我们的老朋友 Java。 Java 的狂热者可能会认为,没有设计模式的 Java 代码,就像失去灵魂的空壳。但是,设计模式真的是在好好“设计”吗?
无论是学术界还是工业界,已经有越来越多的人对设计模式持反对的态度。他们认为,设计模式是有害的 (Design patterns should be considered harmful)。 如果你读过所谓“精通 Java 设计模式”之流的书,并仔细思考其中的“设计理念”,你会发现,其实很多设计模式是语言设计的缺陷导致的。 反观其他一些编程语言,相当一部分设计模式在它们之中并不存在。为什么?因为根本不需要,只要正常写就行了。而访问者模式 (visitor pattern) 就是其中之一。
模式匹配
与其他很多标榜自己为“函数式”的语言一样,Scala 支持模式匹配 (pattern matching)。 如果我们想实现一个对加减法表达式求值的程序,那么只用模式匹配就够了:
Copy sealed abstract class Expr
case class Add(lhs: Expr, rhs: Expr) extends Expr
case class Sub(lhs: Expr, rhs: Expr) extends Expr
case class Number(value: Int) extends Expr
def eval (expr: Expr) : Int = expr match {
case Add(l, r) => eval(l) + eval(r)
case Sub(l, r) => eval(l) - eval(r)
case Number(v) => v
}
简单易懂,易于调试。如果你漏掉了一些 case,如忘记了 Number
:
Copy def eval (expr: Expr) : Int = expr match {
case Add(l, r) => eval(l) + eval(r)
case Sub(l, r) => eval(l) - eval(r)
}
编译器还会报警告:
Copy warning: match may not be exhaustive.
It would fail on the following input: Number(_)
def eval(expr: Expr): Int = expr match {
^
你如果没看见,那么运行时求值到 Number
时也会抛出异常 MatchError
,这样你很快就能意识到哪里出错了。
访问者模式
Java 没有模式匹配。因此,为了实现上述加减法求值的功能,我们得费一番功夫。 如果我们想仿照上述写法,那么可以这样:
Copy abstract class Expr {}
class Add extends Expr { Expr lhs; Expr rhs; }
class Sub extends Expr { Expr lhs; Expr rhs; }
class Number extends Expr { int value; }
int eval( Expr expr) {
if (expr instanceof Add) { // 分支1
var e = (Add) expr;
var l = eval( e . lhs ) ;
var r = eval( e . rhs ) ;
return l + r;
} else if (expr instanceof Sub) { // 分支2
var e = (Sub) expr;
var l = eval( e . lhs ) ;
var r = eval( e . rhs ) ;
return l - r;
} else { // 分支3:expr instanceof Number
var e = (Number) expr;
return e . value ;
}
}
这样也不是不行,但是反复的 instanceof
和强制类型转换让开发者觉得这样的代码十分丑陋。 如果程序员粗心把强制转换写错了,鬼知道在运行时的哪个阶段会莫名其妙的出来 ClassCastException
。 为了解决这个“问题”,Java 开发者设计出了这样一种“巧妙”的方法:
既然我们不愿意强制类型转换,那么不妨把每个 if 分支提取成一个方法,然后让方法的参数就是我们要的那个类型:
Copy interface ExprVisitor < T > {
T visitAdd ( Add e);
T visitSub ( Sub e);
T visitNumber ( Number e);
}
接下来,如何让比如说 visitAdd
恰好能够应用到一个类型为 Add
的表达式上面呢?我们只要让每个表达式都支持一个负责转发的方法即可:
Copy abstract class Expr {
abstract < T > T accept ( ExprVisitor < T > v);
}
class Add extends Expr {
Expr lhs;
Expr rhs;
@ Override
< T > T accept ( ExprVisitor < T > v) { return v . visitAdd ( this ); }
}
class Sub extends Expr {
Expr lhs;
Expr rhs;
@ Override
< T > T accept ( ExprVisitor < T > v) { return v . visitSub ( this ); }
}
class Number extends Expr {
int value;
@ Override
< T > T accept ( ExprVisitor < T > v) { return v . visitNumber ( this ); }
}
最后将 eval
方法实现为一个访问者的实例:
Copy class EvalVisitor implements ExprVisitor < Integer > {
@ Override
int visitAdd ( Add e) {
var l = e . lhs . accept ( this );
var r = e . rhs . accept ( this );
return l + r;
}
@ Override
int visitSub ( Sub e) {
var l = e . lhs . accept ( this );
var r = e . rhs . accept ( this );
return l - r;
}
@ Override
int visitNumber ( Number e) {
return e . value ;
}
}
int eval( Expr expr) {
var v = new EvalVisitor() ;
return expr . accept (v);
}
这样,对于任意的 Expr
,我们只要调用它的 accept
方法,那么按照 OOP 的动态分派 (dynamic dispatch) 机制, 根据这个表达式对象的具体类型,三个分支之一将会被调用。具体来说,执行 e.accept(v)
时:
如果 e instanceof Add
,那么相当于调用的是 Add
类定义的 accept
方法,该方法会调用 v.visitAdd
(分支1)
如果 e instanceof Sub
,那么相当于调用的是 Sub
类定义的 accept
方法,该方法会调用 v.visitSub
(分支2)
如果 e instanceof Number
,那么相当于调用的是 Number
类定义的 accept
方法,该方法会调用 v.visitNumber
(分支3)
进而用了一种更加“优雅”的方法实现了之前那个丑陋的三段 if-else 函数的功能。
总结一下,为了实现相同的功能,Scala 需要定义1个抽象类、3个具体的类和1个方法,而 Java 需要定义1个抽象类、1个接口、4个具体的类和实现6个方法。 回顾这个设计模式,你会发现,我们其实就是在手动模拟一种典型的模式匹配。如果一个语言将模式匹配作为其基础语言特性,那么我们就不用费劲来手动模拟了。
框架中的访问者模式
Scala 实验框架中,只有在与 Java 进行混合编程的地方才会使用访问者模式,其他部分一律使用正常的模式匹配。 如果将来实验框架完全不依赖于 Java 库的话,那么无需访问者模式。目前,Rust 版框架已经做到这一点了。