设计模式 ~ 装饰模式探究

设计模式系列文章目录导读:

设计模式 ~ 面向对象 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();
    }
}

// 控制台输出:
    在处理之前,增加点功能...
    正在处理相关逻辑...

总的来说,装饰模式也很简单,就是装饰器实现需要被装饰的接口,通过构造方法将构件传递进来,然后装饰器在需要被增强的方法中,添加需要增强的逻辑。

通过上面的代码示例展示了最原始的装饰模式,可能感受不到装饰模式的强大。下面通过一个具体的例子展示下装饰模式的使用

实现一个日志案例

需求:要设计一个日志功能的模块,日志可能存储到普通文件中、数据库或远程服务器中,类图如下所示:

适配器模式Log案例

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,类图如下所示:

JDK I/O

装饰模式 VS 适配器模式

装饰模式和适配器都可以称之为 包装(Wrapper)模式,都是将已有的类包装起来达到某种目的,但是设计的目的完全不一样。

适配器模式是将一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法一起工作的两个类能够一起工作。所以适配器的重点是接口的转换,达到重用代码的目的。

装饰模式的装饰角色是直接继承抽象构件(Component)的,保证接口的一致性,装饰器不能改变 Component 对象的接口。

所以当一个接口和目标接口不兼容,且需要重用代码的时候可以使用适配器模式;当不想通过继承(或继承的方式不好维护代码)来增强对象功能的时候可以使用装饰模式。

Reference

  • 《Java与模式》
  • 《Java设计模式及实践》
  • 《Java设计模式深入研究》
  • 《设计模式之禅》

如果你觉得本文帮助到你,给我个关注和赞呗!

另外,我为 Android 程序员编写了一份:超详细的 Android 程序员所需要的技术栈思维导图

如果有需要可以移步我的 GitHub -> AndroidAll,里面包含了最全的目录和对应知识点链接,帮你扫除 Android 知识点盲区。 由于篇幅原因只展示了 Android 思维导图:
超详细的Android技术栈

发布了161 篇原创文章 · 获赞 1125 · 访问量 125万+

猜你喜欢

转载自blog.csdn.net/johnny901114/article/details/100850966
今日推荐