写点什么

08. 装饰者模式设计思想

作者:杨充
  • 2024-11-04
    湖北
  • 本文字数:9297 字

    阅读完需:约 31 分钟

08.装饰者模式设计思想

目录介绍

  • 01.装饰者模式基础

  • 1.1 装饰者模式由来

  • 1.2 装饰者模式定义

  • 1.3 装饰者模式场景

  • 1.4 装饰者模式思考

  • 02.装饰者模式实现

  • 2.1 罗列一个场景

  • 2.2 装饰者结构

  • 2.3 装饰者基本实现

  • 03.装饰者实例演示

  • 3.1 需求分析

  • 3.2 案例基础实现

  • 3.3 演变设计思想

  • 3.4 使用装饰者模式

  • 3.5 装饰器能否精简

  • 3.6 透明性的要求

  • 3.7 半透明装饰者模式

  • 04.装饰器模式场景

  • 4.1 IO 流中装饰者模式

  • 4.2 IO 流设计结构

  • 4.3 半透明装饰者模式

  • 4.4 InputStream 装饰者模式

  • 05.装饰器模式分析

  • 5.1 装饰模式优点

  • 5.2 装饰模式缺点

  • 5.3 总结一下学习

  • 5.4 更多内容推荐

推荐一个好玩网站

一个最纯粹的技术分享网站,打造精品技术编程专栏!编程进阶网


https://yccoding.com/

01.装饰者模式基础

1.0 本博客 AI 摘要

装饰者模式是一种设计模式,用于在不修改原始类的情况下动态地给对象添加新功能。本文档详细介绍了装饰者模式的基础概念、实现方法、应用场景及优缺点。通过具体的咖啡示例和 Java I/O 流中的应用,展示了装饰者模式的灵活性和实用性。此外,还探讨了半透明装饰者模式的概念及其在实际开发中的应用。适合初学者和有一定经验的开发者学习。

1.1 装饰者模式由来

一般有两种方式可以实现给一个类或对象增加行为:


  1. 继承机制,使用继承机制是给现有类添加功能的一种有效途径,通过继承一个现有类可以使得子类在拥有自身方法的同时还拥有父类的方法。但是这种方法是静态的,用户不能控制增加行为的方式和时机。

  2. 关联机制,即将一个类的对象嵌入另一个对象中,由另一个对象来决定是否调用嵌入对象的行为以便扩展自己的行为,我们称这个嵌入的对象为装饰器(Decorator)


装饰模式以对客户透明的方式动态地给一个对象附加上更多的责任,换言之,客户端并不会觉得对象在装饰前和装饰后有什么不同。


装饰模式可以在不需要创造更多子类的情况下,将对象的功能加以扩展。这就是装饰模式的模式动机

1.2 装饰者模式定义

装饰者模式又名包装(Wrapper)模式。装饰者模式以对客户端透明的方式扩展对象的功能,是继承关系的一个替代方案。


装饰者模式动态地将责任附加到对象身上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案

1.3 装饰者模式场景

装饰者模式适用于以下场景


  1. 动态地给一个对象添加额外的功能,而不需要修改其原始类的代码。装饰者模式允许在运行时动态地添加、修改或删除对象的行为。

  2. 需要扩展一个类的功能,但是使用继承会导致类的数量急剧增加。通过装饰者模式,可以避免创建大量的子类,而是通过组合不同的装饰器来实现功能的扩展。

  3. 需要在不影响其他对象的情况下,对对象的功能进行动态组合和排列。装饰者模式可以通过不同的装饰器组合来实现不同的功能组合,而不需要修改原始对象或其他装饰器。


装饰者模式常见的应用场景包括


  1. Java 中 IO 流操作中的装饰者模式、Swing GUI 组件中的装饰者模式、Java I/O 库中的 BufferedInputStream 和 BufferedOutputStream 等。

  2. Android 中自定义 View 的功能扩展、RecyclerView 的 ItemDecoration、Fragment 的功能增强等。

  3. 其他日志功能,日志记录功能的实现也是装饰者模式的理想应用场景。例如,传统的日志记录器 Logger 可以通过装饰者模式添加不同的日志处理和记录策略,如格式化日志、输出到文件、发送电子邮件等。

