【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 多类。这还只是附加了两个增强功能,如果我们需要附加更多的增强功能,那就会导致组合爆炸,类继承结构变得无比复杂,代码既不好扩展,也不好维护,这也是为什么我们有一个合成复用原则:组合优于继承

相关文章
|
2月前
|
设计模式 XML Java
【设计模式】装饰器模式(定义 | 特点 | Demo入门讲解)
【设计模式】装饰器模式(定义 | 特点 | Demo入门讲解)
38 0
|
4天前
|
设计模式 前端开发 JavaScript
前端必须掌握的设计模式——装饰器模式
装饰器模式是一种结构型设计模式,通过创建新类来包装原始对象,实现在不修改原有结构的前提下扩展新行为。其核心在于“组合”思想,使新功能可“即插即拔”。该模式具有解耦性、灵活性和动态性等特点,广泛应用于类的面向对象编程语言中,如JavaScript的注解和TypeScript的写法。示例中,通过装饰器模式为游戏角色动态添加装备,展示了其强大的扩展性和灵活性。
|
16天前
|
存储 Java 数据挖掘
Java 8 新特性之 Stream API:函数式编程风格的数据处理范式
Java 8 引入的 Stream API 提供了一种新的数据处理方式,支持函数式编程风格,能够高效、简洁地处理集合数据,实现过滤、映射、聚合等操作。
33 5
|
17天前
|
设计模式 消息中间件 搜索推荐
Java 设计模式——观察者模式:从优衣库不使用新疆棉事件看系统的动态响应
【11月更文挑战第17天】观察者模式是一种行为设计模式,定义了一对多的依赖关系,使多个观察者对象能直接监听并响应某一主题对象的状态变化。本文介绍了观察者模式的基本概念、商业系统中的应用实例,如优衣库事件中各相关方的动态响应,以及模式的优势和实际系统设计中的应用建议,包括事件驱动架构和消息队列的使用。
|
28天前
|
设计模式 Java 数据库连接
Java编程中的设计模式:单例模式的深度剖析
【10月更文挑战第41天】本文深入探讨了Java中广泛使用的单例设计模式,旨在通过简明扼要的语言和实际示例,帮助读者理解其核心原理和应用。文章将介绍单例模式的重要性、实现方式以及在实际应用中如何优雅地处理多线程问题。
32 4
|
2月前
|
设计模式 Java 程序员
[Java]23种设计模式
本文介绍了设计模式的概念及其七大原则,强调了设计模式在提高代码重用性、可读性、可扩展性和可靠性方面的作用。文章还简要概述了23种设计模式,并提供了进一步学习的资源链接。
49 0
[Java]23种设计模式
|
1月前
|
设计模式 JavaScript Java
Java设计模式:建造者模式详解
建造者模式是一种创建型设计模式,通过将复杂对象的构建过程与表示分离,使得相同的构建过程可以创建不同的表示。本文详细介绍了建造者模式的原理、背景、应用场景及实际Demo,帮助读者更好地理解和应用这一模式。
|
2月前
|
设计模式 监控 算法
Java设计模式梳理:行为型模式(策略,观察者等)
本文详细介绍了Java设计模式中的行为型模式,包括策略模式、观察者模式、责任链模式、模板方法模式和状态模式。通过具体示例代码,深入浅出地讲解了每种模式的应用场景与实现方式。例如,策略模式通过定义一系列算法让客户端在运行时选择所需算法;观察者模式则让多个观察者对象同时监听某一个主题对象,实现松耦合的消息传递机制。此外,还探讨了这些模式与实际开发中的联系,帮助读者更好地理解和应用设计模式,提升代码质量。
Java设计模式梳理:行为型模式(策略,观察者等)
|
3月前
|
存储 设计模式 安全
Java设计模式-备忘录模式(23)
Java设计模式-备忘录模式(23)
|
3月前
|
设计模式 存储 缓存
Java设计模式 - 解释器模式(24)
Java设计模式 - 解释器模式(24)