GoF装饰器模式在IO中的用例和示例

64

我在 维基百科 上看到过装饰器模式被用于 .NetJava IO 类。

有人能解释一下它是如何使用的吗?还有它的好处是什么,能否给出一个可能的例子?

维基百科上有关于 Windows表单 的示例,但我想知道它如何应用于 Java IO 类。


2
可能是何时需要装饰器模式?的重复问题。 - Vineet Reynolds
3
不是重复问题,因为该问题涉及io库的特定用例。 - Levent Divilioglu
8个回答

161

InputStream 是一个抽象类。大部分具体的实现类,如BufferedInputStreamGzipInputStreamObjectInputStream等,都有一个构造函数,该函数接受一个相同的抽象类实例作为参数。这是装饰器模式的标志性特征(该模式还适用于构造函数接收相同接口实例的情况)。

当使用这样的构造函数时,所有方法将委托给包装的实例,并以不同的方式改变方法的行为。例如,在预先缓存流、预先解压流或不同地解释流方面进行更改。一些实现甚至具有额外的方法,它们最终也会进一步委托给包装的实例。这些方法使用额外的行为修饰了包装的实例。

假设我们有一堆序列化的Java对象存储在一个Gzipped文件中,并且我们想要快速读取它们。

首先打开该文件的输入流:

FileInputStream fis = new FileInputStream("/objects.gz");

我们需要速度,因此让我们在内存中进行缓冲:

BufferedInputStream bis = new BufferedInputStream(fis);

这个文件是被gzip压缩过的,所以我们需要将其解压:

GzipInputStream gis = new GzipInputStream(bis);
我们需要反序列化这些 Java 对象:
ObjectInputStream ois = new ObjectInputStream(gis);
现在我们终于可以使用它了:
SomeObject someObject = (SomeObject) ois.readObject();
// ...

优点在于您可以使用一个或多个各种修饰器来装饰流以适应您的需求,这比拥有每种可能组合的单个类如ObjectGzipBufferedFileInputStreamObjectBufferedFileInputStreamGzipBufferedFileInputStreamObjectGzipFileInputStreamObjectFileInputStreamGzipFileInputStreamBufferedFileInputStream等更好。

请注意,在关闭流时,只需关闭最外层的修饰器即可。它会将关闭调用代理到底部。

ois.close();

另请参见:


20
这是一个非常棒的装饰器示例。 - zsljulius
1
很棒的回答。谢谢! - roxrook
1
FileInputStream没有接受InputStream作为参数的构造函数。 - senseiwu
1
灯泡!终于有一篇文章清楚地解释了为什么装饰器模式很酷! - Johnny Z
在一行代码中,它有点奇怪:ObjectInputStream ois = new ObjectInputStream(new GzipInputStream(new BufferedInputStream(new FileInputStream("/objects.gz")))); 让我们采用Kotlin的方式,摆脱“new”。 - user1708042
显示剩余4条评论

19

在讨论Java IO类之前,我们先了解一下装饰器(Decorator)模式的组成部分。

装饰器模式包含四个组成部分(来源于维基百科):

  1. 组件(Component):定义了可以动态添加职责的对象接口。
  2. 具体组件(ConcreteComponent):Component接口的实现。
  3. 装饰器(Decorator):持有一个对Component的引用,并遵循Component接口。装饰器本质上是将Component进行了包装。
  4. 具体装饰器(ConcreteDecorator):只是为原始Component添加了职责。

现在,让我们将这些概念映射到java.io包中的类。

组件(Component):

InputStream

这个抽象类是所有表示字节输入流的类的超类。
需要定义InputStream子类的应用程序必须始终提供一个返回输入的下一个字节的方法。 public abstract int read()是一个抽象方法。
ConcreteComponent:
FileInputStream:
FileInputStream从文件系统中的文件获取输入字节。可用的文件取决于主机环境。
FileInputStream适用于读取原始字节流(如图像数据)。要读取字符流,请考虑使用FileReader。
所有InputStream的ConcreteComponents示例:
AudioInputStream, ByteArrayInputStream, FileInputStream, FilterInputStream, 
InputStream, ObjectInputStream, PipedInputStream, SequenceInputStream, 
StringBufferInputStream

装饰器:

FilterInputStream

FilterInputStream 包含另一个输入流,它将其作为基本数据源,可能会在此过程中转换数据或提供其他功能。

请注意,FilterInputStream 实现了 InputStream => 装饰器实现了 UML 图中的组件

public class FilterInputStream
extends InputStream

具体装饰者:

BufferedInputStream

BufferedInputStream 添加了对另一个输入流的功能,即缓冲输入并支持标记和重置方法。

