百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

从设计IO流到装饰器模式(设计模式 装饰器模式)

wxin55 2024-11-06 12:49 12 浏览 0 评论

装饰器模式

装饰器模式和代理器模式都能给原对象来添加额外的行为。装饰器模式是结构性模式的一种,它可以在不改变原对象的基础上动态的给对象增加额外的行为。它通过创建一个包含原对象引用的装饰器对象,动态的提供额外的行为,它的优点是在动态的增加行为时不会改变原对象/类的架构。

装饰器模式常用来给对象添加额外的行为,那么在JDK中有哪些使用场景来帮助我们理解其思想呢?代理类也能实现额外的功能,为什么还需要装饰器模式呢?这两者之间有什么区别呢?什么叫动态性呢?

本文我将从IO类实现演变来探究装饰器模式的用法和思想,然后通过对比代理模式来说明装饰器模式的本质区别和联系,最后解释装饰器模式的”动态性“。

从IO类设计看装饰器模式

这么说有点抽象,我们来看一个实际的场景,

需求一

假如你是类库开发者,现在让你来设计一组提供基础文件/网络流读写/内存读写的类,这个需求很简单,你马上完成。

第一步:根据需求抽象出接口InputStreamOutputStream 接口

public interface class InputStream {
    public int read() throws IOException;
    public int read(byte[] b) throws IOException;
    public int read(byte[] b, int off, int len) throws IOException;
}

public interface class OutputStream {
    public void write(int b) throws IOException;
    public void write(byte[] b) throws IOException;
    public void write(byte[] b, int off, int len) throws IOException;
}

第二步:继承接口实现文件/网络资源/内存读写类FileInputStreamFileOutputStreamUriInputStreamUriOutputStream,RamInputStreamRamOutputStream

public class FileInputStream implement InputStream {
    // 构造函数
    public FileInputStream(File file) throws FileNotFoundException {
        super();
        // 初始化文件输入流
    }

    // 实现 InputStream 中的方法
    @Override
    public int read() throws IOException {
        // 读取一个字节
        return 0;
    }

    // 其他方法省略
}

public class FileOutputStream implement OutputStream {
    // 构造函数
    public FileOutputStream(File file) throws FileNotFoundException {
        super();
        // 初始化文件输出流
    }

    // 实现 OutputStream 中的方法
    @Override
    public void write(int b) throws IOException {
        // 写入一个字节
    }

    // 其他方法省略
}
public class UriInputStream implement InputStream {
    // 构造函数
    public UriInputStream(String uri) throws UriNotFoundException {
        super();
        // 初始化文件输入流
    }

    // 实现 InputStream 中的方法
    @Override
    public int read() throws IOException {
        // 读取一个字节
        return 0;
    }

    // 其他方法省略
}

public class UriOutputStream implement OutputStream {
    // 构造函数
    public UriOutputStream(String uri) throws UriNotFoundException {
        super();
        // 初始化文件输出流
    }

    // 实现 OutputStream 中的方法
    @Override
    public void write(int b) throws IOException {
        // 写入一个字节
    }

    // 其他方法省略
}
public class RamInputStream implement InputStream {
    // 构造函数
    public RamInputStream(String uri) throws RamNotFoundException {
        super();
        // 初始化文件输入流
    }

    // 实现 InputStream 中的方法
    @Override
    public int read() throws IOException {
        // 读取一个字节
        return 0;
    }

    // 其他方法省略
}

public class RamOutputStream implement OutputStream {
    // 构造函数
    public RamOutputStream(String uri) throws RamNotFoundException {
        super();
        // 初始化文件输出流
    }

    // 实现 OutputStream 中的方法
    @Override
    public void write(int b) throws IOException {
        // 写入一个字节
    }

    // 其他方法省略
}

需求二

现在加一个需求,需要你提供能实现缓存读写、数据读写、鉴权等功能的模块给别人使用,且这三个功能任意组合使用。

现在你就需要好好想一下如何实现了:

