设计模式学习(10) 23-8 装饰者模式

文章目录

  • 0.个人感悟
  • 1.概念
  • [2. 适配场景(什么场景下使用)](#2. 适配场景(什么场景下使用))
    • [2.1 适合的场景](#2.1 适合的场景)
    • [2.2 常见场景举例](#2.2 常见场景举例)
  • [3. 实现方法](#3. 实现方法)
    • [3.1 实现思路](#3.1 实现思路)
    • [3.2 UML类图](#3.2 UML类图)
    • [3.3 代码示例](#3.3 代码示例)
      • [3.3.1 传统设计-继承](#3.3.1 传统设计-继承)
      • [3.3.2 进阶模式-组合](#3.3.2 进阶模式-组合)
      • [3.3.3 装饰者模式](#3.3.3 装饰者模式)
  • [4. 优缺点](#4. 优缺点)
    • [4.1 优点](#4.1 优点)
    • [4.2 缺点](#4.2 缺点)
  • [5. 源码分析:结合JDK的文件IO实现进行说明](#5. 源码分析:结合JDK的文件IO实现进行说明)

0.个人感悟

  • 这个模式开始比较难懂。细究发现是想给主体添加额外的动作,一方面不想影响主体原功能,一方面可以动态扩展这些功能
  • 其实纯用组合的方式来组合主体和辅助部分也可以解决问题,使用装饰者模式更贴合辅助是给主体打工的思想,添加辅助功能后它的身份还是主体。表现上,构造器代码可以嵌套,比如 new BufferedInputStream(new FileInputStream(new File("fileName")))
  • 装饰者模式带来这种迭代效果的同时,也增加了系统复杂性,排查问题可能需要多层定位。实际工作中看情况使用

1.概念

英文定义 (《设计模式:可复用面向对象软件的基础》):

Attach additional responsibilities to an object dynamically. Decorators provide aflexibale alternative to subclassing for extending functionality.

中文翻译:

动态地给一个对象添加一些额外的职责。就增加功能来说,装饰者模式比生成子类更为灵活。

理解:

  • 核心思想 : 通过组合 而非继承的方式,在不改变原有对象结构的情况下,动态地扩展其功能
  • 关键要点 :
    • 透明包装: 装饰者和被装饰对象具有相同的接口,客户端无需知道是否被装饰
    • 动态扩展: 可以在运行时根据需要动态地添加或移除装饰
    • 多层嵌套: 可以多次装饰同一个对象,形成装饰链
    • 开放封闭: 符合开闭原则,可以新增装饰者而不修改原有代码

2. 适配场景(什么场景下使用)

2.1 适合的场景

  1. 动态添加功能: 需要在不修改原有对象的情况下,动态地给对象添加额外的功能
  2. 替代多重继承: 当通过继承扩展功能会导致子类爆炸或类层次复杂时
  3. 撤销功能: 需要能够动态地添加或撤销功能
  4. 组合功能: 需要将多个功能以不同的组合方式添加到对象上
  5. 核心功能与辅助功能分离: 希望将核心功能与装饰性功能分离

2.2 常见场景举例

  1. Java I/O流 : BufferedInputStreamDataInputStream等装饰InputStream
  2. GUI组件: 为窗口添加滚动条、边框、标题栏等装饰
  3. Web服务器: 中间件装饰请求和响应对象,如添加日志、认证、压缩等功能
  4. 日志系统: 为日志记录器添加时间戳、线程信息、调用栈等装饰信息
  5. 权限控制: 为业务对象添加权限检查、审计等装饰功能

3. 实现方法

3.1 实现思路

实现装饰者模式通常遵循以下步骤:

  1. 定义组件接口: 创建一个接口或抽象类,定义被装饰对象和装饰者的共同行为
  2. 创建具体组件: 实现组件接口,这是被装饰的原始对象
  3. 创建装饰者抽象类 :
    • 实现组件接口
    • 持有一个组件接口的引用(用于包装其他组件)
    • 将方法调用委托给持有的组件
  4. 创建具体装饰者: 继承装饰者抽象类,在调用被装饰对象的方法前后添加额外的行为
  5. 客户端使用: 客户端可以自由组合装饰者来装饰具体组件

3.2 UML类图

角色说明:

  • Component (组件接口): 定义对象接口,可以动态地给这些对象添加职责
  • ConcreteComponent (具体组件): 定义一个对象,可以给这个对象添加一些职责
  • Decorator (装饰者抽象类): 维持一个指向Component对象的引用,并定义一个与Component接口一致的接口
  • ConcreteDecorator (具体装饰者): 向组件添加具体的职责

3.3 代码示例

以星巴克饮料和调料为例:

星巴克有很多种饮料,比如咖啡、果汁等,点单的时候可以单点饮料,也可以加入调料,比如牛奶巧克力

3.3.1 传统设计-继承

传统设计,按照继承方式设计,定义父类drink,剩余所有单品都是一个类型

会出现类爆炸问题,而且不利于复用

3.3.2 进阶模式-组合

将调料作为饮品的属性进行组合

这样可以解决扩展问题,但是还是缺一些意思,没有很好地展现饮品和调料的主从关系,使用起来也不直观

3.3.3 装饰者模式

按照装饰者模式进行实现

组件及其具体实现:

java 复制代码
// 饮料
public abstract class Drink {  
    protected String desc;  
  
    private double price;  
  
    public String getDesc() {  
        return desc;  
    }  
  
    public void setDesc(String desc) {  
        this.desc = desc;  
    }  
  
    public double getPrice() {  
        return price;  
    }  
  
    public void setPrice(double price) {  
        this.price = price;  
    }  
  
    public abstract double cost();  
}

// 咖啡
public class Coffee extends Drink {  
    @Override  
    public double cost() {  
        return super.getPrice();  
    }  
}

// 具体组件:意大利咖啡
public class Espresso extends Coffee {  
    public Espresso() {  
        setDesc(" 意大利咖啡 ");  
        setPrice(6.0d);  
    }  
}

装饰器和具体实现

java 复制代码
// 装饰器
public class Decorator extends Drink {  
    private Drink drink;  
  
    public Decorator(Drink drink) {  
        this.drink = drink;  
    }  
  
    @Override  
    public double cost() {  
        return super.getPrice() + drink.cost();  
    }  
  
    @Override  
    public String getDesc() {  
        return STR."\{desc} \{getPrice()}  \{drink.getDesc()} ";  
    }  
}

// 具体实现 牛奶
public class Milk extends Decorator {  
    public Milk(Drink drink) {  
        super(drink);  
        setDesc(" 牛奶 ");  
        setPrice(2.0d);  
    }  
}

// 具体实现 巧克力
public class Chocolate extends Decorator {  
    public Chocolate(Drink drink) {  
        super(drink);  
        setDesc(" 巧克力 ");  
        setPrice(3.0f);  
    }  
}

测试和输出

java 复制代码
public class Client {  
    static void main() {  
        // 单点  
        Drink order = new Espresso();  
        System.out.println("===单点咖啡===");  
        printInfo(order);  
  
        // 加入牛奶  
        order = new Milk(order);  
        System.out.println("===加入牛奶===");  
        printInfo(order);  
  
        // 加入巧克力  
        order = new Chocolate(order);  
        System.out.println("===加入巧克力===");  
        printInfo(order);  
  
  
        // 再加入巧克力  
        order = new Chocolate(order);  
        System.out.println("===再加入巧克力===");  
        printInfo(order);  
    }  
  
    public static void printInfo(Drink drink) {  
        System.out.println(STR."描述 :\{drink.getDesc()}");  
        System.out.println(STR."价格 : \{drink.cost()}");  
    }  
}
复制代码
===单点咖啡===
描述 : 意大利咖啡 
价格 : 6.0
===加入牛奶===
描述 : 牛奶  2.0   意大利咖啡  
价格 : 8.0
===加入巧克力===
描述 : 巧克力  3.0   牛奶  2.0   意大利咖啡   
价格 : 11.0
===再加入巧克力===
描述 : 巧克力  3.0   巧克力  3.0   牛奶  2.0   意大利咖啡    
价格 : 14.0

4. 优缺点

4.1 优点

  1. 高内聚低耦合: 装饰者和被装饰者通过组合方式连接,耦合度低,符合合成复用原则
  2. 复用性: 装饰者类可以在不同的上下文中复用,用于装饰不同的组件对象
  3. 可读性: 虽然装饰者模式会增加类的数量,但每个类的职责单一,代码逻辑清晰
  4. 维护性: 符合开闭原则,可以新增装饰者而无需修改现有代码,易于维护和扩展
  5. 稳定性: 由于不修改原有对象,只是增加包装层,不会破坏原有系统的稳定性
  6. 单一职责: 可以将复杂的功能分解为多个简单的装饰者,每个装饰者只关注一个功能
  7. 动态组合: 可以在运行时动态地组合功能,提供了比继承更灵活的功能扩展方式

4.2 缺点

  1. 增加系统复杂性: 会增加许多小类,特别是装饰链较长时,系统会变得复杂
  2. 多层装饰调试困难: 由于对象被多层装饰,调试时可能需要逐层排查,增加了调试难度
  3. 装饰顺序影响结果: 装饰者的顺序可能会影响最终结果,需要特别注意装饰的顺序
  4. 初始化配置复杂: 客户端需要负责创建装饰者链,代码可能显得冗长

5. 源码分析:结合JDK的文件IO实现进行说明

跟踪代码,不难发现InputStream的类图

  1. 组件接口层次:

    • 字节流: InputStream(输入)、OutputStream(输出)
    • 字符流: Reader(输入)、Writer(输出)
  2. 具体组件:

    • FileInputStreamFileOutputStreamByteArrayInputStreamByteArrayOutputStream
    • FileReaderFileWriterCharArrayReaderCharArrayWriter
  3. 装饰者抽象类:

    • FilterInputStreamFilterOutputStream
    • FilterReaderFilterWriter
  4. 具体装饰者:

    • 缓冲功能: BufferedInputStreamBufferedOutputStreamBufferedReaderBufferedWriter
    • 数据类型: DataInputStreamDataOutputStream
    • 对象序列化: ObjectInputStreamObjectOutputStream
    • 回推功能: PushbackInputStreamPushbackReader
    • 行号功能: LineNumberReader
    • 打印功能: PrintStreamPrintWriter
      核心装饰者:
java 复制代码
//  装饰者抽象类:FilterInputStream
public class FilterInputStream extends InputStream {
    protected volatile InputStream in;  // 持有一个InputStream的引用
    
    protected FilterInputStream(InputStream in) {
        this.in = in;
    }
    
    @Override
    public int read() throws IOException {
        return in.read();  // 委托给被装饰的InputStream
    }
    
    @Override
    public int read(byte b[]) throws IOException {
        return read(b, 0, b.length);
    }
    
    @Override
    public int read(byte b[], int off, int len) throws IOException {
        return in.read(b, off, len);  // 委托给被装饰的InputStream
    }
    
    // 其他方法也类似地委托给in
}

参考:

相关推荐
ybdesire2 小时前
Joern服务器启动后cpgqls-client结合python编程进行扫描
运维·服务器·python
autho2 小时前
conda
linux·python·conda
老蒋每日coding2 小时前
基于LangGraph的AI Agent并行化设计模式详解
设计模式·ai编程
Errorbot2 小时前
F570四轴飞行器学习笔记
笔记·学习·无人机
GISer_Jing2 小时前
AI学习资源总结:免费开放,入门至深入,持续更新
人工智能·学习·设计模式·prompt·aigc
_Kayo_2 小时前
Node.JS 学习笔记7
笔记·学习·node.js
知乎的哥廷根数学学派2 小时前
基于注意力机制的多尺度脉冲神经网络旋转机械故障诊断(西储大学轴承数据,Pytorch)
人工智能·pytorch·python·深度学习·神经网络·机器学习
测试19982 小时前
用Postman测WebSocket接口
自动化测试·软件测试·python·websocket·测试工具·接口测试·postman
l1t2 小时前
数独优化求解C库tdoku-lib的使用
c语言·开发语言·python·算法·数独