Head First设计模式---3.装饰者模式

news2024/11/30 6:46:54

3.1装饰者模式

亦称: 装饰者模式、装饰器模式、Wrapper、Decorator

装饰模式是一种结构型设计模式, 允许你通过将对象放入包含行为的特殊封装对象中来为原对象绑定新的行为。

举个例子:天气很冷,我们一件一件穿衣服,从内到外,穿了很多件衣服。

image-20230221202031891

问题

欢迎来到瑞心咖啡店,本店以迅速的扩张优势占领了附近的几条街,但是随着顾客的增多,我们需要一套更完善和流程的架构去应对咖啡的销售,所以我们需要更新我们的订单系统!

我们原先的类设计是这样的:

image-20230221200142094

  • 每种咖啡不同,价格也不同
  • 消费者可以根据不同的咖啡选择加入不同的小料:摩卡、豆浆等等,或者覆盖奶泡
  • 当然有的咖啡加入了某些小料之后会变得异常难喝

瑞心咖啡会个根据不同的小料收取不同的费用。所以订单系统必须考虑到这些事情。我们继续使用这样的类型,使我们的结构变得异常臃肿:

image-20230221200524193

当每上市一种新的调料或者咖啡,我们就陷入了一种麻烦的循环中去。。。。

解决方案

我们可以使用继承和实例变量来追踪这些调料

我们先从Beverage下手,加上一些实例变量来代表是否加调料,然后加上cost()方法来计算所需要的价格。对不同的咖啡继承对应的Beverage类,其中不包含任何小料,当需要小料时,只需要将小料加入进来即可。那么当然小料也需要一个类去实现。

那么对于我们的设计来说,需要遵循一个设计原则:开闭原则

开闭原则:对扩展开发,对修改关闭

我们可以对类进行扩展,但是就不用更改已经有的实现

所以如果我们有以下需求:

  • 点一杯Espresso(意大利浓咖啡)
  • 加一份摩卡
  • 加一份牛奶

那么根据我们的装饰者模式来说,最后包装的样子是不是这样:

image-20230221202507611

所以当我们是算钱的时候,是不是一层一层的委托给外面,就可以实现了呢?

好了我们已经了解了装饰者模式,让我们看实现的代码:

  • Beverage类,抽象出来,作为咖啡的和调味品的基类,用于算钱和描述
public abstract class Beverage {
    /**
     * 描述
     */
    public String description = "Unknown Beverage";

    /**
     * 返回咖啡描述
     * @return 描述
     */
    public String getDescription(){
        return description;
    }

    /**
     * 购买该描述需要花费的
     */
    public abstract double cost();
}
  • DarkRoast咖啡类
public class DarkRoast extends Beverage {

    public DarkRoast(){
        description = "DarkRoast";
    }

    @Override
    public double cost() {
        return 1.69;
    }
}
  • Espresso咖啡类
public class Espresso extends Beverage {

    /**
     * 咖啡类
     */
    public Espresso(){
        description = "Espresso";
    }
    
    @Override
    public double cost() {
        return 1.99;
    }
}
  • HouseBlend类,和Espresso、DarkRoast一样,不再写

  • CondimentDecorator调味品类,用于描述咖啡所加的内容

public abstract class CondimentDecorator extends Beverage {
    /**
     * 获取描述
     */
    public abstract String getDescription();
}
  • 调味品的实现类:Mocha类
public class Mocha extends CondimentDecorator {

    /**
     * 不同的咖啡作为参数加入进来
     */
    Beverage beverage;

    public Mocha(Beverage beverage){
        this.beverage = beverage;
    }

    /**
     * 计算价格
     * @return 该调味品需要的钱 + coffee需要的钱
     */
    @Override
    public double cost() {
        return 0.20 + beverage.cost();
    }

    @Override
    public String getDescription() {
        return beverage.getDescription() + ",Mocha";
    }
}
  • 其他的调味品类(Soy、Whip)同上

  • 设计好了之后,我们开始调试我们的包装者吧!

