XiaoLin's Blog

Xiao Lin

装饰器设计模式学习笔记

10
2024-02-26

装饰器设计模式介绍

装饰器设计模式是一种结构型设计模式,允许在不改变原有对象的基础上动态地为其增加新的职责和功能。这种模式的主要目的是在不修改原始类的情况下扩展其功能或行为。通过将新功能封装成装饰者组件,你可以根据需要为任意数量的实例应用该功能。

装饰器模式有以下几个主要组成部分:

  1. 抽象构件(Component):这是一个接口或者抽象类,定义了被装饰的对象所应该具备的基本行为和特性。
  2. 具体构件(Concrete Component):这是实现了抽象构件接口的具体类,它们可以被装饰。
  3. 装饰者(Decorator):这也是一个接口或者抽象类,它继承了抽象构件的接口。装饰者的作用是为具体构件添加额外的行为和功能。
  4. 具体装饰者(Concrete Decorator):这个类实现了装饰者接口,并为具体的构件添加了特定的功能和行为。每个具体装饰者都持有一个指向相同类型的具体构件对象的引用,并通过调用该对象的方法来完成其自身的功能。

下面是一个简单的 Java 示例,演示了如何使用装饰器模式为一个房间添加不同的装饰品:

// 抽象构件 - 房间
interface Room {
    void addDecoration(Decoration decoration);
    void display();
}

// 具体构件 - 房间A
class RoomA implements Room {
    private List<Decoration> decorations = new ArrayList<>();

    @Override
    public void addDecoration(Decoration decoration) {
        decorations.add(decoration);
    }

    @Override
    public void display() {
        System.out.println("Room A");
        for (Decoration decoration : decorations) {
            decoration.decorate(this);
        }
    }
}

// 抽象装饰者 - 装饰品
interface Decoration {
    void decorate(Room room);
}

// 具体装饰者 - 地毯
class Carpet implements Decoration {
    @Override
    public void decorate(Room room) {
        System.out.println("Adding carpet to the room.");
    }
}

// 具体装饰者 - 画作
class Painting implements Decoration {
    @Override
    public void decorate(Room room) {
        System.out.println("Adding painting to the room.");
    }
}

public class Main {
    public static void main(String[] args) {
        Room roomA = new RoomA();
        roomA.addDecoration(new Carpet());
        roomA.addDecoration(new Painting());
        roomA.display();
    }
}

在这个例子中,Room 是一个抽象构件,表示具有可装饰性的房间;RoomA 是一个具体构件,表示一个具体的房间;Decoration 是一个抽象装饰者,表示可以为房间添加装饰品的通用接口;CarpetPainting 是两个具体装饰者,分别表示地毯和画作这两种装饰品。

通过使用装饰器模式,我们可以轻松地地为房间添加各种装饰品,而不必直接修改房间类的代码。这使得系统的扩展性和灵活性得到了很大的提高。

装饰器模式的具体实现

  1. 装饰器模式就是对具体的实现进行代理和对代理方法的增强,我们需要先定义一个共同的需要实现的接口
package xyz.xiaolinz.demo.decorator.demo1;
/**
 * 成分
 *
 * @author huangmuhong
 * @date 2023/08/09
 */
public interface Component {

  /**
   * 操作
   *
   * @author huangmuhong
   * @date 2023/08/09
   */
  void operation();

}

  1. 定义实例具体实现
package xyz.xiaolinz.demo.decorator.demo1;
/**
 * @author huangmuhong
 * @date 2023/8/9
 */
public class ConcreteComponent implements Component {

  @Override
  public void operation() {
    System.out.println("未被装饰的操作");
  }
}

  1. 定义一个抽象的装饰类,同样实现被装饰接口,并代理目标装饰类
package xyz.xiaolinz.demo.decorator.demo1;
/**
 * 装饰器设计模式
 * 1. 使用组合的方式,实现对被装饰对象的功能增强
 * 2. 装饰器和被装饰对象实现同一个接口,方便扩展
 * 3. 定义通用的抽象装饰器,方便扩展
 *
 * @author huangmuhong
 * @date 2023/8/9
 */
public abstract class Decorator implements Component {

  protected final Component component;

  public Decorator(Component component) {
    this.component = component;
  }

  @Override
  public void operation() {
    component.operation();
  }
}

  1. 定义具体的装饰类实现
