设计模式系列文章目录导读:
设计模式 ~ 面向对象 6 大设计原则剖析与实战
设计模式 ~ 模板方法模式分析与实战
设计模式 ~ 观察者模式分析与实战
设计模式 ~ 单例模式分析与实战
设计模式 ~ 深入理解建造者模式与实战
设计模式 ~ 工厂模式剖析与实战
设计模式 ~ 适配器模式分析与实战
设计模式 ~ 装饰模式探究
设计模式 ~ 深入理解代理模式
本文主要内容:
- 装饰模式概述
- 装饰模式实现
- 实现一个日志案例
- 装饰模式在
JDK
中应用 - 装饰模式 VS 适配器模式
装饰模式概述
装饰模式(Decorator Pattern)又称包装模式。装饰模式以对客户端透明的方式扩展对象功能。
装饰模式的定义:动态地给一个对象添加一些额外的职责。就增加功能来说,装饰模式比继承的方式更加灵活
装饰模式有以下 4 个角色:
-
抽象构件(Component)角色
该角色用于规范需要装饰的对象
-
具体构件(Component)角色
该角色实现抽象构件接口,定义一个需要装饰的原始类
-
装饰(Decorator)角色
该角色持有一个构件对象的实例,并定义一个与抽象构件接口一致的接口
-
具体装饰(Concrete Decorator)角色
该角色负责对构件对象进行装饰
装饰模式类图如下所示:
什么时候使用装饰模式?
-
需要扩展一个类的功能,或给一个类附加额外职责
扩展类的功能,可能第一想到的就是继承,装饰模式也是很好的实现方式
-
需要动态的给一个对象扩展功能,这些功能可以再动态的撤销
-
需要增加由一些基本功能的排列组合而产生的非常大的功能,如果使用继承则会衍生很多类
装饰模式实现
下面根据装饰模式类图实现一个最简易的装饰模式代码:
抽象构件
public interface Component {
void operation();
}
具体构件
public class ConcreteComponent implements Component {
@Override
public void operation() {
System.out.println("正在处理相关逻辑...");
}
}
装饰
public class Decorator implements Component {
private 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();
}
}
测试
public class Client {
public static void main(String[] args) {
Component component = new ConcreteDecorator(new ConcreteComponent());
component.operation();
}
}
// 控制台输出:
在处理之前,增加点功能...
正在处理相关逻辑...
总的来说,装饰模式也很简单,就是装饰器实现需要被装饰的接口,通过构造方法将构件传递进来,然后装饰器在需要被增强的方法中,添加需要增强的逻辑。
通过上面的代码示例展示了最原始的装饰模式,可能感受不到装饰模式的强大。下面通过一个具体的例子展示下装饰模式的使用
实现一个日志案例
需求:要设计一个日志功能的模块,日志可能存储到普通文件中、数据库或远程服务器中,类图如下所示:
public interface ILogger {
void log(String text);
}
public class FileLogger implements ILogger {
@Override
public void log(String text) {
System.out.println("正在将存储到文件中");
}
}
public class DBLogger implements ILogger {
@Override
public void log(String text) {
System.out.println("正在将日志存储到数据库中");
}
}
public class NetLogger implements ILogger {
@Override
public void log(String text) {
System.out.println("正在将日志存储到远程服务器");
}
}
上面的功能只是将原始的日志数据存储到相应的介质中,如果此时需求发生变化:除了上面的存储原始日志信息,系统还可能需要将日志先加密然后存储。如果通过 继承
的方式实现 加密
功能,需要 3
个子类,也就是说有多少个存储媒介就需要多少个子类:
存储介质 | 额外功能 | 子类 | 父类 |
---|---|---|---|
文件 | 加密 | FileEncryptLogger | FileLogger |
数据库 | 加密 | DBEncryptLogger | DBLogger |
网络 | 加密 | NetEncryptLogger | NetLogger |
但是使用 装饰模式
的话只需要一个装饰器即可应对多个的存储介质:
// 抽象装饰角色
public abstract class LoggerDecorator implements ILogger {
private ILogger logger;
public LoggerDecorator(ILogger logger){
this.logger = logger;
}
@Override
public void log(String text) {
logger.log(text);
}
}
// 具体装饰角色
public class EncryptLogger extends LoggerDecorator {
public EncryptLogger(ILogger logger) {
super(logger);
}
@Override
public void log(String text) {
System.out.println("对日志 " + text + " 进行加密");
super.log(text);
}
}
// 测试类
public class LogClient {
public static void main(String[] args) {
// 通过一个装饰器 EncryptLogger 增强各种日志类
ILogger logger = new EncryptLogger(new FileLogger());
logger.log("hello");
ILogger logger2 = new EncryptLogger(new DBLogger());
logger2.log("world");
ILogger logger3 = new EncryptLogger(new NetLogger());
logger3.log("chiclaim");
}
}
输出结果:
对日志 hello 进行加密
正在将存储到文件中
对日志 world 进行加密
正在将日志存储到数据库中
对日志 chiclaim 进行加密
正在将日志存储到远程服务器
装饰模式在 JDK 中应用
装饰模式
在 JDK
中应用最多的就是在 I/O
流中
一般我们读取字符流使用 InputStreamReader
,它是字节流与字符流之间的桥梁,能将字节流输出为字符流,例如下面一段代码:
InputStream in = new URL(url).openStream();
InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
StringBuilder results = new StringBuilder();
int tmp;
while ((tmp = isr.read()) != -1) {
results.append((char) tmp);
}
但是为了提高效率,需要使用临时缓冲区(buffer),如:
InputStream in = new URL(url).openStream();
InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
StringBuilder results = new StringBuilder();
int tmp;
// 使用临时缓存区
char[] buffer = new char[1028*10];
while ((tmp = isr.read(buffer)) != -1) {
results.append((char) tmp);
}
由于一般使用 I/O
流都需要使用缓冲区,所以 JDK
内部内置了这样的功能类,所以上面的代码可以改成如下形式:
InputStream in = new URL(url).openStream();
InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
BufferedReader bf = new BufferedReader(isr);
StringBuilder results = new StringBuilder();
String newLine;
while ((newLine = bf.readLine()) != null) {
results.append(newLine).append("\n");
}
这里的 BufferedReader
就使用到了装饰模式,我们并没有新建缓冲区,BufferedReader
已经增强了这个功能
我们来看下 BufferedReader
是如何使用缓冲区的:
public class BufferedReader extends Reader {
private static int defaultCharBufferSize = 8192;
public BufferedReader(Reader in) {
this(in, defaultCharBufferSize);
}
public BufferedReader(Reader in, int sz) {
super(in);
if (sz <= 0)
throw new IllegalArgumentException("Buffer size <= 0");
this.in = in;
cb = new char[sz];
nextChar = nChars = 0;
}
// 省略其他代码...
}
可以看出 BufferedReader
默认会创建一个 8192
大小的字符缓冲数组,然后持有 Reader
这个抽象构件
然后来看下 BufferedReader
是如何增强 read()
方法的,read()
里调用了 fill()
方法:
private void fill() throws IOException {
// 省略其他代码...
// cb 就是上面 BufferedReader 的缓冲区
do {
n = in.read(cb, dst, cb.length - dst);
} while (n == 0);
if (n > 0) {
nChars = dst + n;
nextChar = dst;
}
}
BufferedReader
就是具体装饰角色,InputStreamReader
是具体构件,Reader
是抽象构件,这里没有 抽象装饰角色
,BufferedReader
直接继承了 Reader
,类图如下所示:
装饰模式 VS 适配器模式
装饰模式和适配器都可以称之为 包装(Wrapper)模式
,都是将已有的类包装起来达到某种目的,但是设计的目的完全不一样。
适配器模式是将一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法一起工作的两个类能够一起工作。所以适配器的重点是接口的转换
,达到重用代码的目的。
装饰模式的装饰角色是直接继承抽象构件(Component)的,保证接口的一致性,装饰器不能改变 Component 对象的接口。
所以当一个接口和目标接口不兼容,且需要重用代码的时候可以使用适配器模式;当不想通过继承(或继承的方式不好维护代码)来增强对象功能的时候可以使用装饰模式。
Reference
- 《Java与模式》
- 《Java设计模式及实践》
- 《Java设计模式深入研究》
- 《设计模式之禅》
下面是我的公众号,干货文章不错过,有需要的可以关注下,有任何问题可以联系我:
转载:https://blog.csdn.net/johnny901114/article/details/100850966