public class Main {
    public static void main(String[] args) {
        Beverage beverage = new Espresso();
        System.out.println("Beverage:"+beverage.getDescription());

        //DarkRost咖啡 添加了Mocha * 2 , whip * 1
        Beverage beverage1 = new DarkRoast();
        beverage1 = new Mocha(beverage1);
        beverage1 = new Mocha(beverage1);
        beverage1 = new Whip(beverage1);

        System.out.println("DarkRoast:"+beverage1.getDescription() + "  cost: $" + beverage1.cost());

		
        //HouseBlen咖啡 添加了Soy、Mocha、Whip * 1
        Beverage beverage2 = new HouseBlend();
        beverage2 = new Soy(beverage2);
        beverage2 = new Mocha(beverage2);
        beverage2 = new Whip(beverage2);

        System.out.println("HouseBlend:"+beverage2.getDescription() + "  cost: $" + beverage2.cost());

    }
}

运行结果:

Beverage:Espresso
DarkRoast:DarkRoast,Mocha,Mocha,whip  cost: $2.5999999999999996
HouseBlend:House Blend Coffee,soy,Mocha,whip  cost: $2.02

装饰模式结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gGFdizP6-1676982856417)(null)]

  1. 部件 (Component) 声明封装器和被封装对象的公用接口。
  2. 具体部件 (Concrete Component) 类是被封装对象所属的类。 它定义了基础行为, 但装饰类可以改变这些行为。
  3. 基础装饰 (Base Decorator) 类拥有一个指向被封装对象的引用成员变量。 该变量的类型应当被声明为通用部件接口, 这样它就可以引用具体的部件和装饰。 装饰基类会将所有操作委派给被封装的对象。
  4. 具体装饰类 (Concrete Decorators) 定义了可动态添加到部件的额外行为。 具体装饰类会重写装饰基类的方法, 并在调用父类方法之前或之后进行额外的行为。
  5. 客户端 (Client) 可以使用多层装饰来封装部件, 只要它能使用通用接口与所有对象互动即可。

装饰模式适合应用场景

如果你希望在无需修改代码的情况下即可使用对象, 且希望在运行时为对象新增额外的行为, 可以使用装饰模式。

装饰能将业务逻辑组织为层次结构, 你可为各层创建一个装饰, 在运行时将各种不同逻辑组合成对象。 由于这些对象都遵循通用接口, 客户端代码能以相同的方式使用这些对象。

如果用继承来扩展对象行为的方案难以实现或者根本不可行, 你可以使用该模式。

许多编程语言使用 final最终关键字来限制对某个类的进一步扩展。 复用最终类已有行为的唯一方法是使用装饰模式: 用封装器对其进行封装。

实现方式

  1. 确保业务逻辑可用一个基本组件及多个额外可选层次表示。
  2. 找出基本组件和可选层次的通用方法。 创建一个组件接口并在其中声明这些方法。
  3. 创建一个具体组件类, 并定义其基础行为。
  4. 创建装饰基类, 使用一个成员变量存储指向被封装对象的引用。 该成员变量必须被声明为组件接口类型, 从而能在运行时连接具体组件和装饰。 装饰基类必须将所有工作委派给被封装的对象。
  5. 确保所有类实现组件接口。
  6. 将装饰基类扩展为具体装饰。 具体装饰必须在调用父类方法 (总是委派给被封装对象) 之前或之后执行自身的行为。
  7. 客户端代码负责创建装饰并将其组合成客户端所需的形式。

装饰模式优缺点

优点:

  • 你无需创建新子类即可扩展对象的行为。
  • 你可以在运行时添加或删除对象的功能。
  • 你可以用多个装饰封装对象来组合几种行为。
  • 单一职责原则。 你可以将实现了许多不同行为的一个大类拆分为多个较小的类。

缺点:

  • 在封装器栈中删除特定封装器比较困难。
  • 实现行为不受装饰栈顺序影响的装饰比较困难。
  • 各层的初始化配置代码看上去可能会很糟糕。

