基于Scala Trait的设计模式

在《作为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依赖的注入。

原文发布于微信公众号 - 逸言(YiYan_OneWord)

原文发表时间:2017-02-28

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏magicsoar

C++ 游戏之点点水果

大二时利用C++编写的点水果小游戏 程序代码总共3个文件,main.cpp Fruit.h Fruit.cpp  代码将在图片下面给出 至于讲解,由于过了一年多...

2207
来自专栏HansBug's Lab

3401: [Usaco2009 Mar]Look Up 仰望

3401: [Usaco2009 Mar]Look Up 仰望 Time Limit: 3 Sec  Memory Limit: 128 MB Submit: ...

3287
来自专栏哈雷彗星撞地球

iOS动画(补充)--特殊Layer动画

iOS中有一些特殊的layer,也可以做一些动画效果,本文就补充两个可以做动画效果的layer: CAEmitterLayer 和 CAReplicatorLa...

602
来自专栏用户画像

省份和城市的级联下拉菜单

682
来自专栏ios 技术积累

ios 圆形进度条

有很多开源的进度条不用,非要弄这种效果,就不吐槽了,还是想想怎么实现 废话就不多说了 直接上代码

824
来自专栏HansBug's Lab

1578: [Usaco2009 Feb]Stock Market 股票市场

1578: [Usaco2009 Feb]Stock Market 股票市场 Time Limit: 10 Sec  Memory Limit: 64 MB S...

25811
来自专栏lonelydawn的前端猿区

js多级联动示例(省份和城市二级联动)

本文描述的省份和城市二级联动下拉菜单,在js部分设置省份和城市信息,当然也可以将数据保存在数据库中,当需要使用的时候在读取。这里用的是js原生代码 在html文...

2145
来自专栏HansBug's Lab

1596: [Usaco2008 Jan]电话网络

1596: [Usaco2008 Jan]电话网络 Time Limit: 10 Sec  Memory Limit: 162 MB Submit: 601  ...

2567
来自专栏一个会写诗的程序员的博客

org.hamcrest.Matchers 类的方法签名

512
来自专栏河湾欢儿的专栏

定时器

var timer=setInterval(函数,毫秒); 重复执行 clearInterval(timer); 清空定时器

783

扫描关注云+社区