【设计模式】使用装饰器模式对价格计算功能做灵活的拓展

文章目录

  • 1. 概述
  • 2.实现方式
    • 2.1.原始商品类及计算接口
    • 2.2.加入装饰器
    • 2.3.装饰器的组合使用
  • 3.总结

1. 概述

装饰器模式(Decorator Pattern)是一种结构型的设计模式,使用组合的方式来替代了继承,它的核心作用是在不修改对象本身的基础上动态地给一个对象添加新的职责或功能,装饰器类一般与被装饰的类有同一个父类(或父接口),其后缀名一般Decorator或者Wrapper
讲到这里,如果熟悉设计模式的同学肯定能发现,这个定义与上一篇设计模式的文章《代理模式的实现方式与使用场景》中提到了代理模式是高度类似的,事实上也确实如此,都在一定程度上对原始对象的行为进行某种形式的包裹或扩展。

那么,装饰模式和代理模式有什么区别呢?
它们在功能实现上有很多地方都可以互换,两者只是在应用场景上划分了不同的区域:

  • 代理模式:适合于做与业务流程无关的增强,例如对象的访问控制、接口鉴权、RPC远程访问等
  • 装饰模式:适合于对业务功能动态添加额外的职责,这些职责和业务功能流程是紧密关联的。

也就是说,判断选择两种模式的使用,只需要看这次的拓展与业务流程是否有关就可以了,接下来看一下装饰模式的实现方式。

2.实现方式

在这里插入图片描述
先用类图来进行说明,在原来的功能接口(或功能抽象类)和功能实现类的基础上,定义了一个装饰器的抽象类,装饰器和功能类实现同一个接口,并且通过组合的方式依赖顶层的接口。如果对类图不熟悉的同学,可以参考《类图(Class Diagram)》。

这么做了之后,装饰器相对于原有的功能接口既是is-a的关系,也是has-a的关系,不管装饰器有多少个、包装了多少层,它始终都是原有功能接口的子类,并且在每一层中都会持有对上一层装饰器(或原有功能对象)的引用。


下面通过一个简单Demo场景来体验一下装饰器模式,如何在不改变原有的接口和实现类的情况下,通过装饰器动态的添加新的功能。

假如现在有一个商城服务,里面有一个计算购物车中商品的价格的功能,有这么几个需求:

  • 可以通过用户的优惠券做额度抵扣
  • 如果用户是会员,可以打九折
  • 如果不满足上面两种情景,则需要原价支付

按照需求,我们可以首先创建一个计算价格的接口,然后新增一个装饰器的抽象用于修改最终价格,最后通过两个不同的装饰器实例(优惠券、打折)来实现价格的重新计算。

2.1.原始商品类及计算接口

  • 商品类
    	@Data
    	public class Product {
    	    /**
    	     * 商家价格(单位:分)
    	     */
    	    private Long price;
    	    /**
    	     * 商品名称
    	     */
    	    private String name;
    	}
    
  • 价格计算接口及其实现
    public interface PriceCalculate {
        Long calculatePrice(List<Product> products);
    }
    
    public class PriceCalculateImpl implements PriceCalculate {
        @Override
        public Long calculatePrice(List<Product> products) {
            return products.stream().mapToLong(Product::getPrice).sum();
        }
    }
    

2.2.加入装饰器

  • 抽象的价格计算装饰器
    通过组合的方式依赖价格计算的接口,再通过模板方法将价格的折扣与优惠券抵扣交给各自的实现类去实现。
    public abstract class PriceCalculateDecorator implements PriceCalculate {
    
        protected PriceCalculate priceCalculate;
    
        public PriceCalculateDecorator(PriceCalculate priceCalculate) {
            this.priceCalculate = priceCalculate;
        }
    
        @Override
        public Long calculatePrice(List<Product> products) {
            return this.discountPrice(products);
        }
    
        public abstract Long discountPrice(List<Product> products);
        
    }
    
  • 会员折扣装饰器
    这里做了简单的处理,直接写死折扣,实际的场景中应该从会员折扣配置中获取到折扣的值,并且应该通过Bigdecimal对金额做计算。
/**
 * 会员折扣装饰器
 */
public class PriceVipDiscountDecorator extends PriceCalculateDecorator {

    public PriceVipDiscountDecorator(PriceCalculate priceCalculate) {
        super(priceCalculate);
    }