所有具体装饰者的示例:

BufferedInputStream, CheckedInputStream, CipherInputStream, DataInputStream, 
DeflaterInputStream, DigestInputStream, InflaterInputStream, 
LineNumberInputStream, ProgressMonitorInputStream, PushbackInputStream

工作示例代码:

我使用了BufferedInputStream来读取存储在文本文件a.txt中的单词的每个字符。

BufferedInputStream bis = new BufferedInputStream(new FileInputStream(new File("ravindra.txt")));
while(bis.available()>0)
{
        char c = (char)bis.read();
        System.out.println("Character = "+c);
}

何时使用此模式:

  1. 对象的职责和行为应该是动态添加/删除的
  2. 具体实现应该与职责和行为解耦
  3. 当子类化成本过高以动态添加/删除职责时

FileInputStream 怎么被认为是具体构件? 它不是一个装饰器,因为它需要一个 InputStream 参数吗? - nikhil
FileInputStream没有以InputStream为参数的构造函数。FileInputStream从文件系统中获取输入字节,因此FileInputStream是ConcreteComponent,但FilterInputStream将InputStream作为构造函数参数以进一步处理它,例如FilterInputStream(InputStream in),因此它是一个装饰器。装饰器始终会将“组件(如InputStream)”作为其构造函数输入来处理它,如上所述。 - Bagesh Sharma

8
在.NET中,有许多流装饰器,例如BufferedStream、CryptoStream、GzipStream等,所有这些都是装饰Stream类的。

7

A - 装饰器模式

A.1 - 装饰器模式的使用场景

装饰器模式用于在不修改旧类的情况下扩展旧功能。假设我们有一个实现接口的具体类。我们需要扩展现有方法的功能,但是因为现有类及其方法已被其他类使用,因此我们不想更改现有类。但是我们也需要在新类上获得扩展功能,那么我们该如何解决这个问题呢?

1- We can't change the existing legacy code
2- We want to extend the functionality

因此,我们使用装饰者模式,在装饰器内部包装现有的类。

B - 基本的GoF装饰器模式示例

这里有一个简单的接口和实现/具体类。该接口有一个简单的方法,即getMessageOfTheDay,它返回一个String。假设有许多其他类使用此方法。因此,如果我们想要更改实现/具体类,则会影响旧遗留代码。我们只想为新类更改它,因此我们使用装饰器模式。

这是一个Gang Of Four Decorator设计模式的微不足道的例子;

B.1 - Greeter.java

public interface Greeter {
    String getMessageOfTheDay();
}

B.2 - BasicGreeter.java

public class BasicGreeter implements Greeter {

    @Override
    public String getMessageOfTheDay() {
        return "Welcome to my server";
    }

}

B.3 - 抽象装饰器类: GreeterDecorator.java


该类是一个抽象类,用于实现装饰器模式。它扩展了 Greeter 接口并包含一个 Greeter 类型的成员变量,以便在运行时动态添加行为。所有具体的装饰器类都必须继承此类,并覆盖 greet() 方法来添加自定义行为。
public abstract class GreeterDecorator implements Greeter {

    protected Greeter greeter;

    public GreeterDecorator(Greeter greeter) {
        this.greeter = greeter;
    }

    public String getMessageOfTheDay() {
        return greeter.getMessageOfTheDay();
    }

}

B.4 - 具体装饰者类:StrangerDecorator.java

public class StrangerDecorator extends GreeterDecorator {

    public StrangerDecorator(Greeter greeter) {
        super(greeter);
    }

    @Override
    public String getMessageOfTheDay() {
        return "Hello Stranger " + super.getMessageOfTheDay();
    }

}

B.5 - 演示代码:DecoratorDemo.java

public class DecoratorDemo {

    public static void main(String[] args) {
        Greeter greeter = new BasicGreeter();

        String motd = greeter.getMessageOfTheDay();

        System.out.println(motd);

        Greeter newGreeter = new StrangerDecorator(greeter);

        String newMotd = newGreeter.getMessageOfTheDay();

        System.out.println(newMotd);

        Greeter muchNewGreeter = new StrangerDecorator(new StrangerDecorator(greeter));

        String newestMotd = muchNewGreeter.getMessageOfTheDay();

        System.out.println(newestMotd);
    }

}

请看以下示例。抽象装饰器类需要包装原始合同和实现。使用抽象装饰器,您可以创建新的多个装饰器,但在此示例中,BasicGreeter被包装在抽象装饰器内,我们只创建了一个新的装饰器类,即StrangeGreeter。请注意,装饰器类可以像火车一样使用,我们可以将一个装饰器包装在另一个装饰器或相同的装饰器内。功能是可扩展的,但原始类没有任何修改。

