【设计模式】装饰器模式

news2024/11/27 5:30:48

装饰器模式

以生活中的场景来举例,一个蛋糕胚,给它涂上奶油就变成了奶油蛋糕,再加上巧克力和草莓,它就变成了巧克力草莓蛋糕。

像这样在不改变原有对象的基础之上,将功能附加到原始对象上的设计模式就称为装饰模式(Decorator模式),属于结构型模式。

装饰器模式中主要有四个角色:
Component : 定义被装饰对象的接口,装饰器也需要实现一样的接口
ConcreteComponent: 具体的装饰对象,实现了Component接口,通常就是被装饰的原始对象
Decorator: 所有装饰器的抽象父类,需要定义与Component一致的接口,并且持有一个被装饰的Component对象
ConcreteDecorator: 具体装饰器对象
​​​​​​​​
在这里插入图片描述

代码示例

假设我们需要设计一个奖金的系统,目前一个工作人员的奖金包含三部分:

本月销售额的奖金 : 当月销售额的3%

累计销售额的奖金 : 累计销售额的0.1%

团队销售额的奖金 : 团队销售额的1%,只有经理才有

使用装饰模式的实现如下 : 定义一个所有奖金的抽象接口,然后定义一个BasicPrize的初始奖金对象,不同的人奖金的组成不同,就为其添加不同的装饰器


public abstract class Component {

    abstract double calPrize(String userName);
}

public class BasicPrize extends Component {

    //保存了一个员工与销售额的对应关系的map
    public static Map<String,Double> saleMoney = new HashMap<String,Double>();

    static {
        saleMoney.put("小明",9000.0);
        saleMoney.put("小陈",20000.0);
        saleMoney.put("小王",30000.0);
        saleMoney.put("张经理",55000.0);
    }

    public double calPrize(String userName) {
        return 0;
    }
}

/**
 *
 * 所有装饰器的抽象父类,持有一个被装饰对象
 */
public abstract class Decorator extends Component {
    protected Component component;

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

    public abstract double calPrize(String userName);
}

/**
 * 
 * 当月奖金计算规则
 */
public class MonthPrizeDecorator extends  Decorator{

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

    public double calPrize(String userName)  {
        //先计算被装饰对象的奖金(就是在加上本奖金之前的奖金)
        double money = this.component.calPrize(userName);
        //计算本奖金  对应员工的业务额的3%
        double prize = BasicPrize.saleMoney.get(userName)*0.03;
        System.out.println(userName+"当月 业务奖金:"+prize);
        return money + prize;
    }

}
/**
 * 累计奖金
 */
public class SumPrizeDecorator extends Decorator {
    public SumPrizeDecorator(Component component) {
        super(component);
    }


    public double calPrize(String userName)  {
        //先计算被装饰对象的奖金(就是在加上本奖金之前的奖金)
        double money = this.component.calPrize(userName);
        //计算本奖金  累计业务额的0.1% 假设是100000
        double prize = 100000*0.001;
        System.out.println(userName+"当月 累计奖金:"+prize);
        return money + prize;
    }

}
/**
 * 团队奖金
 */
public class GroupPrizeDecorator extends Decorator {
    public GroupPrizeDecorator(Component component) {
        super(component);
    }

    public double calPrize(String userName)  {
        //先计算被装饰对象的奖金(就是在加上本奖金之前的奖金)
        double money = this.component.calPrize(userName);
        //计算本奖金  本团队的业务额的1%
        double sumSale = 0;
        for(double sale: BasicPrize.saleMoney.values()){
            sumSale += sale;
        }
        double prize = sumSale*0.01;
        System.out.println(userName+"当月 团队奖金:"+prize);
        return money + prize;
    }


}
/**
 * @Description
 */
public class Client {
    public static void main(String[] args) throws FileNotFoundException {
        //基础对象 奖金0
        Component basePrice = new BasicPrize();
        //当月奖金
        Component salePrize = new MonthPrizeDecorator(basePrice);
        //累计奖金
        Component sumPrize = new SumPrizeDecorator(salePrize);
        //团队奖金
        Component groupPrize = new GroupPrizeDecorator(sumPrize);
 
        //普通员工的奖金由两部分组成
        double d1 = sumPrize.calPrize("小明");
        System.out.println("========================小明总奖金:"+d1);
        double d2 = sumPrize.calPrize("小陈");
        System.out.println("========================小陈总奖金:"+d2);
        double d3 = sumPrize.calPrize("小王");
        System.out.println("========================小王总奖金:"+d3);
        //王经理的奖金由三部分组成
        double d4 = groupPrize.calPrize("张经理");
        System.out.println("========================张经理总奖金:"+d4);

       
    }
}

