【深入设计模式】装饰模式—什么是装饰模式?装饰模式在源码中的应用

1,530 阅读8分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

我们使用的手机最开始只能打电话、发短信,后来又来彩屏之后手机开始能够拍照、录像,再后来发展到智能机时代,不仅能够通话、发短信、拍照录像,还能够安装 APP、视频通话、玩游戏等等新功能,手机的迭代更新总是在原有功能的基础上进行扩展丰富。在开发中我们也经常会遇到这样的需求,保留现有功能,并在其基础上进行扩展,同时还不能影响到已有功能,这个时候就需要使用到这里所讲的装饰模式。

1333514.gif

1. 装饰模式

1.1 装饰模式简介

当我们需要在现有功能之上对其进行扩展时,可能首先想到的是使用子类的方式完成,如果我们使用子类进行扩展会出现很多的子类,并且在调用方使用这些子类时也会构建很多次,同时这个调用的过程也开放给了调用者,那么如果这些扩展类的调用在业务上存在顺序,就可能造成业务执行混乱的情况。这个时候我们可以考虑使用装饰模式来处理这样的问题。

装饰模式能够在不改变原来对象结构和功能的前提下,动态的给对象增加新的功能,相比于使用子类的扩展的方式,装饰模式更加的灵活。

1.2 装饰模式结构

装饰模式需要涉及到以下几个角色:

  • 抽象组件(Component):定义一个系列对象应该有的功能,今后的扩展调用均基于该接口实现
  • 具体组件(SpecificComponent):抽象组件的具体实现,装饰类将对该具体实现进行扩展
  • 抽象装饰类(Decorator):装饰类将通过继承该类来进行功能扩展,并且在该类中包含具体组件实例
  • 具体装饰类(DecorateComponent):实现抽象装饰类的具体装饰类,用于对抽象装饰类中的具体组件功能调用的扩展

结构图如下:

image-20220625213854540.png

// 抽象组件
public interface Component {
    void operation();
}

//具体组件
public class SpecificComponent implements Component {
    @Override
    public void operation() {
        System.out.println("component base function.");
    }
}

// 抽象装饰类,实现抽象组件
public abstract class Decorator implements Component {
    private Component component;

    public Decorator(Component component) {
        this.component = component;
    }

    @Override
    public void operation() {
        component.operation();
    }
}

// 具体装饰类 1,继承抽象装饰类
public class DecorateComponent1 extends Decorator{
    public DecorateComponent1(Component component) {
        super(component);
    }

    @Override
    public void operation() {
        System.out.println("operation before base function.");
        super.operation();
        System.out.println("operation after base function.");
    }
}

// 具体装饰类 2,继承抽象装饰类
public class DecorateComponent2 extends Decorator {
    public DecorateComponent2(Component component) {
        super(component);
    }

    @Override
    public void operation() {
        System.out.println("operation before base function.");
        super.operation();
        System.out.println("operation after base function.");
    }
}

调用装饰模式的代码如下:

public static void main(String[] args) {
    SpecificComponent component = new SpecificComponent();
    component.operation();

    System.out.println("==================");
    System.out.println("running decorator1.");
    DecorateComponent1 decorator1 = new DecorateComponent1(component);
    decorator1.operation();

    System.out.println("==================");
    System.out.println("running decorator2.");
    DecorateComponent2 decorator2 = new DecorateComponent2(component);
    decorator2.operation();
}

控制台输出如下:

component base function.
==================
running decorator1.
operation before base function.
component base function.
operation after base function.
==================
running decorator2.
operation before base function.
component base function.
operation after base function.

我们可以看到示例代码中,首先创建 SpecificComponent 并调用 operation() 方法,此时运行基本功能。然后我们再将 SpecificComponent 传入两个装饰类 DecorateComponent1 和 DecorateComponent2 并调用这两个对象的 operation() 方法,可以看到不仅执行了基本功能代码,还可以在基本功能代码的前后添加新的功能,从而实现在不改变现有功能的基础上对其进行增强。在平时开发中,往往会将抽象组件去除,让抽象装饰类直接继承具体组件,从而简化代码的结构。

1655826890701.png

1.3 装饰模式示例

我们以手机的发展为例进行,首先我们从最原始的大哥大开始发展,这个大哥大就只有通话的功能,随着时代的进步大哥大也进化成了小型手机,这时的手机能够打电话、发短信。只有进入到了智能手机时代,这个时候的手机能够安装 app 、拍照、视频聊天等等功能。虽然手机在二十多年的时间里疯狂发展,功能也变得花里胡哨,但是其本质永远都是手机。所以在这个场景下我们可以用装饰模式,在手机这个组件的基础上对其进行功能扩展,代码如下:

// 基本组件 Phone
public interface Phone {
    // 这个电话具有的功能
    void functions();
}
// 具体组件 MobilePhone,实现基本组件的功能即可以打电话
public class MobilePhone implements Phone {
    @Override
    public void functions() {
        System.out.println("this phone can make calls. ");
    }
}
// 抽象装饰器
public abstract class PhoneDecorator implements Phone {
    Phone phone;

    public PhoneDecorator(Phone phone) {
        this.phone = phone;
    }

    @Override
    public void functions() {
        phone.functions();
    }
}
// 普通手机,只能够打电话和发短信
public class OrdinaryMobilePhone extends PhoneDecorator{
    public OrdinaryMobilePhone(Phone phone) {
        super(phone);
    }

    @Override
    public void functions() {
        super.functions();
        System.out.println("this phone can send message. ");
    }
}
// 智能手机,拥有上一代的功能,并且还能够安装 app、拍照
public class IntelligentMobilePhone extends PhoneDecorator {

    public IntelligentMobilePhone(Phone phone) {
        super(phone);
    }

    @Override
    public void functions() {
        super.functions();
        System.out.println("this phone can install apps. ");
        System.out.println("this phone can take photos. ");
    }
}

在这段代码中我们对上面的手机发展进行模拟,首先定义一个基本的组件 Phone,表明这是一个手机的发展史,然后创建具体组件 MobilePhone,它的功能只有一个——打电话。再定义一个抽象装饰类 PhoneDecorator,最后再抽象装饰类基础上定义两个具体装饰类——普通手机(OrdinaryMobilePhone)和智能手机(IntelligentMobilePhone),普通手机不光有其上一代的功能,还能够发短信,智能手机更强大了,能够装 app、拍照等功能。接下来对这些类进行调用,代码如下:

public static void main(String[] args) {
    // 第一代手机的功能
    System.out.println("=========first generation==========");
    MobilePhone mobilePhone = new MobilePhone();
    mobilePhone.functions();
    // 第二代手机的功能
    System.out.println("=========second generation==========");
    OrdinaryMobilePhone ordinaryMobilePhone = new OrdinaryMobilePhone(mobilePhone);
    ordinaryMobilePhone.functions();
    System.out.println("=========third generation==========");
    // 第三代手机的功能
    IntelligentMobilePhone intelligentMobilePhone = new IntelligentMobilePhone(ordinaryMobilePhone);
    intelligentMobilePhone.functions();
}

控制台输出如下:

=========first generation==========
this phone can make calls. 
=========second generation==========
this phone can make calls. 
this phone can send message. 
=========third generation==========
this phone can make calls. 
this phone can send message. 
this phone can install apps. 
this phone can take photos. 

可以看到我们把一二三代的手机功能都打印了出来。在调用的代码里,首先是创建了具体组件对象 mobilePhone,然后再将 mobilePhone 作为参数传给了普通手机的构造函数构造 ordinaryMobilePhone 对象,这样 ordinaryMobilePhone 对象就能够拥有 mobilePhone 的功能,同时还有自身代码里的发送短信功能。最后我们将 ordinaryMobilePhone 对象作为参数传入智能手机构造函数中构造 intelligentMobilePhone 对象,此时的 intelligentMobilePhone 对象不仅拥有 mobilePhone 和 ordinaryMobilePhone 对象的功能,同时还有自身的安装 app、拍照功能。

618.jpg

2. 装饰模式在源码中的应用

2.1 装饰模式在 JDK 中的应用

装饰模式在 JDK 中的应用最著名的就是 IO 流了,下面就是我们常用的 InputStream 类和InputStreamReader 类体系。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jgNDH7vB-1656341939501)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220625225754032.png)]

在 InputStream 体系中,读取流的基本方式是使用 InputStream 类来一个一个地读取字节文件,在 InputStream 读取文件的基础上衍生出了FileInputStream、ByteArrayInputStream 等,使其不仅能够读取字节,还能够读取字节数组、文件等。

使用 InputStream 读取文件多少有点麻烦了,在 Reader 体系中,有 InputStreamReader 这个类,将 InputStream 作为参数传入 Reader 的构造函数,Reader 的具体装饰类便能对读进行增强,从而读取具体传入的 InputStream。

2.2 装饰模式在 MyBatis中的应用

在 Mybatis 源码中有一个类叫 TransactionalCache,该类实现了 Cache 接口,而该类构造方法中将 Cache 对象作为参数进行构造,代码如下:

public class TransactionalCache implements Cache {

  private static final Log log = LogFactory.getLog(TransactionalCache.class);

  private final Cache delegate;
  private boolean clearOnCommit;
  private final Map<Object, Object> entriesToAddOnCommit;
  private final Set<Object> entriesMissedInCache;

  public TransactionalCache(Cache delegate) {
    this.delegate = delegate;
    this.clearOnCommit = false;
    this.entriesToAddOnCommit = new HashMap<>();
    this.entriesMissedInCache = new HashSet<>();
  }
  ……
}

TransactionalCache 类中同时实现了 Cache 的方法,于是在 getObject 这个方法中,我们可以看到首先调用了传入的 Cache 对象的 getObject 对象,然后对没获取到对象时进行处理。这就是 MyBatis 的二级缓存的事务缓冲区,使用了装饰模式对 Cache 的方法进行了增强,使其能够满足事务相关缓存功能需求。

public Object getObject(Object key) {
    // issue #116
    Object object = delegate.getObject(key);
    if (object == null) {
        entriesMissedInCache.add(key);
    }
    // issue #146
    if (clearOnCommit) {
        return null;
    } else {
        return object;
    }
}

3. 总结

装饰模式能够动态的对已有功能添加新功能,开发中如果我们需要对现有功能进行增强时,可以考虑使用装饰模式。在对现有功能进行添加新功能时,如果在原代码上面进行修改,一方面增加代码逻辑复杂度,另一方面会影响到已使用到这部分功能的业务,使用装饰模式可以在新的装饰类中添加新的变量、新逻辑及新方法,并且不会对现有部分逻辑造成影响,这样将功能的核心职责与额外添加职责进行分离。在使用时也只需将需要增强的核心功能作为参数传入装饰类中,便能够调用到核心功能与装饰功能。

1656341651185.png