与其他模式的关系

  • 适配器模式可以对已有对象的接口进行修改, 装饰模式则能在不改变对象接口的前提下强化对象功能。 此外, 装饰还支持递归组合, 适配器则无法实现。

  • 适配器能为被封装对象提供不同的接口, 代理模式能为对象提供相同的接口, 装饰则能为对象提供加强的接口。

  • 责任链模式和装饰模式的类结构非常相似。 两者都依赖递归组合将需要执行的操作传递给一系列对象。 但是, 两者有几点重要的不同之处。

    责任链的管理者可以相互独立地执行一切操作, 还可以随时停止传递请求。 另一方面, 各种装饰可以在遵循基本接口的情况下扩展对象的行为。 此外, 装饰无法中断请求的传递。

  • 组合模式和装饰的结构图很相似, 因为两者都依赖递归组合来组织无限数量的对象。

    装饰类似于组合, 但其只有一个子组件。 此外还有一个明显不同: 装饰为被封装对象添加了额外的职责, 组合仅对其子节点的结果进行了 “求和”。

    但是, 模式也可以相互合作: 你可以使用装饰来扩展组合树中特定对象的行为。

  • 大量使用组合和装饰的设计通常可从对于原型模式的使用中获益。 你可以通过该模式来复制复杂结构, 而非从零开始重新构造。

  • 装饰可让你更改对象的外表, 策略模式则让你能够改变其本质。

  • 装饰和代理有着相似的结构, 但是其意图却非常不同。 这两个模式的构建都基于组合原则, 也就是说一个对象应该将部分工作委派给另一个对象。 两者之间的不同之处在于代理通常自行管理其服务对象的生命周期, 而装饰的生成则总是由客户端进行控制。

参考:First Head设计模式、设计模式

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

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

相关文章

学习Flask之五、数据库

学习Flask之五、数据库 数据库有组织的存贮应用数据。根据需要应用发布查询追踪特定部分。网络应用最常用的数据库是基于关系模式的,也称为SQL数据库,引用结构化查询语句。但是近年来,面向文档和键值的数据库,非正式的统称为NoSQ…

乐友商城学习笔记(一)

SpringCloud 什么是SpringCloud 在SpringBoot基础上构建的微服务框架固定步骤 1.引入组件的启动器2.覆盖默认配置3.在引导类上添加相应的注解 eureka 注册中心,服务的注册与发现服务端 1.引入服务器启动器:eureka-server2.添加了配置 spring.applicati…

【Git】使用Git上传项目到远程仓库Gitee码云步骤详解

电脑里存放了很多项目,有的备份,有的没备份,如果不仔细分类管理的话,时间一长,到时看到那就会觉得非常杂乱,很难整理,这里有一个叫源代码托管,用过它的都知道,方便管理和…

如何下载阅读Spring源码-全过程详解

这篇文章记录了下载spring源码和在IDEA中打开运行的全过程,并且记录了过程中遇到的问题和解决方案,适合需要学习spring源码的同学阅读。 1.spring源码下载地址 通过Git下载spring-framework项目源码: git clone https://github.com/spring…

Document-Level event Extraction via human-like reading process 论文解读

Document-Level event Extraction via human-like reading process 论文:2202.03092v1.pdf (arxiv.org) 代码:无 期刊/会议:ICASSP 2022 摘要 文档级事件抽取(DEE)特别困难,因为它提出了两个挑战:论元分散和多事件。第一个挑战…

TPM 2.0实例探索2 —— LUKS磁盘加密(1)

本文大部分内容取自: LUKS磁盘格式_小写的毛毛的博客-CSDN博客_luks 如何破解LUKS加密 一、LUKS介绍 1. 什么是LUKS LUKS是“Linux Unified Key Setup”的简写,是 Linux 硬盘加密的标准。LUKS通过提供标准的磁盘格式,不仅可以促进发行版之…

短链或H5唤醒(跳转)APP应用

唤醒APP(两种方法) 一.短链唤醒(跳转)app ⭐ 短链跳转到APP,当如果用户手机不存在APP(某个应用)将会进入到官网页面。 app links实现 在android studio菜单栏Tools->App Links Ass点击,效果图如下 2.配置如下 点击ok,生成如下效果图 3.完成第二步后,会自动…

深度解读 | 数据资产管理面临诸多挑战,做好这5个措施是关键