输出结果如下:
​​在这里插入图片描述
这里我们使用装饰器模式,主要是为了方便组合和复用。在一个继承的体系中,子类往往是互斥的,比方在一个奶茶店,它会有丝袜奶茶,红茶,果茶等,用户想要一杯饮料,一般都会在这些种类中选一种,不能一杯饮料既是果茶又是奶茶。然后用户可以根据自己的喜好添加任何想要的decorators,珍珠,椰果,布丁等,这些添加物对所有茶类饮品都是相互兼容的,并且是可以被允许反复添加的(同样的装饰器是否允许在同一个对象上装饰多次,视情况而定,像上面的奖金场景显然是不被允许的)

jdk中的装饰器模式

java.io包是用于输入输出的包,这里使用了大量的装饰器模式,我们再来体会一下装饰器模式的优点。

下图是jdk 输出流的一部分类图,很明显是一个装饰模式的类图,OutputStream是顶层父类,FileOutputStream和ObjectOutputStream是具体的被装饰类,FilterOutputStream是所有输出流装饰器的抽象父类
在这里插入图片描述
使用的代码如下:

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

为什么Java IO设计的时候要使用装饰器模式, 而J不设计⼀个继承 FileInputStream 并且⽀持缓存的 BufferedFileInputStream 类呢?

如果InputStream 只有⼀个⼦类 FileInputStream 的话,那我们在 FileInputStream 基础之上,再设计⼀个孙⼦类BufferedFileInputStream,也是可以接受的。但实际上,继承 InputStream 的⼦类有很多。我们需要给每⼀个 InputStream 的⼦类,
再继续派⽣⽀持缓存读取的⼦类。
除此之外,我们还需要对功能进⾏其他⽅⾯的增强,⽐如下⾯的DataInputStream 类,⽀持按照基本数据类型(int、boolean、long 等)来读取数据。这种情形下,使用继承的方式的话类的继承结构变得⽆⽐复杂,代码维护起来也比较费劲。

按照装饰器模式的结构,我们可以继承FilterOutputStream实现自定义的装饰器,并且在使用的时候可以和jdk自带的装饰器对象任意组合。

我们可以实现一个简单的复制输出内容的OutputStream装饰器

public class DuplicateOutputStream2 extends FilterOutputStream {
    /**
     * Creates an output stream filter built on top of the specified
     * underlying output stream.
     *
     * @param out the underlying output stream to be assigned to
     *            the field <tt>this.out</tt> for later use, or
     *            <code>null</code> if this instance is to be
     *            created without an underlying stream.
     */
    public DuplicateOutputStream2(OutputStream out) {
        super(out);
    }

    //将所有的内容复制一份输出 ab 变成aabb
    public void write(int b) throws IOException {
        super.write(b);
        super.write(b);
    }
}

装饰器类是否可以直接实现Component父类?

以输出流为例,如果直接继承OutputStream来实现自定义装饰器

public class DuplicateOutputStream extends OutputStream {
 
    private OutputStream os;
 
    public DuplicateOutputStream(OutputStream os){
        this.os = os;
    }
 
    //将所有的内容复制一份输出 ab 变成aabb
    public void write(int b) throws IOException {
        os.write(b);
        os.write(b);
    }
}
public class ClientTest {
    public static void main(String[] args) throws IOException {
 
        DataOutputStream dataOutputStream = new DataOutputStream(new BufferedOutputStream(new DuplicateOutputStream2(new FileOutputStream("1.txt"))));
        testOutputStream(dataOutputStream);
 
        DataOutputStream dataOutputStream1 = new DataOutputStream(new BufferedOutputStream(new DuplicateOutputStream(new FileOutputStream("1.txt"))));
        testOutputStream(dataOutputStream1);
 
    }
 
    public static void testOutputStream(DataOutputStream dataOutputStream) throws IOException {
        DataInputStream dataInputStream = new DataInputStream(new FileInputStream("1.txt"));
        dataOutputStream.write("bdsaq".getBytes());
        dataOutputStream.close();
        System.out.println(dataInputStream.available());
        byte[] bytes3 = new byte[dataInputStream.available()];
        dataInputStream.read(bytes3);
        System.out.println("文件内容:"+new String(bytes3));
    }
}

输出结果:
在这里插入图片描述
乍一看好像没什么区别,但是如果把BufferedOutputStream装饰器和自定义的装饰器互换。

DataOutputStream dataOutputStream2 = new DataOutputStream(new DuplicateOutputStream2(new BufferedOutputStream(new FileOutputStream("1.txt"))));
testOutputStream(dataOutputStream2);

DataOutputStream dataOutputStream3 = new DataOutputStream(new DuplicateOutputStream(new BufferedOutputStream(new FileOutputStream("1.txt"))));
testOutputStream(dataOutputStream3);

