Java - GoF设计模式详解9(装饰模式)
九、装饰模式
1,基本介绍
(1)装饰模式(Decorator)又叫装饰器模式、装饰者模式、包装模式(Wrapper),它可以在不改变对象结构的情况下,动态地给该对象添加新的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。在很多时候,使用装饰模式进行对象的功能扩展比继承更加灵活,同时解决因功能的不断横向扩展导致子类膨胀的问题。
(1)装饰模式(Decorator Pattern)和代理模式(Proxy Pattern)都是对象结构型模式,主要用于对类的功能进行扩展。但是它们的实现方式和用途是有区别的:
- 装饰模式:是在不改变原有类的基础上,通过增加新的装饰类来扩展功能。装饰类和被装饰类有相同的接口,装饰类通过在被装饰类的基础上添加新的功能来实现对原有类的扩展。
- 代理模式:是在不暴露一个对象的具体实现细节的同时,提供了对这个对象的访问。代理类与被代理类有相同的接口,代理类通过对被代理类的功能进行控制来实现对原有类的扩展。
(2)该模式中包含的角色及其职责如下:
- 抽象构件(Component)角色:给出一个抽象接口,以规范准备接收额外职责的对象。
- 具体构件(Concrete Component)角色:定义一个将要接收额外职责的类。
- 装饰(Decorator)角色:继承了抽象构件角色,并包含了一个抽象构件的实例,同时实现一个与抽象构件接口一致的接口。
- 具体装饰(Concrete Decorator)角色:实现了抽象装饰的相关方法,负责给构件对象添加上额外的职责。
2,样例代码
(1)我们使用外卖套餐来掩饰装饰模式的使用。套餐主食可以选择米饭和面条,同时可以自由添加各种配菜(鸡蛋、肉),并且最终会把所有配菜价格加到主食上从而获得最终套餐价格。首先我们定义一个食物接口(当然也可以是抽象类),规定了所有的子类需要实现的方法(获取食物描述和价格):
// 食物接口(抽象构件) public interface Food { String getDesc(); //获取食物描述 float getPrice(); //获取食物价格 }
(2)然后定义两个子类,分别表示米饭和面条这两个主食:
// 米饭类(具体构件) public class Rice implements Food{ @Override public String getDesc() { return "米饭"; } @Override public float getPrice() { return 5; } } // 面条类(具体构件) public class Noodles implements Food{ @Override public String getDesc() { return "面条"; } @Override public float getPrice() { return 6; } }
(3)接着配菜抽象类:
//配菜抽象类(装饰角色) public abstract class Garnish implements Food { private Food food; public Garnish(Food food) { this.food = food; } @Override public String getDesc() { return this.food.getDesc(); } @Override public float getPrice() { return this.food.getPrice(); } }
(4)然后继承该抽象类定义两个具体配菜类:
// 鸡蛋(具体装饰类) public class Egg extends Garnish{ public Egg(Food food) { super(food); } @Override public String getDesc() { return super.getDesc() + "+鸡蛋"; } @Override public float getPrice() { return super.getPrice() + 2; } } // 肉(具体装饰类) public class Meat extends Garnish{ public Meat(Food food) { super(food); } @Override public String getDesc() { return super.getDesc() + "+肉"; } @Override public float getPrice() { return super.getPrice() + 3; } }
(5)最后测试一下:
public class Test { public static void main(String[] args) { Food food1 = new Noodles(); System.out.println("第1份外卖:" + food1.getDesc()); System.out.println("价格:" + food1.getPrice()); Food food2 = new Egg(new Rice()); System.out.println("第2份外卖:" + food2.getDesc()); System.out.println("价格:" + food2.getPrice()); Food food3 = new Meat(new Egg(new Noodles())); System.out.println("第3份外卖:" + food3.getDesc()); System.out.println("价格:" + food3.getPrice()); } }
附一:JDK 中的装饰模式
(1)JDK 的 I/O 标准库使用到了装饰者模式。以 InputStream 为例子,InputStream 有很多的实现类:
- InputStream:字节输入流基类,抽象类是表示字节输入流的所有类的超类
- FileInputStream:字节文件输入流,从文件系统中的某个文件中获得输入字节,用于读取诸如图像数据之类的原始字节流。
- BufferedInputStream 是一种缓冲输入流,它从输入源读取数据并将它们缓存在内部缓冲区中。这样,应用程序可以一次性从缓冲区中读取大量数据,而不是每次只能从输入源读取一个字节。这样可以提高读取数据的性能。
- DataInputStream 是一种数据输入流,它允许应用程序以与机器无关的方式从输入源读取基本数据类型和字符串。它还提供了一组方法,可以将读取的数据转换为不同的基本类型,例如 int、float 等。
(2)输入流各个类对应的角色如下:
- InputStream 就是装饰者模式中的抽象构件(Component)角色
- ByteArrayInputStream,FileInputStream 相当于具体构件(Concrete Component)角色,这些类都提供了最基本的字节读取功能。
- FilterInputStream 即是装饰(Decorator)角色,它包含被装饰的 InputStream 对象
- BufferedInputStream,DataInputStream 这些 FilterInputStream 的子类即为具体装饰(Concrete Decorator)角色
(3)通过装饰者模式,就可以保证在 InputStream、OutputStream 不变的前提下,增加其他功能。比如要实现将从文件输入流读取数据到缓冲输入流中,同时将数据通过缓冲输出流写入到文件输出流中,实现文件内容的复制就可以这样写:
// 指定要读取文件的缓冲输入字节流 BufferedInputStream in = new BufferedInputStream(new FileInputStream("1.txt")); // 指定要写入文件的缓冲输出字节流 BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream("2.txt")); byte[] bb = new byte[1024];// 用来存储每次读取到的字节数组 int n;// 每次读取到的字节数组的长度 while ((n = in.read(bb)) != -1) { out.write(bb, 0, n);// 写入到输出流 } out.close(); // 关闭输出流 in.close(); // 关闭输入流
(4)如果想实现基本数据类型读写+文件读写,就可以这样写。可以看到虽然被装饰的对象都是 FileInputStream、FileOutputStream,但通过不同的装饰类可以实现不同的功能:
DataOutputStream dos = new DataOutputStream(new FileOutputStream("2.txt")); dos.writeByte(100); dos.writeInt(100); dos.writeChar('a'); dos.writeFloat(123.123f); dos.close(); DataInputStream dis = new DataInputStream(new FileInputStream("2.txt")); byte a = dis.readByte(); int b = dis.readInt(); char c = dis.readChar(); float d = dis.readFloat(); dis.close(); System.out.println(a); System.out.println(b); System.out.println(c); System.out.println(d);
附二:Spring 中的装饰模式
Spring 中用到的包装器模式在类名上有两种表现:一种是类名中含有 Wrapper,另一种是类名中含有 Decorator。基本上都是动态地给一个对象添加一些额外的职责。
1,BeanDefinitionDecorator
Spring 的 BeanDefinitionParser 用于将元素解析成 BeanDefinition,而 BeanDefinitionDecorator 使用了装饰器模式,它允许装饰 BeanDefinition,以定制或扩展 bean 定义的功能,通过添加或修改其属性和元数据。2,TransactionAwareCacheDecorator
TransactionAwareCacheDecorator 类是对缓存 Cache 接口的装饰,声明了 targetCache 成员属性负责对缓存的核心调用,然后通过 put/evict/clear 等操作与 Spring 管理的事务同步,仅在成功的事务的 afterCommit 阶段执行实际的缓存 put/evict/clear 操作。如果没有事务是 active 的,将立即执行 put/evict/clear 操作。
3,ServletRequestWrapper
ServletRequestWrapper 是 Servlet API 中用于装饰 ServletRequest 的类。它包装目标 ServletRequest 实例,可用于为目标 ServletRequest 添加新功能,如特殊的参数解析或请求处理,而不修改目标 ServletRequest 的类。
4,SpringMVC 中的 HttpHeadResponseDecorator
HttpHeadResponseDecorator 是 Spring 框架中用于装饰 HttpServletResponse 的类。它包装目标 HttpServletResponse 实例,内部直接封装了 ServerHttpResponse 调用其核心方法,并在其基础上添加了对 HTTP HEAD 请求的支持,而不修改目标 HttpServletResponse 的类。最后通过流式 API 进行返回。
附三:Mybatis 中的装饰模式
(1)在 MyBatis 的 org.apache.ibatis.cache.decorators 包的里面封装了各种各样的装饰类对 Cache 接口进行修饰,而 Cache 接口只有一个最基本的实现类 PerpetualCache ,其余都是对 Cache 接口的装饰类,实现阻塞、同步、日志、LRU、FIFO 等特性的修饰。
(2)各装饰类的说明如下:
- BlockingCache:阻塞版本的缓存装饰器,它会保证只有一个线程到数据库中查找指定 key 对应的数据
- FifoCache:先入先出的装饰器,当向缓存添加数据时,如果缓存项的个数己经达到上限,则会将缓存中最老(即最早进入缓存)的缓存项删除。
- LruCache:按照近期最少使用算法进行缓存清理的装饰器,在需要清理缓存时,它会清除最近最少使用的缓存项。底层使用 LinkedHashMap。
- SoftCache & WeakCache:在 SoftCache 中,最近使用的一部分缓存项不会被 GC 回收,这就是通过将其 value 添加到 hardLinksToAvoidGarbageCollection 集合中实现的(即有强引用指向其 value)。Soft 引用是内存不够用时候才清理, 而 Weak 弱引用则相反, 只要有 GC 就会回收。
- ScheduledCache:周期性清理缓存的装饰器
- LoggingCache:在 Cache 的基础上提供了日志功能
- SynchronizedCache:通过在每个方法上添加 synchronized 关键字,为 Cache 添加了同步功能
- SerializedCache:在添加缓存项时,会将 value 对应的 Java 对象进行序列化,并将序列化后的 byte 数组作为 value 存入缓存