设计模式之装饰器模式
本文由老王将建好的书房计划请小王来帮忙,小王却想谋权篡位,老王通过教育他引出装饰器设计模式,第二部分针对老王提出的建设性意见实现装饰器模式,第三部分针对装饰器模式在Jdk中的IO、Spring中的缓存管理器、Mybatis的运用来加强我们的理解,第四部分说明装饰器模式和代理模式的区别及他们各自的应用场景。
一、引出问题
上篇文章对老王的书架改造以后,老王是相当的满意,看小王能力突出,这不老王又有了新的需求。
经过组合模式以后老王的书被管理的井井有条,但是随着书的增多,老王就有一些忙不过来了,老王就想让小王帮他处理一些额外的事,比如在买书之前打扫一下书房,在晚上的时候把书房的门锁一下;或者有人借书之前做一下记录,借书者还书以后小王接收一下,等等。
小王听完说这有何难,说完撸起袖子就准备改老王的代码。老王急忙拦住了他,你真是个呆瓜,我写的代码你凭什么要动,你改了会不会影响我的业务逻辑,平时让你多看书你不听,之前学的设计模式呢?不拿出来用,眼看着让他吃灰。
小王不好意思的挠挠头,翻出来了他的设计模式宝典,开始寻找合适的设计模式。
小王大喊有了,之前说过的代理模式可以很好的解决这个问题,代理模式可以动态的增强对象的一些特性,我准备使用代理模式完成这个需求。
老王听完止不住的摇摇头,看来你是打算谋权篡位了,你是想要我整个书房的权利呀!
老王解释说,代理模式是可以实现这个需求,但是在这个场景下显然代理模式不合适,代理模式是着重对对象的控制,而我们今天的需求是在该对象的基础之上增加他的一些功能,我们各自的业务独立发展互不干扰。
二、装饰器模式概念与使用
实际上,在原对象的基础之上增加其功能就是属于装饰器模式。
装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。
在装饰器模式中应该是有四个角色:
①Component抽象构件(老王抽象方法)
②ConcreteComponent 具体构件(老王实现方法)
③Decorator装饰角色(装饰者小王)
④ConcreteDecorator 具体装饰角色(装饰者小王实现方法)
在装饰器模式中,需要增强的类(被装饰者)要实现接口,装饰者继承被装饰者的接口,并将被装饰者的实例传进去,在具体装饰角色中调用被装饰者的方法,在其前后定义增强的方法,在实际应用中往往装饰角色和具体装饰角色合二为一。
我们看下具体的代码实现:
抽象构件:
/** * 书的抽象构件 * @author tcy * @Date 10-08-2022 */ public abstract class ComponentBook { /** * 借书 */ public abstract void borrowBook(); /** * 买书 */ public abstract void buyBook(); }
书的具体构件:
/** * 书的具体构件 * @author tcy * @Date 10-08-2022 */ public class ConcreteComponentBook extends ComponentBook{ @Override public void borrowBook() { System.out.println("老王的书借出去..."); } @Override public void buyBook() { System.out.println("老王的书买回来..."); } }
装饰角色:
/** * 书的装饰者 * @author tcy * @Date 10-08-2022 */ public class DecoratorBook extends ComponentBook{ private ComponentBook componentBook; DecoratorBook(ComponentBook componentBook){ this.componentBook=componentBook; } @Override public void borrowBook() { this.componentBook.borrowBook(); } @Override public void buyBook() { this.componentBook.buyBook(); } }
书的具体装饰角色:
/** * 子类里写了并且使用了无参的构造方法但是它的父类(祖先)中却至少有一个是没有无参构造方法的 * @author tcy * @Date 10-08-2022 */ public class ConcreteDecoratorBook1 extends DecoratorBook{ ConcreteDecoratorBook1(ComponentBook componentBook) { super(componentBook); } public void cleanRoom(){ System.out.println("打扫书房..."); } public void shutRoom(){ System.out.println("关闭书房..."); } public void recordBook(){ System.out.println("记录借出记录..."); } public void returnBook(){ System.out.println("收到借出去的书..."); } @Override public void buyBook() { this.cleanRoom(); super.buyBook(); this.shutRoom(); System.out.println("----------------------------"); } @Override public void borrowBook() { this.recordBook(); super.borrowBook(); this.returnBook(); System.out.println("----------------------------"); } }
如果读者的Java基础扎实,理解装饰器还是比较轻松的,装饰器的实现方式很直观,需要特别指出的是,在书的具体装饰角色中,要显示的定义一个构造方法。
基础不太扎实的读者可能会有一个疑问,在Java的类中默认不是会有一个无参的构造方法吗?为什么这里还需要定义呢?
在java中一个类只要有父类,那么在它实例化的时候,一定是从顶级的父类开始创建。
也就是说当你用子类的无参构造函数创建子类对象时,会去先递归调用父类的无参构造方法,这时候如果某个类的父类没有无参构造方法就会编译出差。所以我们在子类中可以手动定义一个无参方法,或者在父类中显示的定义一个构造方法。
客户端:
/** * @author tcy * @Date 09-08-2022 */ public class { public static void main(String[] args) { ComponentBook componentBook=new ConcreteComponentBook(); componentBook=new ConcreteDecoratorBook1(componentBook); componentBook.borrowBook(); componentBook.buyBook(); } }
方法调用后我们可以看到执行结果:
记录借出记录... 老王的书借出去... 收到借出去的书... ---------------------------- 打扫书房... 老王的书买回来... 关闭书房... ----------------------------
在老王借书和买书的这个事件中,成功的织入进去小王的方法,这样也就实现了装饰器模式。
为了加强理解我们接着看装饰器模式在我们经常接触的源码中的运用。
三、应用
1、jdk中的应用IO
装饰器在java中最典型的应用就是IO,我们知道在IO家族中有各种各样的流,而流往往都是作用在子类之上,然后增加其附加功能,我们以InputStream 举例。
InputStream 是字节输入流,此抽象类是表示字节输入流的所有类的超类。
FileInputStream是InputStream 的一个实现父类,BufferedInputStream是FileInputStream的实现父类。
实际BufferedInputStream就是装饰者,InputStream 就是抽象构件,FileInputStream是具体构件,BufferedInputStream就是对FileInputStream进行了包装。
我们看具体的应用:
FileInputStream fileInputStream = new FileInputStream(filePath); BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
直接将FileInputStream的实例传给BufferedInputStream的构造方法,就能调用BufferedInputStream增强的一些方法了。
我们具体看BufferedInputStream装饰器类:
public class BufferedInputStream extends FilterInputStream { private static int DEFAULT_BUFFER_SIZE = 8192; protected int marklimit; public BufferedInputStream(InputStream in) { this(in, DEFAULT_BUFFER_SIZE); } ... //增强的方法 private void fill() throws IOException { byte[] buffer = getBufIfOpen(); if (markpos < 0) pos = 0; /* no mark: throw away the buffer */ else if (pos >= buffer.length) /* no room left in buffer */ if (markpos > 0) { /* can throw away early part of the buffer */ int sz = pos - markpos; System.arraycopy(buffer, markpos, buffer, 0, sz); pos = sz; markpos = 0; } else if (buffer.length >= marklimit) { markpos = -1; /* buffer got too big, invalidate mark */ pos = 0; /* drop buffer contents */ } else if (buffer.length >= MAX_BUFFER_SIZE) { throw new OutOfMemoryError("Required array size too large"); } else { /* grow buffer */ int nsz = (pos <= MAX_BUFFER_SIZE - pos) ? pos * 2 : MAX_BUFFER_SIZE; if (nsz > marklimit) nsz = marklimit; byte nbuf[] = new byte[nsz]; System.arraycopy(buffer, 0, nbuf, 0, pos); if (!bufUpdater.compareAndSet(this, buffer, nbuf)) { // Can't replace buf if there was an async close. // Note: This would need to be changed if fill() // is ever made accessible to multiple threads. // But for now, the only way CAS can fail is via close. // assert buf == null; throw new IOException("Stream closed"); } buffer = nbuf; } count = pos; int n = getInIfOpen().read(buffer, pos, buffer.length - pos); if (n > 0) count = n + pos; } ...
BufferedInputStream类中有许多其他的方法,就是对FileInputStream类的增强。
2、Spring中的运用
Spring使用装饰器模式有两个典型的特征,一个是类名中含有Wrapper,另一类是含有Decorator,功能也即动态的给某些类增加一些额外的功能。
TransactionAwareCacheDecorator是处理spring有事务的时候缓存的类,我们在使用spring的cache注解实现缓存的时候,当出现事务的时候,那么缓存的同步性就需要做相应的处理了,于是就有了这个装饰者。
public class TransactionAwareCacheDecorator implements Cache { //抽象构件 private final Cache targetCache; /** * Create a new TransactionAwareCache for the given target Cache. * @param targetCache the target Cache to decorate */ public TransactionAwareCacheDecorator(Cache targetCache) { Assert.notNull(targetCache, "Target Cache must not be null"); this.targetCache = targetCache; } /**直接调用未增强 * Return the target Cache that this Cache should delegate to. */ public Cache getTargetCache() { return this.targetCache; } //直接调用未增强 @Override public String getName() { return this.targetCache.getName(); } //直接调用未增强 @Override public Object getNativeCache() { return this.targetCache.getNativeCache(); } //直接调用未增强 @Override @Nullable public ValueWrapper get(Object key) { return this.targetCache.get(key); } //直接调用未增强 @Override public <T> T get(Object key, @Nullable Class<T> type) { return this.targetCache.get(key, type); } //直接调用未增强 @Override @Nullable public <T> T get(Object key, Callable<T> valueLoader) { return this.targetCache.get(key, valueLoader); } //先进行判断确定是否需要增强 @Override public void put(final Object key, @Nullable final Object value) { if (TransactionSynchronizationManager.isSynchronizationActive()) { TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { @Override public void afterCommit() { TransactionAwareCacheDecorator.this.targetCache.put(key, value); } }); } else { this.targetCache.put(key, value); } } }
Cache是抽象构件,TransactionAwareCacheDecorator就是装饰者,而Cache的实现类就是具体构件。
因为并非所有的方法都会使用事务,有的普通方法就不需要装饰,有的就需要,所以就使用了装饰者模式来完成。
比如put()方法:
@Override public void put(final Object key, @Nullable final Object value) { if (TransactionSynchronizationManager.isSynchronizationActive()) { TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { @Override public void afterCommit() { TransactionAwareCacheDecorator.this.targetCache.put(key, value); } }); } else { this.targetCache.put(key, value); } }
会在put前判断是否开启了事务TransactionSynchronizationManager.isSynchronizationActive(),如果开启事务就调用一下额外的方法,如果没有开始事务就调用默认的方法。
我们举的这个例子调用的就是 TransactionSynchronizationManager.registerSynchronization()方法,也即是为当前线程注册一个新的事务同步。
在Spring中将装饰角色和具体装饰角色合二为一,直接在装饰者中实现要增加的方法。
3、MyBatista的运用
了解过MyBatis的大致执行流程的读者应该知道,Executor是MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护;CachingExecutor是一个Executor的装饰器,给一个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); } //... }
Executor就是抽象构件,BaseExecutor是具体构件的实现,CachingExecutor就是装饰角色,那具体装饰角色在哪呢?
实际中具体装饰角色直接在装饰角色中集成了,并没有将具体装饰角色完全独立出来。
另外,Mybatis的一级缓存和二级缓存也是使用的装饰者模式,有兴趣的读者可以拉取Mybatis的源代码本地进行调试研究。
四、总结
到此为止,我们就将装饰器模式的内容讲解清楚了,看到这读者可能发现,针对某一类需求可能会有很多设计模式都能完成需求,但一定是有最合适的那一个,就像我们今天举的例子无论是用装饰器模式还是代理模式都可以实现这个需求。
但我们看代理模式中我们列举的例子是以租房做例子,中介将房子的权利完全移交过去,中介完全控制房子做一些改造,今天书房的需求只是让小王来帮忙的,还是以老王为主体,小王只是做一些附加。
装饰器模式就是在瓶里面插了一朵花,而代理模式是把瓶子都给人家了,让人家随便折腾。
如果我们的需求是日志收集、拦截器,代理模式是最适合的。如果是动态的增加对象的功能、限制对象的执行条件、参数控制和检查等使用适配器模式就更加合适了。