装饰器模式
装饰器模式(Decorator Pattern)又称为包装(Wrapper)模式,允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。
这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。
我们通过下面的实例来演示装饰器模式的用法。其中,我们将把一个形状装饰上不同的颜色,同时又不改变形状类。
介绍
意图:动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活。
主要解决:一般的,我们为了扩展一个类经常使用继承方式实现,由于继承为类引入静态特征,并且随着扩展功能的增多,子类会很膨胀。
何时使用:在不想增加很多子类的情况下扩展类。
如何解决:将具体功能职责划分,同时继承装饰者模式。
关键代码: 1、Component 类充当抽象角色,不应该具体实现。 2、修饰类引用和继承 Component 类,具体扩展类重写父类方法。
应用实例: 1、孙悟空有 72 变,当他变成"庙宇"后,他的根本还是一只猴子,但是他又有了庙宇的功能。 2、不论一幅画有没有画框都可以挂在墙上,但是通常都是有画框的,并且实际上是画框被挂在墙上。在挂在墙上之前,画可以被蒙上玻璃,装到框子里;这时画、玻璃和画框形成了一个物体。
优点:装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。
缺点:多层装饰比较复杂。
使用场景: 1、扩展一个类的功能。 2、动态增加功能,动态撤销。
注意事项:可代替继承。
角色
Component(抽象构件):它是具体构件和抽象装饰类的共同父类,声明了在具体构件中实现的业务方法,它的引入可以使客户端以一致的方式处理未被装饰的对象以及装饰之后的对象,实现客户端的透明操作。
ConcreteComponent(具体构件):它是抽象构件类的子类,用于定义具体的构件对象,实现了在抽象构件中声明的方法,装饰器可以给它增加额外的职责(方法)。
Decorator(抽象装饰类):它也是抽象构件类的子类,用于给具体构件增加职责,但是具体职责在其子类中实现。它维护一个指向抽象构件对象的引用,通过该引用可以调用装饰之前构件对象的方法,并通过其子类扩展该方法,以达到装饰的目的。
ConcreteDecorator(具体装饰类):它是抽象装饰类的子类,负责向构件添加新的职责。每一个具体装饰类都定义了一些新的行为,它可以调用在抽象装饰类中定义的方法,并可以增加新的方法用以扩充对象的行为。
由于具体构件类和装饰类都实现了相同的抽象构件接口,因此装饰模式以对客户透明的方式动态地给一个对象附加上更多的责任,换言之,客户端并不会觉得对象在装饰前和装饰后有什么不同。装饰模式可以在不需要创造更多子类的情况下,将对象的功能加以扩展。
装饰模式的核心在于抽象装饰类的设计。
示例
现在有这么一个场景:
1、有一批厨师,简单点吧,就全是中国厨师,他们有一个共同的动作是做晚饭
2、这批厨师做晚饭前的习惯不同,有些人喜欢做晚饭前洗手、有些人喜欢做晚饭前洗头
那么,按照装饰器模式,先抽象出抽象构建角色,Cook接口
1 public interface Cook { 2 3 public void cookDinner(); 4 5 }
具体构建角色,中国厨师:
1 public class ChineseCook implements Cook { 2 3 @Override 4 public void cookDinner() { 5 System.out.println("做晚饭"); 6 } 7 8 }
定义一个装饰器角色,具体的工作具体装饰器去实现,这样,比如美国厨师做晚饭前也先洗手或者先洗头,这两个动作就可以做到复用,装饰器角色定义为FilterCook,很简单,实现Cook接口并持有Cook的引用:
1 public abstract class FilterCook implements Cook { 2 3 protected Cook cook; 4 5 }
最后定义一个具体装饰角色,该洗手的洗手,该洗头的洗头:
1 public class WashHandsCook extends FilterCook { 2 3 public WashHandsCook(Cook cook) { 4 this.cook = cook; 5 } 6 7 @Override 8 public void cookDinner() { 9 System.out.println("先洗手"); 10 cook.cookDinner(); 11 } 12 13 }
1 public class WashHearCook extends FilterCook { 2 3 public WashHearCook(Cook cook) { 4 this.cook = cook; 5 } 6 7 @Override 8 public void cookDinner() { 9 System.out.println("先洗头"); 10 cook.cookDinner(); 11 } 12 13 public void cookLunch() { 14 System.out.println("做午饭"); 15 } 16 }
测试
1 public class Test { 2 3 public static void main(String[] args) { 4 Cook cook0 = new WashHandsCook(new ChineseCook()); 5 Cook cook1 = new WashHearCook(new ChineseCook()); 6 7 cook0.cookDinner(); 8 cook1.cookDinner(); 9 } 10 }
运行结果为
先洗手
做晚饭
先洗头
做晚饭
简单的一个例子,实现了装饰器模式的两个功能点:
- 客户端只定义了Cook接口,并不关心具体实现
- 给Chinese增加上了洗头和洗手的动作,且洗头和洗手的动作,可以给其他国家的厨师类复用
透明装饰模式与半透明装饰模式
在上面的示例中,装饰后的对象是通过抽象构建类类型Cook 的变量来引用的,在洗头装饰器这个类中我们新增了 cookLunch
方法,如果此时我们想要单独调用该方法是调用不到的
除非引用变量的类型改为 WashHearCook
,这样就可以调用了。
在实际使用过程中,由于新增行为可能需要单独调用,因此这种形式的装饰模式也经常出现,这种装饰模式被称为半透明(Semi-transparent)装饰模式,而标准的装饰模式是透明(Transparent)装饰模式。
(1) 透明装饰模式
在透明装饰模式中,要求客户端完全针对抽象编程,装饰模式的透明性要求客户端程序不应该将对象声明为具体构件类型或具体装饰类型,而应该全部声明为抽象构件类型。
(2) 半透明装饰模式
透明装饰模式的设计难度较大,而且有时我们需要单独调用新增的业务方法。为了能够调用到新增方法,我们不得不用具体装饰类型来定义装饰之后的对象,而具体构件类型还是可以使用抽象构件类型来定义,这种装饰模式即为半透明装饰模式。
半透明装饰模式可以给系统带来更多的灵活性,设计相对简单,使用起来也非常方便;但是其最大的缺点在于不能实现对同一个对象的多次装饰,而且客户端需要有区别地对待装饰之前的对象和装饰之后的对象。
装饰模式注意事项
(1) 尽量保持装饰类的接口与被装饰类的接口相同,这样,对于客户端而言,无论是装饰之前的对象还是装饰之后的对象都可以一致对待。这也就是说,在可能的情况下,我们应该尽量使用透明装饰模式。
(2) 尽量保持具体构件类是一个“轻”类,也就是说不要把太多的行为放在具体构件类中,我们可以通过装饰类对其进行扩展。
(3) 如果只有一个具体构件类,那么抽象装饰类可以作为该具体构件类的直接子类。
装饰者模式总结
装饰模式的主要优点如下:
- 对于扩展一个对象的功能,装饰模式比继承更加灵活性,不会导致类的个数急剧增加。
- 可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的具体装饰类,从而实现不同的行为。
- 可以对一个对象进行多次装饰,通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合,得到功能更为强大的对象。
- 具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,原有类库代码无须改变,符合 “开闭原则”。
装饰模式的主要缺点如下:
- 使用装饰模式进行系统设计时将产生很多小对象,这些对象的区别在于它们之间相互连接的方式有所不同,而不是它们的类或者属性值有所不同,大量小对象的产生势必会占用更多的系统资源,在一定程序上影响程序的性能。
- 装饰模式提供了一种比继承更加灵活机动的解决方案,但同时也意味着比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为繁琐。
适用场景:
- 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
- 当不能采用继承的方式对系统进行扩展或者采用继承不利于系统扩展和维护时可以使用装饰模式。不能采用继承的情况主要有两类:第一类是系统中存在大量独立的扩展,为支持每一种扩展或者扩展之间的组合将产生大量的子类,使得子类数目呈爆炸性增长;第二类是因为类已定义为不能被继承(如Java语言中的final类)。
装饰器模式和适配器模式的区别
其实适配器模式也是一种包装(Wrapper)模式,它们看似都是起到包装一个类或对象的作用,但是它们使用的目的非常不一样:
1、适配器模式的意义是要将一个接口转变成另外一个接口,它的目的是通过改变接口来达到重复使用的目的
2、装饰器模式不要改变被装饰对象的接口,而是恰恰要保持原有的借口哦,但是增强原有接口的功能,或者改变元有对象的处理方法而提升性能
所以这两种设计模式的目的是不同的。
观察者模式在Java中的应用及解读
Java I/O中的装饰者模式
使用 Java I/O 的时候总是有各种输入流、输出流、字符流、字节流、过滤流、缓冲流等等各种各样的流,不熟悉里边的设计模式的话总会看得云里雾里的,现在通过设计模式的角度来看 Java I/O,会好理解很多。
先用一幅图来看看Java I/O到底是什么,下面的这幅图生动的刻画了Java I/O的作用。
由上图可知在Java中应用程序通过输入流(InputStream)的Read方法从源地址处读取字节,然后通过输出流(OutputStream)的Write方法将流写入到目的地址。
流的来源主要有三种:本地的文件(File)、控制台、通过socket实现的网络通信
下面的图可以看出Java中的装饰者类和被装饰者类以及它们之间的关系,这里只列出了InputStream中的关系:
由上图可以看出只要继承了FilterInputStream的类就是装饰者类,可以用于包装其他的流,装饰者类还可以对装饰者和类进行再包装。
这里总结几种常用流的应用场景:
流名称 | 应用场景 |
ByteArrayInputStream | 访问数组,把内存中的一个缓冲区作为 InputStream 使用,CPU从缓存区读取数据比从存储介质的速率快10倍以上 |
StringBufferInputStream | 把一个 String 对象作为。InputStream。不建议使用,在转换字符的问题上有缺陷 |
FileInputStream | 访问文件,把一个文件作为 InputStream ,实现对文件的读取操作 |
PipedInputStream | 访问管道,主要在线程中使用,一个线程通过管道输出流发送数据,而另一个线程通过管道输入流读取数据,这样可实现两个线程间的通讯 |
SequenceInputStream | 把多个 InputStream 合并为一个 InputStream . “序列输入流”类允许应用程序把几个输入流连续地合并起来 |
DataInputStream | 特殊流,读各种基本类型数据,如byte、int、String的功能 |
ObjectInputStream | 对象流,读对象的功能 |
PushBackInputStream | 推回输入流,可以把读取进来的某些数据重新回退到输入流的缓冲区之中 |
BufferedInputStream | 缓冲流,增加了缓冲功能 |
下面看一下Java中包装流的实例:
1 import java.io.BufferedInputStream; 2 import java.io.DataInputStream; 3 import java.io.FileInputStream; 4 import java.io.IOException; 5 public class StreamDemo { 6 public static void main(String[] args) throws IOException{ 7 DataInputStream in=new DataInputStream(new BufferedInputStream(new FileInputStream("D:\\hello.txt"))); 8 while(in.available()!=0) { 9 System.out.print((char)in.readByte()); 10 } 11 in.close(); 12 } 13 }
输出结果
hello world!
hello Java I/O!
上面程序中对流进行了两次包装,先用 BufferedInputStream将FileInputStream包装成缓冲流也就是给FileInputStream增加缓冲功能,再DataInputStream进一步包装方便数据处理。
如果要实现一个自己的包装流,根据上面的类图,需要继承抽象装饰类 FilterInputStream
譬如来实现这样一个操作的装饰者类:将输入流中的所有小写字母变成大写字母
1 import java.io.FileInputStream; 2 import java.io.FilterInputStream; 3 import java.io.IOException; 4 import java.io.InputStream; 5 6 public class UpperCaseInputStream extends FilterInputStream { 7 protected UpperCaseInputStream(InputStream in) { 8 super(in); 9 } 10 11 @Override 12 public int read() throws IOException { 13 int c = super.read(); 14 return (c == -1 ? c : Character.toUpperCase(c)); 15 } 16 17 @Override 18 public int read(byte[] b, int off, int len) throws IOException { 19 int result = super.read(b, off, len); 20 for (int i = off; i < off + result; i++) { 21 b[i] = (byte) Character.toUpperCase((char) b[i]); 22 } 23 return result; 24 } 25 26 public static void main(String[] args) throws IOException { 27 int c; 28 InputStream in = new UpperCaseInputStream(new FileInputStream("D:\\hello.txt")); 29 try { 30 while ((c = in.read()) >= 0) { 31 System.out.print((char) c); 32 } 33 } finally { 34 in.close(); 35 } 36 } 37 }
输出
HELLO WORLD!
HELLO JAVA I/O!
整个Java IO体系都是基于字符流(InputStream/OutputStream) 和 字节流(Reader/Writer)作为基类,下面画出OutputStream、Reader、Writer的部分类图,更多细节请查看其它资料
spring cache 中的装饰者模式
看 org.springframework.cache.transaction
包下的 TransactionAwareCacheDecorator
这个类
1 public class TransactionAwareCacheDecorator implements Cache { 2 private final Cache targetCache; 3 4 public TransactionAwareCacheDecorator(Cache targetCache) { 5 Assert.notNull(targetCache, "Target Cache must not be null"); 6 this.targetCache = targetCache; 7 } 8 9 public <T> T get(Object key, Class<T> type) { 10 return this.targetCache.get(key, type); 11 } 12 13 public void put(final Object key, final Object value) { 14 // 判断是否开启了事务 15 if (TransactionSynchronizationManager.isSynchronizationActive()) { 16 // 将操作注册到 afterCommit 阶段 17 TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { 18 public void afterCommit() { 19 TransactionAwareCacheDecorator.this.targetCache.put(key, value); 20 } 21 }); 22 } else { 23 this.targetCache.put(key, value); 24 } 25 } 26 // ...省略... 27 }
该类实现了 Cache 接口,同时将 Cache 组合到类中成为了成员属性 targetCache,所以可以大胆猜测 TransactionAwareCacheDecorator 是一个装饰类,不过这里并没有抽象装饰类,且
TransactionAwareCacheDecorator 没有子类,这里的装饰类关系并没有Java I/O 中的装饰关系那么复杂
该类的主要功能:通过 Spring 的 TransactionSynchronizationManager 将其 put/evict/clear 操作与 Spring 管理的事务同步,仅在成功的事务的 after-commit 阶段执行实际的缓存 put/evict/clear 操作。如果
没有事务是 active 的,将立即执行 put/evict/clear 操作
spring session 中的装饰者模式
类 ServletRequestWrapper
的代码如下:
1 public class ServletRequestWrapper implements ServletRequest { 2 private ServletRequest request; 3 4 public ServletRequestWrapper(ServletRequest request) { 5 if (request == null) { 6 throw new IllegalArgumentException("Request cannot be null"); 7 } 8 this.request = request; 9 } 10 11 @Override 12 public Object getAttribute(String name) { 13 return this.request.getAttribute(name); 14 } 15 //...省略... 16 }
可以看到该类对 ServletRequest 进行了包装,这里是一个装饰者模式,再看下图,spring session 中 SessionRepositoryFilter 的一个内部类 SessionRepositoryRequestWrapper 与 ServletRequestWrapper 的关系
可见 ServletRequestWrapper 是第一层包装,HttpServletRequestWrapper 通过继承进行包装,增加了 HTTP 相关的功能,SessionRepositoryRequestWrapper 又通过继承进行包装,增加了 Session 相关的功能
Mybatis 缓存中的装饰者模式
org.apache.ibatis.cache
包的文件结构如下所示
我们通过类所在的包名即可判断出该类的角色,Cache
为抽象构件类,PerpetualCache
为具体构件类,decorators
包下的类为装饰类,没有抽象装饰类
通过名称也可以判断出装饰类所要装饰的功能