1.4 装饰者模式思考

当考虑使用装饰者模式时,可以思考以下几个方面:更多内容


  1. 是否需要在运行时动态地添加、修改或删除对象的行为?如果是的话,装饰者模式符合要求

  2. 需要扩展一个类的功能,但是使用继承会导致类的数量急剧增加吗?如果是的话,装饰者模式可以通过组合不同的装饰器来实现功能的扩展,而不需要创建大量的子类。

  3. 是否需要为一个对象的不同部分或属性提供独立的扩展?如果是的话,装饰者模式允许对对象的不同部分进行独立的扩展,而不会影响其他部分。

02.装饰者模式实现

2.1 罗列一个场景

我们将以一个具体的咖啡示例来介绍装饰者模式的实现。在这个例子中,我们有一个基本的咖啡对象,可以动态地添加不同的配料(如牛奶和糖)。更多内容


比如:


  1. 咖啡 A,加牛奶和半糖

  2. 咖啡 B,加牛奶和全糖

  3. 咖啡 C,加牛奶和多糖

  4. 咖啡 D,不加牛奶和半糖

2.2 装饰者结构

装饰者模式以对客户透明的方式动态地给一个对象附加上更多的责任。


换言之,客户端并不会觉得对象在装饰前和装饰后有什么不同。装饰者模式可以在不使用创造更多子类的情况下,将对象的功能加以扩展。


在装饰模式中的角色有:更多内容


  1. ● 抽象构件(Component)角色:给出一个抽象接口,以规范准备接收附加责任的对象。

  2. ● 具体构件(ConcreteComponent)角色:定义一个将要接收附加责任的类。

  3. ● 装饰(Decorator)角色:持有一个构件(Component)对象的实例,并定义一个与抽象构件接口一致的接口。

  4. ● 具体装饰(ConcreteDecorator)角色:负责给构件对象“贴上”附加的责任。

2.3 装饰者基本实现

抽象构件角色


// Java实现public interface Coffee {    double cost();    String getDescription();}
复制代码


具体构件角色


public class SimpleCoffee implements Coffee {    public double cost() {        return 5.0;    }        public String getDescription() {        return "Simple Coffee";    }}
复制代码


装饰角色


public abstract class CoffeeDecorator implements Coffee {    protected Coffee coffee;
public CoffeeDecorator(Coffee coffee) { this.coffee = coffee; } public double cost() { return coffee.cost(); } public String getDescription() { return coffee.getDescription(); }}
复制代码


具体装饰角色


public class MilkDecorator extends CoffeeDecorator {    public MilkDecorator(Coffee coffee) {        super(coffee);    }
public double cost() { return super.cost() + 1.5; } public String getDescription() { return super.getDescription() + ", Milk"; }}
public class SugarDecorator extends CoffeeDecorator { public SugarDecorator(Coffee coffee) { super(coffee); }
public double cost() { return super.cost() + 0.5; } public String getDescription() { return super.getDescription() + ", Sugar"; }}
复制代码


使用装饰者模式


private void test1() {    Coffee coffee = new SimpleCoffee();    System.out.println(coffee.getDescription() + " Cost: $" + coffee.cost());
coffee = new MilkDecorator(coffee); System.out.println(coffee.getDescription() + " Cost: $" + coffee.cost());
coffee = new SugarDecorator(coffee); System.out.println(coffee.getDescription() + " Cost: $" + coffee.cost());}
复制代码

03.装饰者实例演示

3.1 需求分析

齐天大圣的例子


孙悟空有七十二般变化,他的每一种变化都给他带来一种附加的本领。他变成鱼儿时,就可以到水里游泳;他变成鸟儿时,就可以在天上飞行。

3.2 案例基础实现

3.3 演变设计思想

3.4 使用装饰者模式

