在《作为Scala语法糖的设计模式》中,我重点介绍了那些已经融入Scala语法的设计模式。今天要介绍的两个设计模式,则主要与Scala的trait有关。
Decorator Pattern
在GoF 23种设计模式中,Decorator Pattern算是一个比较特殊的模式。它充分利用了继承和组合(或者委派)各自的优势,将它们混合起来,不仅让优势扩大,还让各自的缺点得到了抵消。Decorator模式的核心思想其实是“职责分离”,即将要装饰的职责与装饰的职责分离,从而使得它们可以在各自的继承体系下独立演化,然后通过传递对象(组合)的形式完成对被装饰职责的重用。
从某种角度来讲,装饰职责与被装饰职责之间的分离与各自抽象,不妨可以看做是Bridge模式的变种。但不同之处在于Decorator模式又额外地引入了继承,但不是为了重用,而是为了多态,使得装饰者因为继承自被装饰者,从而拥有了被装饰的能力。所以说,继承的引入真真算得上是点睛之笔了。
理解Decorator模式,一定要理解继承与组合各自扮演的角色。简而言之,就是:
- 继承:装饰者的多态
- 组合:被装饰者的重用
正因为此,在Java代码中实现Decorator模式,要注意装饰器类在重写被装饰器的业务行为时,一定要通过传入的对象来调用被装饰者的行为。假设传入的被装饰者对象为decoratee,则调用时就一定是decoratee,而不是super(由于继承的关系,装饰类是可以访问super的)。
例如BufferedOutputStream
类作为装饰类,要装饰OutputStream
的write行为,就必须这样实现:
public interface OutputStream { void write(byte b); void write(byte[] b); }public class FileOutputStream implements OutputStream { /* ... */ }public class BufferedOutputStream extends OutputStream { //这里是组合的被装饰者 protected final OutputStream decoratee; public BufferedOutputStream(OutputStream decoratee) { this.decoratee = decoratee; } public void write(byte b) { //这里应该是调用decoratee, 而非super,虽然你可以访问super decoratee.write(buffer) } }
然而,在Scala中实现Decorator模式,情况却有些不同了。Scala的trait既体现了Java Interface的语义,却又可以提供实现逻辑(相当于Java 8的default interface),并在编译时采用mixin方式完成代码的重用。换言之,trait已经完美地融合了继承与组合的各自优势。因此,在Scala中若要实现Decorator模式,只需要定义trait去实现装饰者的功能即可:
trait OutputStream { def write(b: Byte) def write(b: Array[Byte]) }class FileOutputStream(path: String) extends OutputStream { /* ... */ }trait Buffering extends OutputStream { abstract override def write(b: Byte) { // ... super.write(buffer) } }
在Buffering的定义中,根本看不到组合的影子,且在对write方法进行重写时,调用的是super,这与我前面讲到的内容背道而驰啊!
区别在于组合(delegation)的时机。在Java(原谅我,因为使用Scala的缘故,我对Java 8的default interface没有研究,不知道是否与scala的trait完全相同)语言中,组合是通过传递对象方式完成的职责委派与重用,也就是说,组合是在运行时发生的。Scala的实现则不然,在trait中利用abstract override关键字来完成一种stackable modifications,这种方式被称之为Stackable Trait Pattern。这种语法仅能用于trait,它表示trait会将某个具体类针对该方法提供的实现混入(mixin)到trait中。装饰的客户端代码如下:
new FileOutputStream("foo.txt") with Buffering
FileOutputStream的write方法实现在编译时就被混入到Buffering中。所以可以称这种组合为静态组合。
Dependency Injection
Dependency Injection(依赖注入或者称为IoC,即控制反转)其实应该与依赖倒置原则结合起来理解,首先应该保证不依赖于实现细节,而是依赖于抽象(接口),然后,再考虑将具体依赖从类的内部转移到外面,并在运行时将依赖注入到类的内部。这也是Dependency Injection的得名由来。
在Java世界,多数情况下我们会引入框架如Spring、Guice来完成依赖注入(这并不是说依赖注入一定需要框架,严格意义上,只要将依赖转移到外面,然后通过set或者构造器注入依赖,都可以认为是实现了依赖注入),无论是基于xml配置,还是annotation,或者Groovy,核心思想都是将对象之间的依赖设置(装配)转交给框架来完成。Scala也有类似的IoC框架。但是,多数情况下,Scala程序员会充分利用trait与self type来实现所谓的依赖注入。这种设计模式在Scala中常常被昵称为Cake Pattern。
一个典型的案例就是将一个Repository的实现注入到Service中。在Scala中,就应该将Repository的抽象定义为trait,然后在具体的Service实现中,通过Self Type引入Repository:
trait Repository { def save(user: User) }trait DatabaseRepository extends Repository { /* ... */ }trait UserService { self: Repository => def create(user: User) { //这里调用的是Repository的save方法 //调用Self Type的方法就像调用自己的方法一般 save(user) } } //这里的with完成了对DatabaseRepository依赖的注入new UserService with DatabaseRepository
Cake Pattern遵循了Dependency Inject的要求,只是它没有像Spring或者Guice那样彻底将注入依赖的职责转移给外部框架,而是将注入的权利交到了调用者手里。这样会导致调用端代码并没有完全与具体依赖解耦,但在大多数情况下,这种轻量级的依赖注入方式,反而更讨人喜欢。
在Scala开发中,我们常常会使用Cake Pattern。在我的一篇文章《一次设计演进之旅》中,就引入了Cake Pattern来完成将ReportMetadata
依赖的注入。