【Java设计模式 设计模式与范式】结构型模式 三:装饰器模式(上)

简介: 【Java设计模式 设计模式与范式】结构型模式 三:装饰器模式

本篇Blog继续学习结构型模式,了解如何更优雅的布局类和对象。结构型模式描述如何将类或对象按某种布局组合以便获得更好、更灵活的结构。虽然面向对象的继承机制提供了最基本的子类扩展父类的功能,但结构型模式不仅仅简单地使用继承,而更多地通过组合与运行期的动态组合来实现更灵活的功能。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。本篇学习的是装饰器模式。由于学习的都是设计模式,所有系列文章都遵循如下的目录:

  • 模式档案:包含模式的定义、模式的特点、解决什么问题、优缺点、使用场景等
  • 模式结构:包含模式的角色定义及调用关系以及其模版代码
  • 模式示例:包含模式的实现方式代码举例,生活中的简单问题映射
  • 模式实践:如果工作中或开源项目用到了该模式,就将使用过程贴到这里,并且客观讨论使用的是否恰当
  • 模式对比:如果模式相似,有必要体现其相似点及不同点,区分使用,说明哪些场景下使用哪种模式比较好
  • 模式扩展:如果模式有与标准结构定义不同的变体形式,一并体现出其变体结构

接下来所有设计模式的介绍都暂且遵循此基本行文逻辑吗,如果某一条目没有则无需体现,但条目顺序遵循此结构

模式档案

我们去蛋糕店买蛋糕,可以对蛋糕做各种定制,例如蛋糕加奶油、蛋糕加水果,蛋糕加芝士,通过这些装饰让蛋糕更好看,也更美味。这些附加的材料和设计装饰了蛋糕使蛋糕的功能更强了。在软件开发过程中,有时想用一些现存的组件。这些组件可能只是完成了一些核心功能。但在不改变其结构的情况下,可以动态地扩展其功能。这些情况可以釆用装饰器模式来实现。

模式定义:指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式,它属于对象结构型模式

模式特点:没有装饰器模式之前我们通常的做法是使用继承去让子类扩展更丰富的功能,但是继承会让子类变得臃肿,继承关系耦合度又比较高,所以我们采用组合的形式解决扩展类功能的问题

解决什么问题:通常情况下,扩展一个类的功能会使用继承方式来实现。但继承具有静态特征,耦合度高,并且随着扩展功能的增多,子类会很膨胀。如果使用组合关系来创建一个包装对象(即装饰对象)来包裹真实对象,并在保持真实对象的类结构不变的前提下,为其提供额外的功能,这就是装饰器模式的目标

优点:该模式的主要优点如下:

  • 装饰器是继承的有力补充,比继承灵活,在不改变原有对象的情况下,动态的给一个对象扩展功能,即插即用
  • 通过使用装饰类及这些装饰类的排列组合,可以实现不同效果
  • 装饰器模式完全遵守开闭原则

缺点:该模式的主要缺点如下:

  • 装饰器模式多层装饰时会增加许多子类,过度使用会增加系统复杂性

使用场景: 该模式通常适用于以下场景。

  • 当需要给一个现有类添加附加职责,而又不能采用生成子类的方法进行扩充时。例如,该类被隐藏或者该类是终极类或者采用继承方式会产生大量的子类。
  • 当需要通过对现有的一组基本功能进行排列组合而产生非常多的功能时,采用继承关系很难实现,而采用装饰器模式却很好实现。
  • 对象的功能要求可以动态地添加,也可以再动态地撤销时

装饰器模式在 Java 语言中的最著名的应用莫过于 Java I/O 标准库的设计了。例如,InputStream 的子类 FilterInputStream,OutputStream 的子类 FilterOutputStream,Reader 的子类 BufferedReader 以及 FilterReader,还有 Writer 的子类 BufferedWriter、FilterWriter 以及 PrintWriter 等,它们都是抽象装饰类。为 FileReader 增加缓冲区而采用的装饰类 BufferedReader 的例子

BufferedReader in = new BufferedReader(new FileReader("filename.txt"));
String s = in.readLine();

模式结构

装饰器模式主要包含以下四个主要角色:

  • 抽象组件(Component)角色:定义一个抽象接口以规范准备接收附加责任的对象,抽象类或者接口
  • 具体组件(ConcreteComponent)角色:实现抽象组件,通过装饰角色为其添加一些职责。
  • 抽象装饰(Decorator)角色:继承抽象组件,并包含具体组件的实例,可以通过其子类扩展具体组件的功能,抽象类或者普通的父类
  • 具体装饰(ConcreteDecorator)角色:实现抽象装饰的相关方法,并给具体组件对象添加附加的责任。

类结构图如下所示:

模式实现

下面我们依据模式结构图来分别定义装饰器模式组成角色和进行代码实现

1 抽象组件角色

//抽象组件角色
interface Component {
     void operation();
}

2 具体组件角色

//具体组件角色
class ConcreteComponent implements Component {
    public void operation() {
        System.out.println("调用具体组件角色的方法operation()");
    }
}