1、继承实现:最简单的办法是通过继承来实现.目前有2个接口,6个基类实现三个资源形式读取,共8个类。假设实现3个额外的功能,那么需要实现6*A3!=36个类。这会导致类爆炸,而且如果需要修改某一个功能,也会涉及到多个类的修改,违背了开闭原则。

2、组合实现:如果采用组合的方式实现呢,如实现缓存读写、数据读写、鉴权等功能的类,该类持有一个接口对象,那么就只需要实现2*3=6个类。通过这个计算你知道为什么说组合优于继承了嘛?

第一步:接下来就按照组合的思路实现缓存读写的BufferedInputStreamBufferedOutputStream

public class BufferedInputStream extends FileInputStream {
    private byte[] buf;
    private int pos, count;

    // 构造函数
    public BufferedInputStream(InputStream in) {
        super(in);
        buf = new byte[8192]; // 默认缓冲区大小
        pos = count = 0;
    }

    // 读取一个字节
    @Override
    public int read() throws IOException {
        if (pos >= count) {
            fill();
        }
        if (pos >= count) {
            return -1;
        }
        return buf[pos++] & 0xff;
    }

    // 填充缓冲区
    private void fill() throws IOException {
        pos = 0;
        count = in.read(buf);
    }

    // 其他方法省略
}

public class BufferedOutputStream extends FileOutputStream {
    private byte[] buf;
    private int count;

    // 构造函数
    public BufferedOutputStream(OutputStream out) {
        super(out);
        buf = new byte[8192]; // 默认缓冲区大小
        count = 0;
    }

    // 写入一个字节
    @Override
    public void write(int b) throws IOException {
        if (count >= buf.length) {
            flushBuffer();
        }
        buf[count++] = (byte)b;
    }

    // 刷新缓冲区
    private void flushBuffer() throws IOException {
        out.write(buf, 0, count);
        count = 0;
    }

    // 其他方法省略
}

第二步:实现数据读写功能,DataInputStreamDataOutputStream

public class DataInputStream extends FileInputStream {
    // 构造函数
    public DataInputStream(InputStream in) {
        super(in);
    }

    // 读取一个整数
    public int readInt() throws IOException {
        int i = ((in.read() << 24) |
                 (in.read() << 16) |
                 (in.read() << 8) |
                 (in.read()));
        return i;
    }

    // 其他方法省略
}

public class DataOutputStream extends FileOutputStream {
    // 构造函数
    public DataOutputStream(OutputStream out) {
        super(out);
    }

    // 写入一个整数
    public void writeInt(int v) throws IOException {
        out.write((v >>> 24) & 0xFF);
        out.write((v >>> 16) & 0xFF);
        out.write((v >>> 8) & 0xFF);
        out.write(v & 0xFF);
    }

    // 其他方法省略
}

到这相信你已经知道了Java中IO流的设计思路。但是在JDK中实际的IO比这个实现方式更高级,BufferedInputStream /DataInputStream 没有继承自InputStream,而是继承了一个FilterInputStream。

这是为什么呢?这是因为FilterInputStream 实现了InputStream中很多方法,这样BufferedInputStream /DataInputStream中就不用处理不需要增强的方法了,精简了代码。BufferedOutputStream /DataOutputStream也是同理。

从IO流中抽象出装饰器类的角色

通过Java IO设计中抽象出装饰器模式的相关角色,指导我们设计自己的装饰器:

  1. 抽象组件: InputStreamOutputStream 接口定义了所有组件的公共行为。
  2. 具体组件: FileInputStreamFileOutputStream 是实现了这些接口的具体类。
  3. 装饰器: FilterInputStreamFilterOutputStream 是装饰器类,它们持有对抽象组件的引用,并提供了基本的方法实现。
  4. 具体装饰器: BufferedInputStream, BufferedOutputStream, DataInputStream, DataOutputStream 等实现了具体的增强功能。

装饰器模式和代理模式的对比

学习装饰器和代理模式的实现,我发现这两者的作用,实现方式好像是一样的, 这两者有什么区别呢?能不能相互取代呢?