    @Override
    public Long discountPrice(List<Product> products) {
        Long originPrice = priceCalculate.calculatePrice(products);
        // 会员九折(此处为简化,实际应该从会员服务获取会员折扣)
        return originPrice * 9 / 10;
    }
}
  • 优惠券抵扣装饰器
    和上面的会员抵扣一样,做简化处理
    /**
     * 优惠券抵扣装饰器
     */
    public class PriceCouponDeductionDecorator extends PriceCalculateDecorator {
    
        public PriceCouponDeductionDecorator(PriceCalculate priceCalculate) {
            super(priceCalculate);
        }
    
        @Override
        public Long discountPrice(List<Product> products) {
            Long originPrice = priceCalculate.calculatePrice(products);
            // 优惠券抵扣(此处为简化,实际应该从优惠券服务获取优惠券抵扣金额)
            return Math.max(originPrice - 5000, 0);
        }
    }
    

最后,写一个测试来验证一下,在这个测试中,分别用VIP折扣和优惠券来做抵扣。

public void testWrapper() {
        List<Product> products = getProduct();

        PriceCalculate priceCalculate = new PriceCalculateImpl();
        Long price = priceCalculate.calculatePrice(products);
        System.out.println("原价:" + price + "分");

        // 会员折扣
        PriceCalculate vipDiscountDecorator = new PriceVipDiscountDecorator(priceCalculate);
        Long vipPrice = vipDiscountDecorator.calculatePrice(products);
        System.out.println("会员折扣后,价格为:" + vipPrice + "分");

        // 优惠券抵扣
        PriceCalculate couponDiscountDecorator = new PriceCouponDeductionDecorator(priceCalculate);
        Long couponPrice = couponDiscountDecorator.calculatePrice(products);
        System.out.println("优惠券抵扣后,价格为:" + couponPrice + "分");
    }

    private List<Product> getProduct() {
        List<Product> products = new ArrayList<>();
        // 创建商品
        Product product = new Product();
        product.setName("商品1");
        product.setPrice(100L * 100);
        products.add(product);

        Product product2 = new Product();
        product2.setName("商品2");
        product2.setPrice(200L * 100);
        products.add(product2);
        return products;
    }

在这里插入图片描述

2.3.装饰器的组合使用

除了上面的分别计算以外,两个装饰器实例还可以组合起来使用,例如现在有个用户既是VIP会有,又有50块的优惠券,按照先打折再优惠券抵扣的规则,可以将VIP装饰器实例通过构造方法直接传入到优惠券装饰器中,修改如下:

        List<Product> products = getProduct();

        PriceCalculate priceCalculate = new PriceCalculateImpl();
        Long price = priceCalculate.calculatePrice(products);
        System.out.println("原价:" + price + "分");

        // 先打折,再优惠券抵扣
        PriceCalculate vipDiscountDecorator = new PriceVipDiscountDecorator(priceCalculate);
        PriceCouponDeductionDecorator priceCouponDeductionDecorator = new PriceCouponDeductionDecorator(vipDiscountDecorator);
        Long discountPrice = priceCouponDeductionDecorator.calculatePrice(products);
        System.out.println("折扣价:" + discountPrice + "分");

在这里插入图片描述
如果需求是先抵扣优惠券再打折的话,只需要调整一下装饰器的包装顺序就可以了,如果还有其他的活动折扣,满减之类的对价格计算的调整,再跟进具体的需求新增装饰器并包装起来即可。

3.总结

装饰器的作用是在不改变目标类的情况下,动态的添加功能或职责,上述的功能使用继承也可以实现,但是使用继承之后,一旦修改了父类的方法所有子类都会受到影响,而使用装饰器的方式,目标对象与装饰器对象并不会互相影响。

另外,需要注意到的是装饰模式和代理模式非常相似,从功能上来讲,一个可以实现的功能使用另一个也可以实现,我们在使用的过程中只需要注意一点,即:代理模式用于实现非业务的功能增强,装饰器模式往往用于对业务功能的增强

最后,再谈一下装饰器的缺点,包装的层数过多的时候会给我们的问题排查带来一定的困难,需要一层一层的往里面剥才有可能找到是哪个装饰器出现的问题,所以我们在使用的时候尽可能的减少装饰器包装的层数,两到三层即可。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