输出结果:
在这里插入图片描述
使用了实现OutputStream的DuplicateOutputStream会出现没有正常输出数据,这是因为我们使用了BufferedOutputStream这个带缓存区的输出流,缓存区的输出流在缓存区没有满的情形下是不会进行输出操作的。一般情形下我们在调用jdk的DataOutputStream的close方法的时候会调用其传入的输出流的flush()方法,并且向下传递调用,BufferedOutputStream里的数据会正常输出。

在使用DuplicateOutputStream2的时候其调用关系是这样的:

dataOutputStream.close()–>duplicateOutputStream2.flush()–>bufferedOutputStream.flush()

使用DuplicateOutputStream的时候由于DuplicateOutputStream继承的OutputStream的flush()方法是空实现,所以不会继续往下调用bufferedOutputStream的flush()方法,故而最后没有得到输出内容

所以装饰器类需要继承Decorator抽象父类,而不是直接继承Component抽象类,我认为是为了在Decorator里实现一些共性的代码,以便在使用装饰器的时候能够更加自由,无视其组合顺序 (当然如果你的Decorator里没有任何逻辑代码,在合适的场景下你可以不定义抽象装饰器类)

总结

装饰器模式主要用于解决继承关系过于复杂的问题,通过组合来替代继承。
它主要的作⽤是给原始类添加增强功能。

除此之外,装饰器模式还有⼀个特点,那就是可以对原始类嵌套使⽤多个装饰器。为了满⾜这个应⽤场景,在设计的时候,装饰器类需要跟原始类继承相同的抽象类或者接⼝。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/402752.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何查看磁盘空间并挂载磁盘

df -h内容参数含义Filesystem文件系统Size分区大小1k-blocks单位是1KB(使用df查看)Used已用容量Avail还可用的容量Use%已用百分比Mounted on挂载点du -h查看某目录下占用空间最多的文件或目录。取前10个。需要先进入该目录下。du -cks * | sort -rn | head -n 10参数含义-s对每…

腾讯游戏,“迷失”自己

【潮汐商业评论/原创】“那个号我忘记密码了&#xff0c;你等我换个新号跟你玩”。这是Lynn《王者荣耀》双排队友常说的话。因为未成年&#xff0c;账号只有周末能玩&#xff0c;而且只有两小时。所以Lynn的这位网友&#xff0c;经常用家长的手机号注册游戏账号&#xff0c;但是…

Yarn调度器和调度算法

目录 1 先进先出调度器&#xff08;FIFO&#xff09; 2 容量调度器&#xff08;Capacity Scheduler&#xff09; 3 公平调度器&#xff08;Fair Scheduler&#xff09; 缺额&#xff1a; 公平调度器队列资源分配方式 公平调度器资源分配算法 Hadoop作业调度器主要有三种&…

分库分表原理

一、数据库瓶颈 会导致数据库的活跃连接数增加&#xff0c;进而逼近甚至达到数据库可承载活跃连接数的阈值。在业务Service来看就是&#xff0c;可用数据库连接少甚至无连接可用。接下来就可以想象了吧&#xff08;并发量、吞吐量、崩溃&#xff09;。 IO瓶颈-分库和垂直分表…

探索测试的一些总结

1)探索性测试与脚本化测试的主要区别&#xff1a;1)探索性测试将更多更高的认知水平的工作放在测试执行&#xff0c;而脚本化测试则更关注测试设计;2)前者更强调测试活动的并行和相互反馈(学习、设计、执行与结果分析等)&#xff0c;而后者的测试活动是相对串行的。 2)脚本化测…

Grafana系统的备份、恢复、迁移

Grafana系统的备份、恢复、迁移 1. 备份Grafana相关数据 首先先关闭Grafana服务&#xff08;systemctl stop grafana-server&#xff09;&#xff0c;到目录下备份以下文件或者目录&#xff1a; 备份grafana目录中的grafana.db&#xff08;一般情况下路径&#xff1a;/var/l…

电子技术——数字IC技术,逻辑电路和设计方法

电子技术——数字IC技术&#xff0c;逻辑电路和设计方法 在我们之前的学习中&#xff0c;我们学习了CMOS技术&#xff0c;然而CMOS技术并不是唯一的数字逻辑技术&#xff0c;因此&#xff0c;本节系统的介绍当今使用的数字技术和逻辑电路族。 数字IC技术和逻辑电路族 逻辑电…

k8s--services(微服务)

文章目录一、k8s网络通信service和iptables的关系二、services1.简介2.默认3.IPVS模式的service4.clusterip5.headless6.从外部访问service的三种方式&#xff08;1&#xff09;nodeport&#xff08;2&#xff09;loadbalancer7.metallb一、k8s网络通信 k8s通过CNI接口接入其他…

tf_nndistancen 安装