日前,大数据技术标准推进委员会(中国通信标准化协会下(CCSA)的专业技术委员会,简称TC601)发布《数据资产管理实践白皮书》(6.0 版)(以下简称:报告&#xff09…

网友说socket通信讲的不彻底,原来这才是Socket

关于对 Socket 的认识,大致分为下面几个主题,Socket 是什么,Socket 是如何创建的,Socket 是如何连接并收发数据的,Socket 套接字的删除等。 Socket 是什么以及创建过程 一个数据包经由应用程序产生,进入到…

linux下安装mongoDB

一、下载mongoDB包 下载地址: https://www.mongodb.com/try/download/community 个人建议:如果是学习阶段,使用5以下版本更好些。 二、安装及配置 1、安装 # 1、解压 $ tar -zxvf mongodb-linux-x86_64-rhel70-4.4.19-rc1.tgz# 2、迁移目…

【二叉树】

1,利用类来构建结点,利用函数递归来构建树2,因为左子树的结点编号是父节点的2倍,右子树的结点编号是父节点的2倍1,所以可以用数组模拟建树的过程构建二叉树第一种构建方式class treenode():#二叉树节点def __init__(se…

【西安】Python-GEE遥感云大数据分析、管理与可视化技术及多领域案例实践应用

目录 第一章 理论基础 第二章 开发环境搭建 第三章 遥感大数据处理 基础 第四章 典型案例操作实践 第五章 输入输出及数据 资产高效管理 第六章 云端数据论文出版级可视化 ​随着航空、航天、近地空间等多个遥感平台的不断发展,近年来遥感技术突飞猛进。由此&…

使用代码生成器生成代码

一、新建数据源配置 因考虑到多数据源问题,代码生成器作为一个通用的模块,后续可能会为其他工程生成代码,所以,这里不直接读取系统工程配置的数据源,而是让用户自己维护。 新建数据源 参数说明 数据源名称&#xff1…

CIMCAI intellgent ship product applied by world top3 shipcompany

CIMCAI智慧船公司集装箱管理产品ceaspectusS™全球规模应用全球前三大船公司认可验箱标准应用落地全球港航人工智能AI独角兽 CIMCAI中集飞瞳CIMCAI Intellgent shipping product ceaspectusS ™which applied by the worlds top three shipping companiesGlobal port and shipp…

关于ch340驱动安装

这是一个悲伤的故事,搞了一上午,最后的解决办法是我找到了开发板的原装数据线,一换上去,板卡上电后,点击安装,就安装驱动成功了。。。。。把我走过的弯路记录在下面,链接里的办法是能解决阶段问…

【Go】使用Go语言打造定时提醒小工具,从基础到优化全方位探索

文章目录一、引言1.目的和背景2.选择GO语言的原因二、GO语言中的时间和定时器1.时间相关的包和函数2.定时器相关的包和函数三、使用GO语言实现功能四、代码改进1.time.AfterFunc()2.sync.WaitGroup3.接收参数五、总结一、引言 1.目的和背景 本文为征文活动“CSDN 征文活动&am…

(二十二)、实现评论功能(2)【uniapp+uinicloud多用户社区博客实战项目(完整开发文档-从零到完整项目)】

1,渲染评论列表 1.1,在detail页面中定义评论列表数组和getcomment方法: commentList: [],getcomment方法: //获取评论列表async getComment() {let commentTemp db.collection("quanzi_comment").where(article_id …

浏览器跨域问题

跨域问题什么是跨域问题如何解决跨域问题JSONPCORS方式解决跨域使用 Nginx 反向代理使用 WebSocket跨源请求是否能携带Cookie什么是跨域问题 跨域问题指的是不同站点之间,使用 ajax 无法相互调用的问题。跨域问题本质是浏览器的一种保护机制,它的初衷是为…

【离线数仓-3-数仓建模方法理论汇总】

离线数仓-3-数仓建模方法理论汇总离线数仓-3-数仓建模方法理论汇总1.数仓概述2.数据仓库核心架构(Hive)3.数据仓库建模概述4.数据仓库建模方法论1.ER(Entity Relationship)模型2.维度模型1.维度建模理论-事实表1. 事实表概述2.事实…

RabbitMQ学习(十):发布确认高级

一、概述在生产环境中由于一些不明原因,导致 RabbitMQ 重启,在 RabbitMQ 重启期间生产者消息投递失败导致消息丢失,需要手动处理和恢复。在这样比较极端的情况,当RabbitMQ 集群不可用的时候,无法投递的消息该如何处理呢…