值得收藏的的适用于 Windows 11 的免费数据恢复软件榜单

终于要说到Windows 11了&#xff0c;有太多令人惊叹的功能&#xff0c;让人跃跃欲试。但是&#xff0c;在升级到 Windows 11 或使用 Windows 11 时&#xff0c;人们可能会因计算机问题而导致文件被删除或丢失。这就是为什么需要 Windows 11 的免费文件恢复的原因。这是适用于 W…

一周学会Django5 Python Web开发-项目配置settings.py文件-其他配置

锋哥原创的Python Web开发 Django5视频教程&#xff1a; 2024版 Django5 Python web开发 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili2024版 Django5 Python web开发 视频教程(无废话版) 玩命更新中~共计17条视频&#xff0c;包括&#xff1a;2024版 Django5 Python we…

C++ //练习 7.27 给你自己的Screen类添加move、set和display函数,通过执行下面的代码检验你的类是否正确。

C Primer&#xff08;第5版&#xff09; 练习 7.27 练习 7.27 给你自己的Screen类添加move、set和display函数&#xff0c;通过执行下面的代码检验你的类是否正确。 Screen myScreen(5, 5, X); myScreen.move(4, 0).set(#).display(cout); cout<<"\n"; myScr…

开源C语言库Melon:Cron格式解析

本文介绍开源C语言库Melon的cron格式解析。 关于 Melon 库&#xff0c;这是一个开源的 C 语言库&#xff0c;它具有&#xff1a;开箱即用、无第三方依赖、安装部署简单、中英文文档齐全等优势。 Github repo 简介 cron也就是我们常说的Crontab中的时间格式&#xff0c;格式如…

Debezium发布历史136

原文地址&#xff1a; https://debezium.io/blog/2023/01/06/change-data-capture-with-questdb-and-debezium/ 欢迎关注留言&#xff0c;我是收集整理小能手&#xff0c;工具翻译&#xff0c;仅供参考&#xff0c;笔芯笔芯. Change Data Capture with QuestDB and Debezium …

【NI-DAQmx入门】处理数据采集和测试系统中噪声的几种主要方法

在实际的测试系统中测量模拟信号并不总是像将信号源连接到测量设备那么简单。数据完整性取决于被控制和监视的电气设备发送和接收的干净的电信号。 电噪声可能会掩盖电信号并使其无法识别&#xff0c;从而损害原本具备功能的 DAQ 系统。数据采集​​是关键任务应用测试系统的一…

深入浅出了解谷歌「Gemini大模型」发展历程

Google在2023年12月官宣了Gemini模型&#xff0c;随后2024年2月9日才宣布Gemini 1.0 Ultra正式对公众服务&#xff0c;并且开始收费。现在2024年2月14日就宣布了Gemini 1.5 Pro&#xff0c;史诗级多模态最强MoE首破100万极限上下文纪录&#xff01;&#xff01;&#xff01;Gem…

语音唤醒——

文章目录 配置主代码 参考文档&#xff1a;https://picovoice.ai/docs/quick-start/porcupine-python/ 配置 pip install pvporcupine主代码 ACCESS_KEY&#xff1a;需要将该参数填入即可 # # Copyright 2018-2023 Picovoice Inc. # # You may not use this file except in …

如何修复Microsoft Edge不能以全屏模式打开​?这里提供几个故障排除方法

随着越来越多的Windows 10用户将Edge设置为默认浏览器&#xff0c;各种错误和小故障层出不穷。例如&#xff0c;许多用户抱怨他们无法在全屏模式下启动Edge。如果你正在寻找解决方案来解决这个恼人的问题&#xff0c;请按照下面的故障排除步骤进行操作。 修复Microsoft Edge不…

内容检索(2024.02.17)

随着创作数量的增加&#xff0c;博客文章所涉及的内容越来越庞杂&#xff0c;为了更为方便地阅读&#xff0c;后续更新发布的文章将陆续在此汇总并附上原文链接&#xff0c;感兴趣的小伙伴们可持续关注文章发布动态&#xff01; 本期更新内容&#xff1a; 1. 信号完整性理论与…

JavaWeb:关于登录认证的简单拓展