本例中,Component 的角色便由鼎鼎大名的齐天大圣扮演;ConcreteComponent 的角色属于大圣的本尊,就是猢狲本人;Decorator 的角色由大圣的七十二变扮演。而 ConcreteDecorator 的角色便是鱼儿、鸟儿等七十二般变化。


抽象构件角色“齐天大圣”接口定义了一个 move()方法,这是所有的具体构件类和装饰类必须实现的。


//大圣的尊号public interface TheGreatestSage {    void move();}
复制代码


具体构件角色“大圣本尊”猢狲类更多内容


public class Monkey implements TheGreatestSage {
@Override public void move() { //代码 System.out.println("Monkey Move"); }}
复制代码


抽象装饰角色“七十二变”


public class Change implements TheGreatestSage {    private TheGreatestSage sage;
public Change(TheGreatestSage sage){ this.sage = sage; } @Override public void move() { // 代码 sage.move(); }}
复制代码


具体装饰角色“鱼儿”


public class Fish extends Change {
public Fish(TheGreatestSage sage) { super(sage); }
@Override public void move() { // 代码 System.out.println("Fish Move"); }}
复制代码


具体装饰角色“鸟儿”


public class Bird extends Change {
public Bird(TheGreatestSage sage) { super(sage); }
@Override public void move() { // 代码 System.out.println("Bird Move"); }}
复制代码


客户端调用


public class Client {
public static void main(String[] args) { TheGreatestSage sage = new Monkey(); // 第一种写法 单层装饰 TheGreatestSage bird = new Bird(sage); TheGreatestSage fish = new Fish(bird); // 第二种写法 双层装饰 //TheGreatestSage fish = new Fish(new Bird(sage)); fish.move(); }}
复制代码


“大圣本尊”是 ConcreteComponent 类,而“鸟儿”、“鱼儿”是装饰类。要装饰的是“大圣本尊”,也即“猢狲”实例。


上面的例子中,第二种些方法:系统把大圣从一只猢狲装饰成了一只鸟儿(把鸟儿的功能加到了猢狲身上),然后又把鸟儿装饰成了一条鱼儿(把鱼儿的功能加到了猢狲+鸟儿身上,得到了猢狲+鸟儿+鱼儿)。

3.5 装饰器能否精简

大多数情况下,装饰者模式的实现都要比上面给出的示意性例子要简单。更多内容


如果只有一个 ConcreteComponent 类,那么可以考虑去掉抽象的 Component 类(接口),把 Decorator 作为一个 ConcreteComponent 子类。


如果只有一个 ConcreteDecorator 类,那么就没有必要建立一个单独的 Decorator 类,而可以把 Decorator 和 ConcreteDecorator 的责任合并成一个类。甚至在只有两个 ConcreteDecorator 类的情况下,都可以这样做。

3.6 透明性的要求

装饰者模式的透明性要求是指在使用装饰者模式时,对于客户端来说,应该能够透明地使用原始对象和装饰器对象,而不需要关心具体的对象类型。


装饰者模式对客户端的透明性要求程序不要声明一个 ConcreteComponent 类型的变量,而应当声明一个 Component 类型的变量。


用孙悟空的例子来说,必须永远把孙悟空的所有变化都当成孙悟空来对待,而如果把老孙变成的鱼儿当成鱼儿,而不是老孙,那就被老孙骗了,而这时不应当发生的。下面的做法是对的:


TheGreatestSage sage = new Monkey();TheGreatestSage bird = new Bird(sage);
复制代码


而下面的做法是不对的:


Monkey sage = new Monkey();Bird bird = new Bird(sage);
复制代码


在实际应用中,可能会因为装饰器对象的嵌套而导致透明性的破坏。因此,在使用装饰者模式时,需要权衡透明性和复杂性,并根据具体的需求和情况做出决策。

3.7 半透明装饰者模式

然而,纯粹的装饰者模式很难找到。装饰者模式的用意是在不改变接口的前提下,增强所考虑的类的性能。在增强性能的时候,往往需要建立新的公开的方法。更多内容


即便是在孙大圣的系统里,也需要新的方法。比如齐天大圣类并没有飞行的能力,而鸟儿有。这就意味着鸟儿应当有一个新的 fly()方法。再比如,齐天大圣类并没有游泳的能力,而鱼儿有,这就意味着在鱼儿类里应当有一个新的 swim()方法。


这就导致了大多数的装饰者模式的实现都是“半透明”的,而不是完全透明的。换言之,允许装饰者模式改变接口,增加新的方法。这意味着客户端可以声明 ConcreteDecorator 类型的变量,从而可以调用 ConcreteDecorator 类中才有的方法:


TheGreatestSage sage = new Monkey();Bird bird = new Bird(sage);bird.fly();
复制代码


半透明的装饰者模式是介于装饰者模式和适配器模式之间的。适配器模式的用意是改变所考虑的类的接口,也可以通过改写一个或几个方法,或增加新的方法来增强或改变所考虑的类的功能。大多数的装饰者模式实际上是半透明的装饰者模式,这样的装饰者模式也称做半装饰、半适配器模式。

04.装饰器模式场景

4.1 IO 流中装饰者模式

装饰者模式在 Java 语言中的最著名的应用莫过于 Java I/O 标准库的设计了。


由于 Java I/O 库需要很多性能的各种组合,如果这些性能都是用继承的方法实现的,那么每一种组合都需要一个类,这样就会造成大量性能重复的类出现。而如果采用装饰者模式,那么类的数目就会大大减少,性能的重复也可以减至最少。因此装饰者模式是 Java I/O 库的基本模式。


Java I/O 库的对象结构图如下,由于 Java I/O 的对象众多,因此只画出 InputStream 的部分。


  1. InputStream 是最上层的父类,表示字节流

  2. FileInputStream 是一个具体类,用来对文件进行读取

  3. BufferedInputStream 是字节缓冲流,主要是为了减少 I/O 操作的次数,提高效率。

4.2 IO 流设计结构

IO 流中装饰者模式结构如下所示:更多内容


  1. 抽象构件(Component)角色:由 InputStream 扮演。这是一个抽象类,为各种子类型提供统一的接口。

  2. 具体构件(ConcreteComponent)角色:由 ByteArrayInputStream、FileInputStream、PipedInputStream、StringBufferInputStream 等类扮演。它们实现了抽象构件角色所规定的接口。

  3. 抽象装饰(Decorator)角色:由 FilterInputStream 扮演。它实现了 InputStream 所规定的接口。

  4. 具体装饰(ConcreteDecorator)角色:由几个类扮演,分别是 BufferedInputStream、DataInputStream 以及两个不常用到的类 LineNumberInputStream、PushbackInputStream。

4.3 半透明装饰者模式

装饰者模式和适配器模式都是“包装模式(Wrapper Pattern)”,它们都是通过封装其他对象达到设计的目的的,但是它们的形态有很大区别。


理想的装饰者模式在对被装饰对象进行功能增强的同时,要求具体构件角色、装饰角色的接口与抽象构件角色的接口完全一致。而适配器模式则不然,一般而言,适配器模式并不要求对源对象的功能进行增强,但是会改变源对象的接口,以便和目标接口相符合。


装饰者模式有透明和半透明两种,这两种的区别就在于装饰角色的接口与抽象构件角色的接口是否完全一致。透明的装饰者模式也就是理想的装饰者模式,要求具体构件角色、装饰角色的接口与抽象构件角色的接口完全一致。相反,如果装饰角色的接口与抽象构件角色接口不一致,也就是说装饰角色的接口比抽象构件角色的接口宽的话,装饰角色实际上已经成了一个适配器角色,这种装饰者模式也是可以接受的,称为“半透明”的装饰模式。


在适配器模式里面,适配器类的接口通常会与目标类的接口重叠,但往往并不完全相同。换言之,适配器类的接口会比被装饰的目标类接口宽。更多内容


显然,半透明的装饰者模式实际上就是处于适配器模式与装饰者模式之间的灰色地带。如果将装饰者模式与适配器模式合并成为一个“包装模式”的话,那么半透明的装饰者模式倒可以成为这种合并后的“包装模式”的代表。

4.4 InputStream 装饰者模式

InputStream 类型中的装饰者模式是半透明的。为了说明这一点,不妨看一看作装饰者模式的抽象构件角色的 InputStream 的源代码。这个抽象类声明了九个方法,并给出了其中八个的实现,另外一个是抽象方法,需要由子类实现。


public abstract class InputStream implements Closeable {
public abstract int read() throws IOException;
public int read(byte b[]) throws IOException {}
public int read(byte b[], int off, int len) throws IOException {}
public long skip(long n) throws IOException {}
public int available() throws IOException {}
public void close() throws IOException {}
public synchronized void mark(int readlimit) {}
public synchronized void reset() throws IOException {}
public boolean markSupported() {}}
复制代码


下面是作为装饰模式的抽象装饰角色 FilterInputStream 类的源代码。更多内容可以看出,FilterInputStream 的接口与 InputStream 的接口是完全一致的。也就是说,直到这一步,还是与装饰模式相符合的。


public class FilterInputStream extends InputStream {    protected FilterInputStream(InputStream in) {}
public int read() throws IOException {}
public int read(byte b[]) throws IOException {}
public int read(byte b[], int off, int len) throws IOException {}
public long skip(long n) throws IOException {}
public int available() throws IOException {}
public void close() throws IOException {}
public synchronized void mark(int readlimit) {}
public synchronized void reset() throws IOException {}
public boolean markSupported() {}}
复制代码


下面是具体装饰角色 PushbackInputStream 的源代码。


public class PushbackInputStream extends FilterInputStream {    private void ensureOpen() throws IOException {}
public PushbackInputStream(InputStream in, int size) {}
public PushbackInputStream(InputStream in) {}
public int read() throws IOException {}
public int read(byte[] b, int off, int len) throws IOException {}
public void unread(int b) throws IOException {}
public void unread(byte[] b, int off, int len) throws IOException {}
public void unread(byte[] b) throws IOException {}
public int available() throws IOException {}
public long skip(long n) throws IOException {}
public boolean markSupported() {}
public synchronized void mark(int readlimit) {}
public synchronized void reset() throws IOException {}
public synchronized void close() throws IOException {}}
复制代码


查看源码,你会发现,这个装饰类提供了额外的方法 unread(),这就意味着 PushbackInputStream 是一个半透明的装饰类。更多内容,换言 之,它破坏了理想的装饰者模式的要求。如果客户端持有一个类型为 InputStream 对象的引用 in 的话,那么如果 in 的真实类型是 PushbackInputStream 的话,只要客户端不需要使用 unread()方法,那么客户端一般没有问题。但是如果客户端必须使用这个方法,就 必须进行向下类型转换。将 in 的类型转换成为 PushbackInputStream 之后才可能调用这个方法。但是,这个类型转换意味着客户端必须知道它 拿到的引用是指向一个类型为 PushbackInputStream 的对象。这就破坏了使用装饰者模式的原始用意。


现实世界与理论总归是有一段差距的。纯粹的装饰者模式在真实的系统中很难找到。一般所遇到的,都是这种半透明的装饰者模式。


下面是使用 I/O 流读取文件内容的简单操作示例。


public static void main(String[] args) throws IOException {    // 流式读取文件    DataInputStream dis = null;    try{        dis = new DataInputStream(new BufferedInputStream(new FileInputStream("test.txt")));        //读取文件内容        byte[] bs = new byte[dis.available()];        dis.read(bs);        String content = new String(bs);        System.out.println(content);    }finally{        dis.close();    }}
复制代码


观察上面的代码,会发现最里层是一个 FileInputStream 对象,然后把它传递给一个 BufferedInputStream 对象,经过 BufferedInputStream 处理,再把处理后的对象传递给了 DataInputStream 对象进行处理,这个过程其实就是装饰器的组装过程。


FileInputStream 对象相当于原始的被装饰的对象,而 BufferedInputStream 对象和 DataInputStream 对象则相当于装饰器。更多内容

05.装饰器模式分析

5.1 装饰模式优点

  1. 1)装饰模式与继承关系的目的都是要扩展对象的功能,但是装饰模式可以提供比继承更多的灵活性。装饰模式允许系统动态决定“贴上”一个需要的“装饰”,或者除掉一个不需要的“装饰”。继承关系则不同,继承关系是静态的,它在系统运行前就决定了。

  2. 2)通过使用不同的具体装饰类以及这些装饰类的排列组合,设计师可以创造出很多不同行为的组合。

5.2 装饰模式缺点

  1. 由于使用装饰模式,可以比使用继承关系需要较少数目的类。使用较少的类,当然使设计比较易于进行。更多内容,但是,在另一方面,使用装饰模式会产生比使用继承关系更多的对象。更多的对象会使得查错变得困难,特别是这些对象看上去都很相像。

5.3 总结一下学习

01.装饰者模式基础


给一个类或者对象增加行为:1.继承,子类通过重写父类方法修改行为。2.关联机制,将一个类的对象嵌入另一个对象中用于行为拓展。


装饰者模式以对客户端透明的方式扩展对象的功能,是继承关系的一个替代方案。


装饰者模式适用于以下场景:更多内容


  1. 动态地给一个对象添加额外的功能,而不需要修改其原始类的代码。

  2. 需要扩展一个类的功能,但是使用继承会导致类的数量急剧增加。

  3. 需要在不影响其他对象的情况下,对对象的功能进行动态组合和排列。


02.装饰者模式实现


比如:有一个基本的咖啡对象,可以动态地添加不同的配料(如牛奶和糖)。在不改变对象情况下,对行为进行拓展!


  1. 咖啡 A,加牛奶和半糖

  2. 咖啡 B,加牛奶和全糖

  3. 咖啡 C,加牛奶和多糖

  4. 咖啡 D,不加牛奶和半糖


在装饰模式中的角色有:


  1. ● 抽象构件(Component)角色:给出一个抽象接口,以规范准备接收附加责任的对象。

  2. ● 具体构件(ConcreteComponent)角色:定义一个将要接收附加责任的类。

  3. ● 装饰(Decorator)角色:持有一个构件(Component)对象的实例,并定义一个与抽象构件接口一致的接口。

  4. ● 具体装饰(ConcreteDecorator)角色:负责给构件对象“贴上”附加的责任。


03.装饰者实例演示


半透明的装饰者模式是介于装饰者模式和适配器模式之间的。适配器模式的用意是改变所考虑的类的接口,也可以通过改写一个或几个方法,或增加新的方法来增强或改变所考虑的类的功能。大多数的装饰者模式实际上是半透明的装饰者模式,这样的装饰者模式也称做半装饰、半适配器模式。


04.装饰器模式场景


装饰者模式在 Java 语言中的最著名的应用莫过于 Java I/O 标准库的设计了。


IO 流中装饰者模式结构如下所示:


  1. 抽象构件(Component)角色:由 InputStream 扮演。这是一个抽象类,为各种子类型提供统一的接口。

  2. 具体构件(ConcreteComponent)角色:由 ByteArrayInputStream、FileInputStream、PipedInputStream、StringBufferInputStream 等类扮演。它们实现了抽象构件角色所规定的接口。

  3. 抽象装饰(Decorator)角色:由 FilterInputStream 扮演。它实现了 InputStream 所规定的接口。

  4. 具体装饰(ConcreteDecorator)角色:由几个类扮演,分别是 BufferedInputStream、DataInputStream 以及两个不常用到的类 LineNumberInputStream、PushbackInputStream。

5.4 更多内容推荐


23 种设计模式


5.5 更多内容


发布于: 刚刚阅读数: 4
用户头像

杨充

关注

还未添加个人签名 2018-07-30 加入

还未添加个人简介

评论

发布
暂无评论
08.装饰者模式设计思想_杨充_InfoQ写作社区