package xyz.xiaolinz.demo.decorator.demo1;
/**
 *
 * 具体的装饰器
 * 通过重写父类的方法,实现对父类的功能增强
 *
 * @author huangmuhong
 * @date 2023/8/9
 */
public class DecoratorOne extends Decorator {

  public DecoratorOne(Component component) {
    super(component);
  }

  @Override
  public void operation() {
    System.out.println("装饰器1增强的行为");
    super.operation();
    System.out.println("装饰器1增强的行为");
  }
}

使用场景

在 Java 中,装饰器模式的应用非常广泛,特别是在 I/O 操作中。Java 中的 I/O 类库就是使用装饰器模式来实现不同的数据流之间的转换和增强的。

从 IO 库的设计理解装饰器

在初学 Java 的时候,曾经对 Java IO 的一些用法产生过很大疑惑,比如下面这样一段代码。我们打开文件 test. txt,从中读取数据。其中,InputStream 是一个抽象类,FileInputStream 是专门用来读取文件流的子类。BufferedInputStream 是一个支持带缓存功能的数据读取类,可以提高数据读取的效率,具体的代码如下:

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

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

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

InputStream 基于装饰器模式的设计方案

“组合优于继承”,可以“使用组合来替代继承”。针对刚刚的继承结构过于复杂的问题,我们可以通过将继承关系改为组合关系来解决。下面的代码展示了 Java IO 的这种设计思路。不过,我对代码做了简化,只抽象出了必要的代码结构,如果你感兴趣的话,可以直接去查看 JDK 源码。

public abstract class InputStream {
  //...
  public int read(byte b[]) throws IOException {
    return read(b, 0, b.length);
  }
  
  public int read(byte b[], int off, int len) throws IOException {
    //...
  }
  
  public long skip(long n) throws IOException {
    //...
  }
  public int available() throws IOException {
    return 0;
  }
  
  public void close() throws IOException {}
  public synchronized void mark(int readlimit) {}
    
  public synchronized void reset() throws IOException {
    throw new IOException("mark/reset not supported");
  }
  public boolean markSupported() {
    return false;
  }
}
public class BufferedInputStream extends InputStream {
  protected volatile InputStream in;
  protected BufferedInputStream(InputStream in) {
    this.in = in;
  }
  
  //...实现基于缓存的读数据接口...  
}
public class DataInputStream extends InputStream {
  protected volatile InputStream in;
  protected DataInputStream(InputStream in) {
    this.in = in;
  }
  
  //...实现读取基本类型数据的接口
}


看了上面的代码,你可能会问,那装饰器模式就是简单的“用组合替代继承”吗?当然不是。从 Java IO 的设计来看,装饰器模式相对于简单的组合关系,还有两个比较特殊的地方。

第一个比较特殊的地方是:装饰器类和原始类继承同样的父类,这样我们可以对原始类“嵌套”多个装饰器类。
比如,下面这样一段代码,我们对 FileInputStream 嵌套了两个装饰器类:BufferedInputStream 和 DataInputStream,让它既支持缓存读取,又支持按照基本数据类型来读取数据。

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

第二个比较特殊的地方是:装饰器类是对功能的增强,这也是装饰器模式应用场景的一个重要特点。实际上,符合“组合关系”这种代码结构的设计模式有很多,比如之前讲过的代理模式,还有现在的装饰器模式。尽管它们的代码结构很相似,但是每种设计模式的意图是不同的。
就拿比较相似的代理模式和装饰器模式来说吧,代理模式中,代理类附加的是跟原始类无关的功能,而在装饰器模式中,装饰器类附加的是跟原始类相关的增强功能。

// 代理模式的代码结构(下面的接口也可以替换成抽象类)
public interface IA {
    void f();
}
public class A impelements IA {
    public void f() { //... }
    }
    public class AProxy impements IA {
        private IA a;
        public AProxy(IA a) {
            this.a = a;
        }

        public void f() {
            // 新添加的代理逻辑
            a.f();
            // 新添加的代理逻辑
        }
    }
    // 装饰器模式的代码结构(下面的接口也可以替换成抽象类)
    public interface IA {
        void f();
    }
    public class A impelements IA {
        public void f() { //... }
        }
        public class ADecorator impements IA {
            private IA a;
            public ADecorator(IA a) {
                this.a = a;
            }

            public void f() {
                // 功能增强代码
                a.f();
                // 功能增强代码
            }
        }