基于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 条评论
登录 后参与评论

相关文章

来自专栏coolblog.xyz技术专栏

Spring AOP 源码分析系列文章导读

前一段时间,我学习了 Spring IOC 容器方面的源码,并写了数篇文章对此进行讲解。在写完 Spring IOC 容器源码分析系列文章中的最后一篇后,没敢懈...

833
来自专栏Java成长之路

Java内存模型

多任务处理在现代计算机操作系统中几乎已经是一项必备的功能了。计算机cpu的运算速度与它的存储和通信子系统速度的差距太大,大量的时间都花费在磁盘I/O、网络通信或...

571
来自专栏移动开发面面观

Android并发备忘录

1443
来自专栏Jimoer

JVM学习记录-线程安全与锁优化(二)

高效并发是程序员们写代码时一直所追求的,HotSpot虚拟机开发团队也为此付出了很多努力,为了在线程之间更高效地共享数据,以及解决竞争问题,HotSpot开发团...

832
来自专栏Linyb极客之路

JVM系列一:JVM内存组成及分配

java内存组成介绍:堆(Heap)和非堆(Non-heap)内存 按照官方的说法:“Java 虚拟机具有一个堆,堆是运行时数据区域,所有类实例和...

3346
来自专栏Java成长之路

先行发生原则(happens-before)介绍

如果Java内存模型中所有的有序性都仅仅靠volatile和synchronized来完成,那么有一些操作将会变得很烦琐,但是我们在编写Java并发代码的时候并...

662
来自专栏牛肉圆粉不加葱

【源码剖析】- Spark 新旧内存管理方案(上)

Spark 作为一个以擅长内存计算为优势的计算引擎,内存管理方案是其非常重要的模块。作为使用者的我们,搞清楚 Spark 是如何管理内存的,对我们编码、调试及优...

771
来自专栏coolblog.xyz技术专栏

MyBatis 源码分析 - 缓存原理

在 Web 应用中,缓存是必不可少的组件。通常我们都会用 Redis 或 memcached 等缓存中间件,拦截大量奔向数据库的请求,减轻数据库压力。作为一个重...

581
来自专栏Android中高级开发

Android并发编程 开篇

该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列。该系列引用了《Android开发艺术探索...

451
来自专栏JAVA高级架构

JAVA后端面试100 Q&A之第一篇

891

扫码关注云+社区