前提介绍 本文基于文章-------JavaWeb&#xff1a;SpringBootWeb登录认证 --黑马笔记 -------再做简单拓展 如果没有关于登录认证知识的基础&#xff0c;可以先看上面所说的的文章&#xff0c;文章在专栏javaweb中&#xff0c;下面我为了大家观看&#xff0c;直接放了链接。…

代码随想录刷题笔记 DAY 28 | 复原 IP 地址 No.93 | 子集 No.78 | 子集 II No.90

文章目录 Day 2801. 复原 IP 地址&#xff08;No. 93&#xff09;1.1 题目1.2 笔记1.3 代码 02. 子集&#xff08;No. 78&#xff09;2.1 题目2.2 笔记2.3 代码 03. 子集 II&#xff08;No. 90&#xff09;3.1 题目3.2 笔记3.3 代码 Day 28 01. 复原 IP 地址&#xff08;No. 9…

《读者》2023-18:定力决定你能走多远

定力决定你能走多远 - 董宇辉 我苦练英语很长时间之后&#xff0c;有一次上口语课&#xff0c;老师让我回答问题。 我回答完&#xff0c;老师说&#xff0c;没想到你的口语还挺好的。 我突然感觉自己的付出被看见了&#xff0c;虽然它小到不值一提。 请你记住&#xff0c;很多小…

2024年华为OD机试真题-多段线数据压缩-Java-OD统一考试(C卷)

题目描述: 下图中,每个方块代表一个像素,每个像素用其行号和列号表示。 为简化处理,多段线的走向只能是水平、竖直、斜向45度。 上图中的多段线可以用下面的坐标串表示:(2, 8), (3, 7), (3, 6), (3, 5), (4, 4), (5, 3), (6, 2), (7, 3), (8, 4), (7, 5)。 但可以发现,这…

C++智能指针的冷知识!

个人主页&#xff1a;PingdiGuo_guo 收录专栏&#xff1a;C干货专栏 大家好呀&#xff0c;我是PingdiGuo_guo&#xff0c;今天我们来学习一下智能指针。 文章目录 1.智能指针的概念 2.智能指针的思想 3.智能指针的作用 3.1 自动内存管理 3.2 共享所有权 3.3 避免悬挂指针…

PyTorch使用Tricks:学习率衰减 !!

文章目录 前言 1、指数衰减 2、固定步长衰减 3、多步长衰减 4、余弦退火衰减 5、自适应学习率衰减 6、自定义函数实现学习率调整&#xff1a;不同层不同的学习率 前言 在训练神经网络时&#xff0c;如果学习率过大&#xff0c;优化算法可能会在最优解附近震荡而无法收敛&#x…

PowerPoint安装IguanaTex插件

1 前提 电脑已经配置好Latex环境 2 安装过程 2.1 下载IguanaTex_v1_56插件 官网下载地址 下载的文件格式为&#xff1a;IguanaTex v1.56 (.ppam) .ppam 2.2 移动插件 将IguanaTex v1.56 .ppam移动到C:\Users\ 你的用户名\AppData\Roaming\Microsoft\AddIns目录下。 2.3 …

【初始消息队列】消息队列的各种类型

消息队列相关概念 什么是消息队列 MQ(message queue)&#xff0c;从字面意思上看&#xff0c;本质是个队列&#xff0c;FIFO 先入先出&#xff0c;只不过队列中存放的内容是 message 而已&#xff0c;还是一种跨进程的通信机制&#xff0c;用于上下游传递消息。在互联网架构中…

[晓理紫]每日论文分享(有中文摘要,源码或项目地址)--强化学习、机器人等

专属领域论文订阅 VX关注{晓理紫}&#xff0c;每日更新论文&#xff0c;如感兴趣&#xff0c;请转发给有需要的同学&#xff0c;谢谢支持 如果你感觉对你有所帮助&#xff0c;请关注我&#xff0c;每日准时为你推送最新论文。 为了答谢各位网友的支持&#xff0c;从今日起免费为…

SQL-Labs靶场“11-15”关通关教程

君衍. 一、十一关 基于POST单引号字符型注入1、源码分析2、联合查询注入3、报错注入 二、十二关 基于POST双引号字符型注入1、源码分析2、联合查询注入3、报错注入 三、十三关 基于POST单引号报错注入变形1、源码分析2、报错注入 四、十四关 基于POST双引号报错注入1、源码分析…