为了更好地比较装饰器模式和代理模式,我们可以从多个维度进行对比。下面是一个表格,展示了这两种模式在不同方面的特点和区别。

特征/模式

装饰器模式 (Decorator)

代理模式 (Proxy)

目的

动态地给对象添加新的功能或责任。

控制对某个对象的访问,并提供一个替代对象。

适用场景

当需要扩展一个类的功能或为一组对象添加行为时。

当直接访问某个对象会给客户端带来不便时。

核心思想

通过组合而不是继承来扩展功能。

通过代理对象来间接访问目标对象。

类结构

- 抽象组件 (Component) - 具体组件 (ConcreteComponent) - 装饰器 (Decorator) - 具体装饰器 (ConcreteDecorator)

- 接口 (Subject) - 真实主题 (RealSubject) - 代理 (Proxy)

动态性

动态地添加或移除功能。

通常是静态的配置,但在某些实现中也可以是动态的。

优缺点

- 优点 1. 灵活性高。 2. 遵循开放封闭原则。 3. 减少继承层次。 - 缺点 1. 对象数量增加。 2. 可能增加复杂性。

- 优点 1. 控制对真实对象的访问。 2. 提供额外功能(如缓存、日志等)。 3. 保护目标对象。 - 缺点 1. 增加了代理类的开销。 2. 可能增加系统复杂性。

本质区别和联系

  • 本质区别:
    • 装饰器模式主要用于在运行时动态地给对象添加新的功能或责任。
    • 代理模式主要用于控制对某个对象的访问,并提供一个替代对象,可以用于实现缓存、日志等功能。
  • 联系:
    • 两者都涉及到接口或抽象类的使用。
    • 两者都使用了组合的方式,而不是继承来实现扩展。
    • 两者都可以用于在不改变原有对象的基础上,增加新的行为或功能。

什么叫动态性?

装饰器模式时重要的两条:

1、组合优于继承,可以用来解决多层次继承和类爆炸问题;

2、与代理模式相比,装饰器模式可以实现动态的增加额外的行为。

在接触到装饰器模式时我很疑惑什么叫动态的,后来我想明白了,我长期是在完成功能代码的编写,代码也是只给我自己一个人使用,所以我既是类设计者又是使用者。我在开发时只满足于当前功能实现,并没有考虑到提供可扩展、灵活性的代码给人使用。所以要真正理解动态性我们应该把视角从类库使用者转换成类库设计者的角度:

类库设计者的目标

  • 灵活性:设计者希望提供一个灵活的框架,使得使用者可以根据需要动态地添加功能,而不需要修改现有的类或接口。
  • 可扩展性:设计者希望提供一个易于扩展的架构,使得未来的功能扩展变得简单和方便。
  • 代码复用:设计者希望通过装饰器模式实现代码复用,避免重复编写相同的功能。

好的设计应该让使用者:

  • 按需添加功能:使用者可以根据实际需求动态地添加功能,而不是在一开始就确定所有的功能。
  • 灵活配置:使用者可以在运行时根据配置或需求的变化动态地调整功能,例如,可以根据配置参数决定是否启用缓冲功能。
  • 易于维护:使用者可以通过装饰器模式轻松地修改或添加功能,而不需要修改原有的代码。

让我们再看一下使用IO类缓冲数据读写一个文件的使用示例:

import java.io.*;