为了评估MMD&#xff0c;断断续续装了4天&#xff0c;踩了几乎所有的坑&#xff0c;终于装上了QAQ 1. 库链接&#xff1a;pointnet-autoencoder/tf_ops/nn_distance at master charlesq34/pointnet-autoencoder GitHub 2. 安装TensorFlow&#xff0c;我的环境是cuda 11.5 ,…

【Go】用Go在命令行输出好看的表格

用Go在命令行输出好看的表格前言正文生成Table表头设置插入行表格标题自动标号单元格合并列合并行合并样式设置居中设置数字自动高亮标红完整Demo代码结语前言 最近在写一些运维小工具&#xff0c;比如批量进行ping包的工具&#xff0c;实现不困难&#xff0c;反正就是ping&am…

java(Class 常用方法 获取Class对象六种方式 动态和静态加载 类加载流程)

ClassClass常用方法获取Class对象六种方式哪些类型有Class对象动态和静态加载类加载流程加载阶段连接阶段连接阶段-验证连接阶段-准备连接阶段-解析初始化阶段获取类结构信息Class常用方法 第一步&#xff1a;创建一个实体类 public class Car {public String brand "宝…

mysql数据库innodb存储引擎之事务原理

事务是一组操作的集合&#xff0c;它是一个不可分割的工作单位&#xff0c;事务会把所有的操作作为一个整体一起向系统提交或撤销操作要求&#xff0c;即这些操作要么同时成功&#xff0c;要么同时失败。 四大特性&#xff1a; redo log和undo log&#xff1a;一致性、原子性…

Map和Set总结

Map和Set Map和Set是专门用来进行搜索的数据结构&#xff0c;适合动态查找 模型 搜索的数据称为关键字(key)&#xff0c;关键字对应的叫值(value)&#xff0c;key-value键值对 key模型key-value模型 Map存储的就是key-value模型&#xff0c;Set只存储了key Map Map是接口类…

ES增量同步方案

1 基于业务代码嵌入式的增量同步方式在Java业务代码要修改业务数据的地方&#xff0c;增加调用写入ES数据的方法优点&#xff1a;1、实现方式简单&#xff0c;可控粒度高&#xff1b;2、不依赖第三方数据同步框架&#xff1b;3、数据库不用做特殊配置和部署&#xff1b;缺点&am…

Qt学习_10_纯Ui操作_设置窗口、菜单栏、工具栏、按钮的图标

前言 Qt项目&#xff0c;如果功能少&#xff0c;项目小&#xff0c;用代码来配置图标的方式问题不大&#xff0c;无可厚非。但是一旦项目的功能复杂内容很多&#xff0c;用代码来配置图标的方式就显得很冗余&#xff0c;能在ui设计界面完成的工作&#xff0c;尽量就在ui设计界…

人工智能实验一:使用搜索算法实现罗马尼亚问题的求解

1.任务描述 本关任务&#xff1a; 了解有信息搜索策略的算法思想&#xff1b;能够运用计算机语言实现搜索算法&#xff1b;应用A*搜索算法解决罗马尼亚问题&#xff1b; 2.相关知识 A*搜索 算法介绍 A*算法常用于 二维地图路径规划&#xff0c;算法所采用的启发式搜索可以…

66 - 进程互斥锁的应用示例

---- 整理自狄泰软件唐佐林老师课程 查看所有文章链接&#xff1a;&#xff08;更新中&#xff09;深入浅出操作系统 - 目录 文章目录1. 简单生产消费者问题1.1 具体问题描述1.2 解决方案1.3 简单生产消费者问题模型1.4 编程实验&#xff1a;生产消费者示例2. 多任务读写问题&a…

中国人民大学与加拿大女王大学金融硕士,让这一年有一个骄傲的句号

在中国人民大学与加拿大女王大学金融硕士项目就读的同学&#xff0c;都有一个共同的目标&#xff0c;那就是在就读的这一年能画上一个圆满的句号。当拿到毕业证书的那一刻&#xff0c;所有的付出和努力都是值得的&#xff0c;在这里学习提升各自理论知识与金融服务经验&#xf…

学生信息表

目录 一、功能说明 二、核心思想 三、所用知识回顾 四、基本框架 五、js功能实现部分 一、功能说明 &#xff08;1&#xff09;输入对应的信息&#xff0c;点击录入可以为下面的表格添加一条记录&#xff0c;注意当所填信息不完整时不允许进行提交。 &#xff08;2&…

高校如何通过校企合作/实验室建设来提高大数据人工智能学生就业质量

高校人才培养应该如何结合市场需求进行相关专业设置和就业引导&#xff0c;一直是高校就业工作的讨论热点。亘古不变的原则是&#xff0c;高校设置不能脱离市场需求太远&#xff0c;最佳的结合方式是&#xff0c;高校具有前瞻性&#xff0c;能领先市场一步&#xff0c;培养未来…