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