public class DecoratorExample {
    public static void main(String[] args) {
        try (FileInputStream fis = new FileInputStream("example.txt")) {
            // 添加缓冲功能
            InputStream bufferedInput = new BufferedInputStream(fis);
            // 添加数据读写功能
            DataInputStream dataInput = new DataInputStream(bufferedInput);

            int intValue = dataInput.readInt();
            System.out.println("Read integer: " + intValue);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

我们作为类库使用者,可以方便的组合缓冲、数据读写等功能,而不需要改变类库设计者的框架。这是不是一个精巧的设计?这下我讲清楚什么是装饰器模式的“动态性”嘛?

相关推荐

Java框架 —— Spring简介

简介一般来说,Spring指的是SpringFramework,它提供了很多功能,例如:控制反转(IOC)、依赖注入(DI)、切面编程(AOP)、事务管理(TX)主要jar包org.sprin...

Monkey自动化测试

Monkey1.通过Monkey程序模拟用户触摸屏幕、滑动Trackball、按键等操作来对设备上的程序进行压力测试,检测程序多久的时间会发生异常;2.Monkey主要用于Android的压力...

十年之重修SpringBoot启动&amp;自动装载&amp;Bean加载过程

总结Springboot的自动装载,完全是依赖Bean的自动注册,其中默认的规则,是把需要自动装载的bean全名称编辑在spring.factories(2.7之后的版本,还支持.imports文件)...

一些可以显著提高大型 Java 项目启动速度的尝试

我们线上的业务jar包基本上普遍比较庞大,动不动一个jar包上百M,启动时间在分钟级,拖慢了我们在故障时快速扩容的响应。于是做了一些分析,看看Java程序启动慢到底慢在哪里,如何去优化,...

class 增量发包改造为 jar 包方式发布

大纲class增量发包介绍项目目录结构介绍jar包方式发布落地方案class增量发包介绍当前项目的迭代修复都是通过class增量包来发版本的将改动的代码class增量打包,如下图cla...

Flink架构及其工作原理(很详细)

原文链接:https://www.cnblogs.com/code2one/p/10123112.html关键词:Flink架构、面试杀手锏!更多大数据架构、实战经验,欢迎关注【大数据与机器学习】,...

大促系统优化之应用启动速度优化实践

作者:京东零售宋维飞一、前言本文记录了在大促前针对SpringBoot应用启动速度过慢而采取的优化方案,主要介绍了如何定位启动速度慢的阻塞点,以及如何解决这些问题。希望可以帮助大家了解如何定位该类问...

Maven工程如何使用非Maven仓库jar包

使用Maven之前,一直都是自己手工在网上搜索需要的jar包,然后添加到工程中。以这样的方式开发,工作了好多年,曾经以为以后也会一直这样下去。直到碰上Maven,用了第一次,就抛弃老方法了。Maven...

【推荐】一款开源免费、功能强大的短链接生成平台

项目介绍reduce是一款开源免费、功能强大的短链接生成平台。部署在服务器,使用短域名解析即可提供服务。CoodyFramework首秀,自写IOC、MVC、ORM、TASK、JSON、DB连接池、...

K8S官方java客户端之七:patch操作

欢迎访问我的GitHubhttps://github.com/zq2599/blog_demos内容:所有原创文章分类汇总及配套源码,涉及Java、Docker、Kubernetes、DevOPS等;...

Java 的业务逻辑验证框架 之-fluent-validator

开发人员在维护核心业务逻辑的同时,还需要为输入做严格的校验。当输入不合法时,能够给caller一个明确的反馈,最常见的反馈就是返回封装了result的对象或者抛出exception。一些常见...

互联网大厂后端必看!手把手教你替换 Spring Boot 中的日志框架

在互联网大厂的后端开发工作中,SpringBoot框架是搭建项目的“得力助手”,使用十分普遍。但不少开发者都遇到过这样的困扰:SpringBoot默认集成的Logback日志框架,在实际...

测试经理教你如何用monkey进行压力测试!

一、monkey是什么1、monkey程序由android系统自带,使用Java语言写成,在Android文件系统中的存放路径是:/system/framework/monkey.jar2、Mo...

Java-Maven详解

一、什么是Maven?ApacheMaven是一个软件项目管理的综合工具。基于项目对象模型(POM)的概念,提供了帮助管理构建、文档、报告、依赖、发布等方法,Maven简化和标准化项目建设过程。处理...

SpringBoot打包部署最佳实践

springboot介绍SpringBoot目前流行的javaweb应用开发框架,相比传统的spring开发,springboot极大简化了配置,并且遵守约定优于配置的原则即使0配置也能正常运...

取消回复欢迎 发表评论: