3年工作必备 装饰器模式


好啦,进入我们的主题,今天我给大家分享设计模式中的装饰器模式。用贴切的生活故事,以及真实项目场景来讲设计模式,最后用一句话来总结这个设计模式。

故事

古话说的好:人靠衣裳马靠鞍。下面先带大家来熟悉这句话的背景:

人靠衣装马靠鞍,狗配铃铛跑的欢出自沈自晋《望湖亭记》第十出:“虽然如此,佛靠金装,人靠衣装,打扮也是很要紧的。”《醒世恒言》卷一‧两县令竞义婚孤女:”常言道:’佛是金装,人是衣装,世人眼孔浅的多,只有皮相,没有骨相。’”俗语我们会说成人靠衣装马靠鞍。

这个经典故事,让我想起了一个设计模式:装饰器模式。

什么是装饰器模式呢?请听老田慢慢道来。

装饰器模式概述

装饰器模式(Decorator Pattern)也叫作包装器模式(Wrapper Pattern),指在不改变原有对象的基础上,动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活,属于结构型设计模式。

英文:

Attach additional responsibilities to an object dynamicallykeeping the same interface.Decorators provide a flexible alternativeto subclassing for extending functionality.

装饰器模式提供了比继承更有弹性的替代方案(扩展原有对象的功能)将功能附加到对象上。因此,装饰器模式的核心是功能扩展。使用装饰器模式可以透明且动态地扩展类的功能。

生活中的案例

一套毛坯房,没有装修之前,看起来非常难看,但只要稍微装修一番,那就漂亮多了,并且能洗澡、睡觉、做饭等,但本质还是房子。

一辆汽车,原本就是一辆代步的车,但是玛丽加大,配置提升,然后就成了豪车,但本质还是一辆代步的车。

一个女生,原本很平凡,长相一般,但是经过一番化妆,再穿点好看的衣服,然后就成了很多人心中的女神了。

总之,经过点装饰后,就是不一样了,功能增强了。

装饰器模式通用代码实现

我们还是用代码来实现一把,程序员都喜欢先搞个demo,然后再慢慢研究。

//抽象组件
public abstract class Component {
    public abstract void operation();
}
//具体组件
public class ConcreteComponent extends Component {
    @Override
    public void operation() {
        System.out.println("ConcreteComponent operation");
    }
}
//装饰器抽象
public abstract class Decorator extends Component {

    protected Component component;

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

    @Override
    public void operation() {
        component.operation();
    }
}
//具体装饰器
public class ConcreteDecorator extends Decorator {
    public ConcreteDecorator(Component component) {
        super(component);
    }

    @Override
    public void operation() {
        System.out.println("开始前搞点事");
        super.operation();
        System.out.println("结束后搞点事");
    }
}
//测试
public class Client {
    public static void main(String[] args) {
        Component component = new ConcreteDecorator(new ConcreteComponent());
        component.operation();
    }
}

运行结果:

开始前搞点事
ConcreteComponent operation
结束后搞点事

以上便是装饰器模式的通用代码实现,下面我们来分析一下。

装饰器模式UML图

3年工作必备 装饰器模式


从UML途中可以看出,其中的角色

装饰器模式中的角色

  • 抽象组件(Component):可以是一个接口或者抽象类,充当被装饰类的原始对象,规定了被装饰对象的行为。
  • 具体组件(ConcreteComponent):实现/继承Component的一个具体对象,即被装饰对象。
  • 抽象装饰器(Decorator):通用的装饰ConcreteComponent的装饰器,其内部必然有一个属性指向Component;其实现一般是一个抽象类,主要为了让其子类按照其构造形式传入一个Component,这是强制的通用行为。如果系统中装饰逻辑单一,则并不需要实现许多装饰器,可以直接省略该类,而直接实现一个具体装饰器即可。
  • 具体装饰器(ConcreteDecorator):Decorator的具体实现类,理论上,每个ConcreteDecorator都扩展了Component对象的一种功能。

小结

装饰器模式角色分配符合设计模式的里氏替换原则、依赖倒置原则,从而使得其具备很强的扩展性,最终满足开闭原则。

装饰器模式的实现原理是,让装饰器实现与被装饰类(例如ConcreteComponent)相同的接口(例如Component),使得装饰器与被扩展类类型一致,并在构造函数中传入该接口对象,然后在实现这个接口的被包装类对象的现有功能上添加新功能。由于装饰器与被包装类属于同一类型(均为Component),且构造函数的参数为其实现接口类(Component),因此装饰器模式具备嵌套扩展功能,这样就能使用装饰器模式一层一层地对底层被包装类进行功能扩展了。

实战

在实际开发中,都会存在系统与系统之间的调用,假如说我们现在有个支付功能,现在一切都是没问题的,但是 我们此时需要对发起支付前的请求参数和支付后的相应参数。进行统一处理,原功能不变,只是在原功能上做了一点扩展(增强)。

老功能代码如下:

/**
 * @author 田先生
 * @date 2021-06-02
 *
 * 欢迎关注公众号:java后端技术全栈
 */
public interface IOrderPayService {
    String payment(Long orderId, BigDecimal amount);
}
public class OrderPayServiceImpl implements IOrderPayService {

    @Override
    public String payment(Long orderId, BigDecimal amount) {
        //先调用余额查询是否足够
        System.out.println("发起支付,订单号:" + orderId + ", 支付金额:" + amount.toString());
        //调用支付系统
        String result = "订单id=" + orderId + "支付完成";
        System.out.println("支付结果:" + result);
        return result;
    }
}
public class OrderClient {
    public static void main(String[] args) {
        IOrderPayService orderPayService = new OrderPayServiceImpl();
        orderPayService.payment(10001L,new BigDecimal("5000"));
    }
}

运行输出:

发起支付,订单号:10001, 支付金额:5000
支付结果:订单id=10001支付完成

新需求,需要把这些请求参数和相应结果进行单独搜集处理,此时为了不影响原有功能,于是我们可以对其进行功能增强。

/**
 * @author 田先生
 * @date 2021-06-02
 *
 * 欢迎关注公众号:java后端技术全栈
 */
public class OrderPayDecorator implements IOrderPayService {

    private IOrderPayService orderPayService;

    public OrderPayDecorator(IOrderPayService orderPayService) {
        this.orderPayService = orderPayService;
    }

    @Override
    public String payment(Long orderId, BigDecimal amount) {
        System.out.println("把这个订单信息(发起支付)" + "订单id=" + orderId + "支付金额=" + amount.toString() + " 【发送给MQ】");
        String result = orderPayService.payment(orderId, amount);
        System.out.println("把订单支付结果信息" + result + " 【发送给MQ】");
        return result;
    }
}
public class OrderClient {
    public static void main(String[] args) {
        IOrderPayService orderPayService =new OrderPayDecorator(new OrderPayServiceImpl());
        orderPayService.payment(10001L,new BigDecimal("5000"));
    }
}

运行输出:

把这个订单信息(发起支付)订单id=10001支付金额=5000 【发送给MQ】
发起支付,订单号:10001, 支付金额:5000
支付结果:订单id=10001支付完成
把订单支付结果信息订单id=10001支付完成 【发送给MQ】

整个过程,大家有没有发现,我们并没动原有的代码,仅仅只是做了功能增强。

装饰器模式在新项目中基本上不会用到,通常都是在老项目中使用,因为已有的功能不变,只是做了一些功能增强。

大神们是怎么用的

装饰器设计模式在JDK源码、Spring源码以及Mybatis源码中都有。

JDK源码中

装饰器模式比较经典的应用就是 JDK 中的 java.io 包下,InputStream、OuputStream、Reader、Writer 及它们的子类。

以 InputStream 为例

  • FileInputStream 是 InputStream 的子类,用来读取文件字节流
  • BufferedInputStream 是 InputStream 的子类的子类,可缓存的字节流
  • DataInputStream 也是 InputStream 的子类的子类,可直接读取 Java 基本类型的字节流

UML图

3年工作必备 装饰器模式


DataInputStream 中构造器入参便是自己的父类(InputStream)。

3年工作必备 装饰器模式

如果希望提供一个可以读取文件 + 可缓存的字节流,使用继承方式,就需要派生 FileBufferedInputStream;

如果希望提供一个可以读取文件 + 直接读取基本类型的字节流,使用继承方式,就需要派生 FileDataInputStream。

字节流功能的增强还包括支持管道 pipe、字节数组 bytearray、字节对象 object、字节流字符流的转换 等维度,如果用继承方式,那类的层级与种类会多到爆炸。

为了解决问题,这边就使用了装饰器模式。

Spring源码中

在Spring中,我们可以尝试理解一下TransactionAwareCacheDecorator类,这个类主要用来处理事务缓存,代码如下。

public class TransactionAwareCacheDecorator implements Cache {
    private final Cache targetCache;
    //构造方法入参类型为自己的父类(接口类型)
    public TransactionAwareCacheDecorator(Cache targetCache) {
        Assert.notNull(targetCache, "Target Cache must not be null");
        this.targetCache = targetCache;
    }

    public Cache getTargetCache() {
        return this.targetCache;
    }
    //...
}

TransactionAwareCacheDecorator就是对Cache的一个包装,因此,这里也是使用了装饰器模式。

Mybatis源码中

MyBatis中关于Cache和CachingExecutor接口的实现类也使用了装饰者设计模式。Executor是MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护;CachingExecutor是一个Executor的装饰器,给一个Executor增加了缓存的功能。此时可以看做是对Executor类的一个增强,故使用装饰器模式是合适的。

在CachingExecutor 中

public class CachingExecutor implements Executor {
  //持有组件对象
  private Executor delegate;
  private TransactionalCacheManager tcm = new TransactionalCacheManager();
    //构造方法,传入组件对象
  public CachingExecutor(Executor delegate) {
    this.delegate = delegate;
    delegate.setExecutorWrapper(this);
  }
  @Override
  public int update(MappedStatement ms, Object parameterObject) throws SQLException {
      //转发请求给组件对象,可以在转发前后执行一些附加动作
    flushCacheIfRequired(ms);
    return delegate.update(ms, parameterObject);
  }
  //...
 }

总结

看完装饰器模式后,你是否有感觉,装饰器模式和代理模式非常的相像,下面我们就来做个对比。

1.装饰器模式可以理解为一种特殊的代理模式。

2.装饰器模式强调自身的功能扩展,透明的扩展(即用户想增强什么功能就增强什么功能),可动态定制的扩展。

3.代理模式强调的是代理过程的控制。

优点

  • 装饰器是继承的有力补充,比继承灵活,在不改变原有对象的情况下,动态地给一个对象扩展功能,即插即用。
  • 通过使用不同装饰类及这些装饰类的排列组合,可以实现不同效果。
  • 装饰器模式完全遵守开闭原则。

缺点

  • 会出现更多的代码、更多的类,增加程序的复杂性。
  • 动态装饰在多层装饰时会更复杂。

以上就是3年工作必备 装饰器模式的详细内容,更多请关注其它相关文章!