A.4 扩展性提供成长的空间
按照第A.2节和A.3节的原则实施,我们的抽象已经具备了合适的曝露度(极简)和纯粹度(精炼)。可是现在客户又要求给程序添加新功能,所以你不得不在抽象中加入额外的行为。那么现在是时候检验一下抽象的扩展能力是否足够。
A.4.1 什么是扩展性
扩展性的作用是让抽象能以逐步逐块的方式成长,同时不影响已有的客户。不同的开发范式具有不同的扩展性机制。本小节将总括性地介绍如何运用面向对象编程和函数式编程领域较为流行的技术来设计具备扩展能力的抽象。扩展性的其他表现形式将在介绍高级语言特性和高层次抽象时讨论,详情请参阅第5章至第8章。
Java中的Map
抽象提供了一种关联性数据结构的基本功能,并向其客户提供一个Dictionary
接口。标准JDK类库对java.util.Map
进行了好几种扩展。其中一些是具体的子类实现,例如HashMap
和TreeMap
提供了Map
接口的全部操作,但内部采用不同的底层存储机制。另一些是对Map
的子类型化,例如SortedMap
和ConcurrentMap
提供了比Map
类型更丰富的行为语义。
那么java.util.Map
的扩展能力如何呢?假如要给java.util.Map
增加一项特定的行为,并且要求对Map
接口的所有变体和实现起作用,应该怎么做呢?请仔细考虑一下这个问题,因为就Java语言提供的扩展手段而言,并不容易找到答案。
仅对HashMap
之类的某个具体实现进行扩展并不可行,因为那样的话,Map
接口的其他实现还是得不到新功能。
还可以用一个装饰器把Map
的实例包装起来(参见第A.6节文献[3]),但这种方案只有在特定的使用场景下才有效。在其他用例中,Map
被包装之后,会失去原始Map
实例的一些重要内容,例如SortedMap
和ConcurrentMap
。考虑以下的例子:
class DecoratedMap<K, V> implements Map<K, V> {
private final Map<K, V> m; →❶ 包装Map
public DecoratedMap(Map<K, V> m) {
this.m = m;
}
//.. 实现 Map<K, V>
}
DecoratedMap
装饰被包装对象Map
❶,此时即使客户在构造器中传入ConcurrentMap
或SortedMap
类型的实例,包装器也无法使用子类型提供的额外功能。Eugene在第A.6节文献[5]讨论了一种应用场景,其中为全部Map
实现提供扩展功能的唯一办法,是编写一个独立的工具函数,虽然按照纯粹主义者的观点,这是完全不符合面向对象的方式。
最后的对策唯有从头实现Map
接口,但这样做将产生大量复制黏贴的重复代码。
看过所有的可能性之后,你可能会疑问用纯正的OO方式扩展java.util.Map
为什么如此困难。
我们面对由抽象组成的一个层级结构,试图在java.util.Map
类型的所有实现中插入一个新的行为。解决这个问题迫切需要实现继承的帮助,更确切地说,需要多重实现继承的帮助,因为这其实是一个行为的相重性问题。我们需要一种组合独立的小粒度抽象手段,通过把它们无缝地附加、合并到主抽象之上,来实现引入新行为或覆盖已有行为的目的。Mixin提供了一种可行的途径,请看下一小节。
A.4.2 mixin:满足扩展性的一种设计模式
mixin正是我们所需要的手段。有不少语言提供了这种特性,利用mixin便于混合、搭配的性质,帮助开发者建造更大规模的抽象。
假设提供服务的金融中介公司决定在市场中引入一种新的票据,名叫热带风情票据。这种票据的一系列特性其实在一般的票据中也很常见,因此都已经在领域模型中实现过了。剩下的工作是把各种已有的特性组装起来,为新票据扩展抽象。用基于mixin的编程方式来实现的话,简直就像Mixin的字面意义一样,把各种单独的特性“混入”基本抽象,就能组合成我们的热带风情票据。从图A-3可以很清楚地看出它们是怎样“混入”的。mixin类CouponPayment
、Maturable
和Tradable
混入父抽象Instrument
,为完整的类ExoticInstrument
提供行为和实现。
Gilad Bracha是一位计算理论学家,在他提交到1990年度OOPSLA(面向对象编程、系统、语言及应用)大会的论文(参见第A.6节文献[4])中,mixin被定义为一种抽象子类,可对一系列多样化的父类进行行为特化。mixin不可以被单独使用,所以把它们叫做抽象子类。mixin定义统一的类扩展,然后无缝地附在一个抽象家族身上,为整个抽象家族增加同样的行为。Scala(http://www.scala-lang.org)通过Traits的形式实现mixin,而在Ruby中则叫做Modules。Trait可以理解为Java中的接口,只不过接口中的方法声明还可以包含可选的实现给基类共享。
图A-3 基于mixin的继承。ExoticInstrument从Instrument
获得issue()
和close()
的实现,然后与CouponPayment
、Maturable
、Tradable
等mixin组合
A.4.3 用mixin扩展Map
现在来看看怎样用mixin给Map
抽象的所有实现增加一项特定行为。假设需要给各种Map
添加一个同步的get
方法,覆盖原本的API。首先,在Scala中定义一个trait,在标准Map
接口的基础上提供一个新增行为的实现。
trait SynchronizedGet[A,B] extends Map[A, B] {
abstract override def get(key: A): Option[B] = synchronized {
super.get(key)
}
}
然后这个行为可以混入Scala的任意一种Map
类型变体,无论其底层如何实现:
val names = new HashMap[String, List[String]]
with SynchronizedGet[String, List[String]] →对Scala HashMap进行混入
val stuff = new scala.collection.jcl.LinkedHashMap[String, List[String]]
with SynchronizedGet[String, List[String]] →对Java LinkedHashMap进行混入
注意,在最后的实现中,SynchronizedGet
这个trait是在运行时对象创建期间被动态混入的。
Scala trait还可以作为多个属性的组合体,静态地混入已有的抽象。Scala的trait混入手法既能达到实现继承的目的,又避免了Java实现的缺点。第6章介绍Scala语言特性的时候会更进一步讨论Scala trait。
A.4.4 函数式的扩展性
很多人抱怨OO编程迫使他们编写很多不必要的类。“一切都是类”并不成立,虽然面向对象有时候希望你这样想。在现实世界中,很多问题更适合建模成函数式的抽象或者基于规则的抽象。
假设要建模一个有限步骤的算法,如下面的代码片段。我故意省略了作为算法输入的参数。
def process(...) = {
try {
if (init) proc
} finally { end }
}
init
是算法的初始化部分。如果init
成功完成,接着调用负责核心处理的部分proc
。end
是终结部分,负责清理各种资源。
现在要求你进行扩展,让init
、proc
和end
都有不同的实现。一种思路是为每个步骤分别定义一个对象,把各阶段的流程包装成functor或者函数对象。这个思路是可行的,但如果能够发挥函数式编程和高阶函数的作用,会得到更好的结果。你可以把init
、proc
和end
建模为函数,然后作为闭包传递给主流程。如下所示:
def process(init: =>Boolean, proc: =>Unit, end: =>Unit) = {
try { → 通用的算法
if (init) proc
} finally {
end
}
}
def doInit = { //.. } → 初始化
def doProcess = { //.. } → 核心处理
def doEnd = { //.. } → 终结
最后我们来看一种非常规的扩展方式。并非所有语言都支持这种手法,但如果能理智地使用,它将是一件高效的工具。
A.4.5 扩展性也可以临时抱佛脚
扩展不一定需要建立新的抽象。不少语言提供一种叫做开放类(open classes)的特性,你可以向这种类注入新的方法或者修改类中已存在的方法,直接扩展其现有结构。Ruby和Groovy都支持开放类,允许你打开任何一个类去修改其行为。一般把这种做法叫做猴子补丁(monkey patching)。很多开发者认为猴子补丁极不安全,对它大皱眉头。如果能负责任地运用,这种技术可以发挥极大的威力,可是当前的软件开发中能找到许多实际例子,让它的自我标榜站不住脚。
Ruby猴子补丁的主要问题在于缺少语法作用域;加在一个抽象上的任何东西都会进入全局命名空间,并对该抽象的所有用户可见。Scala在这一点上要好很多,它提供一种限制词法作用域的开放类,在Scala的术语里面叫做implicits,通过在语法作用域内的隐式转换来扩展现有类。(对这种语言特性的讨论可参阅http://debasishg.blogspot.com/2008/02/why-i-like-scalas-lexically-scoped-open.html)这种特性出乎意料地有用,能够在不安全的Ruby猴子补丁和严格封闭的Java类两个极端中间找到完美的平衡点。