3 抽象装饰器角色

//抽象装饰角色
@AllArgsConstructor
abstract class Decorator implements Component {
    protected Component component;
    public Decorator(Component component) {
        this.component = component;
    }
    public void operation() {
        component.operation();
    }
}

虽然抽象装饰类也继承或实现自抽象组件类,但这里利用继承或实现主要目的是利用继承达到类型匹配,而不是利用继承获得行为,目的是让装饰者和被装饰者拥有共同的超类。有了抽象装饰器类,具体的装饰器类只需要实现它需要增强的方法就可以了,其他方法继承装饰器父类的默认实现。而之所以抽象装饰器类需要实现一遍抽象组件的方法(如果不增强也需要简单包裹)是因为如果不重新实现,那装饰器类就无法将方法的功能委托给传递进来的具体构建对象来完成,使用的就是默认的抽象组件的方法,这样就不能实现具体组件的多态了。

4 具体装饰器角色

//具体装饰角色
class ConcreteDecorator extends Decorator {
    public ConcreteDecorator(Component component) {
        super(component);
    }
    public void operation() {
        component.operation();
        addedFunction();
    }
    public void addedFunction() {
        System.out.println("为具体组件角色增加额外的功能addedFunction()");
    }
}

客户端调用代码如下

public class DecoratorPattern {
    public static void main(String[] args) {
        Component concreteComponent = new ConcreteComponent();
        concreteComponent.operation();
        System.out.println("---------------------------------");
        Component concreteDecorator = new ConcreteDecorator(concreteComponent);
        concreteDecorator.operation();
    }
}

调用结果:

模式实践

接下来我们来看两个模式实践:设计一个可装饰的蛋糕制作方案设计一套可装饰的Java IO类

设计一个可装饰的蛋糕制作方案

拿开篇的蛋糕制作过程示例,如果我们想给一个蛋糕胚增强功能,使其变成不同的蛋糕,例如奶油蛋糕、芝士蛋糕等:

package com.example.designpattern.decorator;
import lombok.AllArgsConstructor;
public class CakeMakeTest {
    public static void main(String[] args) {
        CakeMake cakeMaker = new CakeMaker();
        cakeMaker.makeCake();
        System.out.println("--------------使用装饰器模式增强蛋糕-------------------");
        CakeMake cheeseDecoratorPerson = new CheeseDecorator(new CreamDecorator(cakeMaker));
        cheeseDecoratorPerson.makeCake();
    }
}
//蛋糕制作
interface CakeMake {
    void makeCake();
}
//蛋糕烘焙
class CakeMaker implements CakeMake {
    public void makeCake() {
        System.out.println("制作一个蛋糕胚");
    }
}
//装饰
@AllArgsConstructor
abstract class CakeDecorator implements CakeMake {
    protected CakeMake cakeMake;
    public void makeCake() {
        cakeMake.makeCake();
    }
}
//奶油装饰
class CreamDecorator extends CakeDecorator {
    public CreamDecorator(CakeMake cakeMake) {
        super(cakeMake);
    }
    public void makeCake() {
        cakeMake.makeCake();
        addedCream();
    }
    public void addedCream() {
        System.out.println("给蛋糕胚抹上奶油,蛋糕已经装饰奶油了");
    }
}
//芝士装饰
class CheeseDecorator extends CakeDecorator {
    public CheeseDecorator(CakeMake cakeMake) {
        super(cakeMake);
    }
    public void makeCake() {
        cakeMake.makeCake();
        addedCheese();
    }
    public void addedCheese() {
        System.out.println("给蛋糕胚抹上芝士,蛋糕已经装饰芝士了");
    }
}

这里进行了多层装饰,例如要制作一个奶油芝士蛋糕,打印结果如下:

这样蛋糕就加上了奶油和芝士,变成了奶油芝士双拼蛋糕

设计一套可装饰的Java IO类

我们想要给原始类增强功能的时候首先想到的就是继承:复用父类的代码并在此基础之上进行增强。所以在学习Java IO时可能会有疑惑:

InputStream in = new FileInputStream("/user/test.txt");
InputStream bin = new BufferedInputStream(in);
byte[] data = new byte[128];
while (bin.read(data) != -1) {
  //...
}

上边代码需要先创建一个 FileInputStream 对象,然后再传递给 BufferedInputStream 对象来使用。Java IO 为什么不设计一个继承 FileInputStream 并且支持缓存的 BufferedFileInputStream 类呢?这样我们就可以像下面的代码中这样,直接创建一个 BufferedFileInputStream 类对象,打开文件读取数据,用起来岂不是更加简单?

InputStream bin = new BufferedFileInputStream("/user/test.txt");
byte[] data = new byte[128];
while (bin.read(data) != -1) {
  //...
}

1 为什么不使用继承

如果 InputStream 只有一个子类 FileInputStream 的话,那我们在 FileInputStream 基础之上,再设计一个孙子类 BufferedFileInputStream,也算是可以接受的,毕竟继承结构还算简单。但实际上,继承 InputStream 的子类有很多。我们需要给每一个 InputStream 的子类,再继续派生支持缓存读取的子类,例如除了支持缓存读取之外,如果我们还需要对功能进行其他方面的增强,比如下面的 DataInputStream 类,支持按照基本数据类型(int、boolean、long 等)来读取数据

FileInputStream in = new FileInputStream("/user/test.txt");
DataInputStream din = new DataInputStream(in);
int data = din.readInt();

在这种情况下,如果我们继续按照继承的方式来实现的话,就需要再继续派生出 DataFileInputStream、DataPipedInputStream 等类。如果我们还需要既支持缓存、又支持按照基本类型读取数据的类,那就要再继续派生出 BufferedDataFileInputStream、BufferedDataPipedInputStream 等 n 多类。这还只是附加了两个增强功能,如果我们需要附加更多的增强功能,那就会导致组合爆炸,类继承结构变得无比复杂,代码既不好扩展,也不好维护,这也是为什么我们有一个合成复用原则:组合优于继承

相关文章
|
28天前
|
设计模式 Java Spring
Java 设计模式之责任链模式:优雅处理请求的艺术
责任链模式通过构建处理者链,使请求沿链传递直至被处理,实现发送者与接收者的解耦。适用于审批流程、日志处理等多级处理场景,提升系统灵活性与可扩展性。
197 2
|
28天前
|
设计模式 网络协议 数据可视化
Java 设计模式之状态模式:让对象的行为随状态优雅变化
状态模式通过封装对象的状态,使行为随状态变化而改变。以订单为例,将待支付、已支付等状态独立成类,消除冗长条件判断,提升代码可维护性与扩展性,适用于状态多、转换复杂的场景。
238 0
|
3月前
|
设计模式 缓存 Java
Java设计模式(二):观察者模式与装饰器模式
本文深入讲解观察者模式与装饰器模式的核心概念及实现方式,涵盖从基础理论到实战应用的全面内容。观察者模式实现对象间松耦合通信,适用于事件通知机制;装饰器模式通过组合方式动态扩展对象功能,避免子类爆炸。文章通过Java示例展示两者在GUI、IO流、Web中间件等场景的应用,并提供常见陷阱与面试高频问题解析,助你写出灵活、可维护的代码。
|
27天前
|
设计模式 算法 搜索推荐
Java 设计模式之策略模式:灵活切换算法的艺术
策略模式通过封装不同算法并实现灵活切换,将算法与使用解耦。以支付为例,微信、支付宝等支付方式作为独立策略,购物车根据选择调用对应支付逻辑,提升代码可维护性与扩展性,避免冗长条件判断,符合开闭原则。
240 35
|
27天前
|
设计模式 消息中间件 传感器
Java 设计模式之观察者模式:构建松耦合的事件响应系统
观察者模式是Java中常用的行为型设计模式,用于构建松耦合的事件响应系统。当一个对象状态改变时,所有依赖它的观察者将自动收到通知并更新。该模式通过抽象耦合实现发布-订阅机制,广泛应用于GUI事件处理、消息通知、数据监控等场景,具有良好的可扩展性和维护性。
208 8
|
6月前
|
设计模式 存储 缓存
【设计模式】【结构型模式】享元模式(Flyweight)
一、入门 什么是享元模式? 享元模式(Flyweight Pattern)是一种结构型设计模式,旨在通过共享对象来减少内存使用,特别适用于存在大量相似对象的情况。 它的核心思想是将对象的内在状态(不变
244 16
|
2月前
|
人工智能 Java API
Java与大模型集成实战:构建智能Java应用的新范式
随着大型语言模型(LLM)的API化,将其强大的自然语言处理能力集成到现有Java应用中已成为提升应用智能水平的关键路径。本文旨在为Java开发者提供一份实用的集成指南。我们将深入探讨如何使用Spring Boot 3框架,通过HTTP客户端与OpenAI GPT(或兼容API)进行高效、安全的交互。内容涵盖项目依赖配置、异步非阻塞的API调用、请求与响应的结构化处理、异常管理以及一些面向生产环境的最佳实践,并附带完整的代码示例,助您快速将AI能力融入Java生态。
429 12
|
6月前
|
设计模式 缓存 安全
【高薪程序员必看】万字长文拆解Java并发编程!(8):设计模式-享元模式设计指南
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发编程中的经典对象复用设计模式-享元模式,废话不多说让我们直接开始。
158 0
|
3月前
|
设计模式 安全 Java
Java设计模式(一):单例模式与工厂模式
本文详解单例模式与工厂模式的核心实现及应用,涵盖饿汉式、懒汉式、双重检查锁、工厂方法、抽象工厂等设计模式,并结合数据库连接池与支付系统实战案例,助你掌握设计模式精髓,提升代码专业性与可维护性。
|
3月前
|
设计模式 XML 安全
Java枚举(Enum)与设计模式应用
Java枚举不仅是类型安全的常量,还具备面向对象能力,可添加属性与方法,实现接口。通过枚举能优雅实现单例、策略、状态等设计模式,具备线程安全、序列化安全等特性,是编写高效、安全代码的利器。