C - OutputStream演示

让我们看一下这个例子。我们想用OutputStream将字符串写入文件。以下是演示代码;

C.1 - 写入文件的示例OutputStream演示

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

public class FileWriterDemo {

    public static void main(String[] args) throws IOException {
        File file = new File("./normal.txt");
        file.createNewFile();

        OutputStream oStream = new FileOutputStream(file);

        String content = "I love Commodore 64";

        oStream.write(content.getBytes());

        oStream.close();
    }

}

C.2 - JSON装饰器输出: normal.txt

在项目文件夹下将创建一个名为"normal.txt"的新文件,其内容如下:

I love Commodore 64

D - JSON OutputStream 装饰器示例

现在,我想创建一个以下格式的 JSON 包装器;

{
    data: <data here>
}

我希望能够将内容写入简单的一字段JSON格式中。我们如何实现这个目标?有很多琐碎的方法。然而,我会使用GoF装饰器模式,编写一个名为JSONDecorator的类,它扩展了Java的OutputStream类。
D.1 - 用于OutputStream的JSON装饰器:JSONStream.java
public class JSONStream extends OutputStream {

    protected OutputStream outputStream;

    public JSONStream(OutputStream outputStream) {
        this.outputStream = outputStream;
    }

    @Override
    public void write(int b) throws IOException {
        outputStream.write(b);
    }

    @Override
    public void write(byte[] b) throws IOException {
        String content = new String(b);

        content = "{\r\n\tdata:\"" + content + "\"\r\n}";

        outputStream.write(content.getBytes());
    }

}

D.2 - JSON装饰器演示:JSONDecoratorDemo.java

public class JSONDecoratorDemo {

    public static void main(String[] args) throws IOException {
        File file = new File("./json.txt");
        file.createNewFile();

        OutputStream oStream = new FileOutputStream(file);

        JSONStream js = new JSONStream(oStream);

        String content = "I love Commodore 64";

        js.write(content.getBytes());

        js.close();
        oStream.close();
    }

}

D.3 - JSON Decorator Output: json.txt

{
    data:"I love Commodore 64"
}

实际上,OutputStream 本身就是一个装饰器模式,而这里的抽象装饰器是它自己,具体装饰器是 JSONStream 类。


js.close() 应该通过调用 JSONStream 中覆盖的 close 来自动关闭 oStream,因此不需要 oStream.close()。 - Bagesh Sharma

5
装饰者模式在java.io类中被用于操作输入/输出流(对于读者和写入者也是如此)。
inputstream、bytearrayinputstream、stringbuilderinputstreams等都是基础元素。Filterinputstream是装饰器类的基类。过滤输入流(例如bufferedinput stream)在读取流或向流中写入时可以执行附加操作。
它们通过封装流来构建,并且本身也是流。
new BufferedReader( new FileInputStream() ).readLine();

我无法想到在java.net中实现此模式的任何类,但我认为您被告知了此包,因为它与java.io紧密相关(例如socket.getInputStream)。
实际上,这里有一门来自O'Relly的课程(uwosh.edu上的pdf | archive.org, slideshare.net上的幻灯片),解释了如何在java.io中实现装饰器。

2

您可以通过应用压缩/解压缩来装饰输入/输出流。例如,可以查看java.util.zip中的类。这样装饰的流可以像“常规”的输入/输出流一样使用,完全透明地执行压缩/解压缩。


2

装饰者模式用于为已存在的对象(例如在库中定义的类)添加功能。您可以将其“装饰”以适应您的需求。如果您对学习模式感兴趣,我建议阅读《设计模式》(Gang of Four著)。


2

嗯,我可能有点晚了,但是这个问题从来没有过时。理解装饰器的关键是它使您能够将一个对象插入到另一个现有对象中,然后再插入到另一个现有对象中,依此类推。在构造函数中实现此模式很流行。例如:

    Icecream ic = new RainbowTopUp(new ChocoTopUp(new Vanilla()));

如果您查看维基百科中的图表,您会看到ConcreteComponentDecorator继承自相同的超类/接口,即Component。也就是说,这两个类有相同的实现方法。

然而,在Decorator类中,您会看到一个指向Component的箭头,这意味着您在Decorator类中使用了Component。在这种情况下,您将Component用作Decorator中构造函数的数据类型。这就是重点所在。如果没有这个技巧,您将无法将新对象插入到现有对象中。

之后,您可以创建从Decorator类继承的子类。由于所有类都具有相同的根,因此每个类都可以自由地插入而无需任何顺序。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接