设计模式之【装饰者模式】,实现“穿衣打扮”自由原来这么简单

2023-05-16

文章目录

  • 一、什么是装饰者模式
    • 1、装饰者模式原理
    • 2、装饰者模式四大角色
    • 3、代理、桥接、装饰器、适配器 4 种设计模式的区别
    • 4、装饰者模式的应用场景
    • 5、装饰者模式和代理模式的对比
    • 6、装饰者模式优缺点
    • 7、抽象装饰器(Decorator)是必需的吗
  • 二、实例1-煎饼
    • 使用装饰者模式优化代码
  • 三、实例2-日志
  • 四、JDK中IO流对装饰者模式的使用
  • 参考资料

一、什么是装饰者模式

装饰者模式(Decorator Pattern),也称为包装模式(Wrapper Pattern)、装饰器模式,是指在不改变原有对象的基础之上,将功能附加到对象上,提供了比继承更有弹性的替代方案(扩展原有对象的功能),属于结构型模式。

在 GoF 的《设计模式》一书中,对装饰者模式是这样理解的:动态地给一个对象添加一些额外的职责,就增加功能来说,装饰者模式相比生成子类更灵活。

装饰者模式的核心是扩展功能。使用装饰者模式可以透明且动态地扩展类的功能。

装饰器模式主要解决继承关系过于复杂的问题,通过组合来替代继承。它主要的作用是给原始类添加增强功能。这也是判断是否该用装饰器模式的一个重要的依据。除此之外,装饰器模式还有一个特点,那就是可以对原始类嵌套使用多个装饰器。为了满足这个应用场景,在设计的时候,装饰器类需要跟原始类继承相同的抽象类或者接口。

1、装饰者模式原理

装饰者模式主要用于透明且动态地扩展类的功能。

其实现原理为:让装饰器实现被包装类(ConcreteComponent)和相同的接口(Component)(使得装饰器与被扩展类类型一致),并在构造函数中传入该接口(Component)对象,然后就可以在接口需要实现的方法中在被包装类对象的现有个功能上添加新功能了。而且由于装饰器与被包装类属于同一类型(均为Component),且构造函数的参数为其实现接口类(Component),因此装饰器模式具备嵌套扩展功能,这样我们就能使用装饰器模式一层一层的对最底层被包装类进行功能扩展了。

2、装饰者模式四大角色

从UML类图中,我们可以看到,装饰者模式主要包含四种角色:
在这里插入图片描述
抽象组件(Component):可以是一个接口或者抽象类,其充当被装饰类的原始对象,规定了被装饰对象的行为;

具体组件(ConcreteComponent):实现/继承Component的一个具体对象,也即 被装饰对象;

抽象装饰器(Decorator):通用的装饰ConcreteComponent的装饰器,其内部必然有一个属性指向Component抽象组件;其实现一半是一个抽象类,主要是为了让其子类按照其构造形式传入一个Component抽象组件,这是强制的通用行为(当然,如果系统中装饰逻辑单一,并不需要实现许多装饰器,那么我们可以直接忽略该类,而直接实现一个具体装饰器(ConcreteDecorator)即可);

具体装饰器(ConcreteDecorator):Decorator的具体实现类,理论上,每个ConcreteDecorator都扩展了Component对象的一种功能。

装饰者模式角色分配符合设计模式里氏替换原则、依赖倒置原则,从而使得其具备很强的扩展性,最终满足开闭原则。

3、代理、桥接、装饰器、适配器 4 种设计模式的区别

代理、桥接、装饰器、适配器,这 4 种模式是比较常用的结构型设计模式。它们的代码结构非常相似。笼统来说,它们都可以称为 Wrapper 模式,也就是通过 Wrapper 类二次封装原始类。

尽管代码结构相似,但这 4 种设计模式的用意完全不同,也就是说要解决的问题、应用场景不同,这也是它们的主要区别。这里我就简单说一下它们之间的区别。

代理模式:代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。

桥接模式:桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变。

装饰器模式:装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。

适配器模式:适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口。

4、装饰者模式的应用场景

装饰者模式在我们生活中应用很广,比如说煎饼加鸡蛋加肠,给蛋糕加水果,给房子装修等等,为对象扩展一些额外的职责。装饰器在代码程序中适用于以下场景:
(1)用于扩展一个类的功能或给一个类添加附加职责。
(2)动态的给一个对象添加功能,这些功能可以再动态的撤销。
(3)需要为一批的兄弟类进行改装或加装功能。

5、装饰者模式和代理模式的对比

我们看一下代理模式的UML类图:
在这里插入图片描述
从代理模式的UML类图和通用代码实现上看,代理模式与装饰者模式几乎一模一样。代理模式的Subject对应装饰者模式的Component,代理模式的RealSubject对应装饰者模式的ConcreteComponent,代理模式的Proxy对应装饰器模式的Decorator。确实,从代码实现上看,代理模式的确与装饰者模式是一样的(其实装饰者模式就是代理模式的一个特殊应用),但是这两种设计模式所面向的功能扩展面是不一样的:

装饰者模式强调自身功能的扩展。Decorator所做的就是增强ConcreteComponent的功能(也有可能减弱功能),主体对象为ConcreteComponent,着重类功能的变化;

代理模式强调对代理过程的控制。Proxy完全掌握对RealSubject的访问控制,因此,Proxy可以决定对RealSubject进行功能扩展,功能缩减甚至功能散失(不调用RealSubject方法),主体对象为Proxy;

简单来讲,假设现在小明想租房,那么势必会有一些事务发生:房源搜索、联系房东谈价格……
假设我们按照代理模式进行思考,那么小明只需找到一个房产中介,让他去 干房源搜索,联系房东谈价格这些事情,小明只需等待通知然后付点中介费就行了;
而如果采用装饰器模式进行思考,因为装饰器模式强调的是自身功能扩展,也就是说,如果要找房子,小明自身就要增加房源搜索能力扩展,联系房东谈价格能力扩展,通过相应的装饰器,提升自身能力,一个人做满所有的事情。

6、装饰者模式优缺点

优点:
1.装饰器是继承的有力补充,比继承灵活,不改变原有对象的情况下动态地给一个对象扩展功能,即插即用。
2.通过使用不同装饰类以及这些装饰类的排列组合,可以实现不同效果。
3.装饰器完全遵守开闭原则。

缺点:
1.会出现更多的代码,更多的类,增加程序复杂性。
2.动态装饰时,多层装饰时会更复杂。

7、抽象装饰器(Decorator)是必需的吗

不是必须的,抽象装饰器的本质就是将附加功能抽离出来,简化原有逻辑,可以根据业务模型,可选择的忽略抽象装饰器。

二、实例1-煎饼

很多小伙伴喜欢逛街吃小吃,此时我们有这样一个需求:一个煎饼摊,煎饼可以加鸡蛋、加香肠,计算最终的价格,我们如何实现?

创建一个煎饼类:

public class Battercake {

    protected String getMsg(){ return "煎饼";}

    public int getPrice(){ return 5;}
}

加一个鸡蛋:

public class BattercakeWithEgg extends Battercake {

    protected String getMsg(){ return super.getMsg() + "+1个鸡蛋";}

    public int getPrice(){ return super.getPrice() + 1;}

}

即加鸡蛋又加香肠:

public class BattercakeWithEggAndSauage extends BattercakeWithEgg {

    protected String getMsg(){ return super.getMsg() + "+1根香肠";}

    public int getPrice(){ return super.getPrice() + 2;}

}

此时的类继承关系如下:
在这里插入图片描述
测试类:

public class Test {

    public static void main(String[] args) {
        Battercake battercake = new Battercake();
        System.out.println(battercake.getMsg() + ",总价:" + battercake.getPrice());

        BattercakeWithEgg battercakeWithEgg = new BattercakeWithEgg();
        System.out.println(battercakeWithEgg.getMsg() + ",总价:" + battercakeWithEgg.getPrice());

        BattercakeWithEggAndSauage battercakeWithEggAndSauage = new BattercakeWithEggAndSauage();
        System.out.println(battercakeWithEggAndSauage.getMsg() + ",总价:" + battercakeWithEggAndSauage.getPrice());
    }

}

运行结果是没有问题的,但是,如果客户想要只加香肠,或者加辣条等等,那么我们的工作量无疑是很大的。

此时,用装饰器模式可以完美解决这个问题。

使用装饰者模式优化代码

// 煎饼抽象类,或者接口
public abstract class Battercake {

    protected abstract String getMsg();

    protected abstract int getPrice();
}
// 煎饼基础套餐(什么也不包装,什么也不加)
public class BaseBattercake extends Battercake{

    protected String getMsg(){ return "煎饼";}

    public int getPrice(){ return 5;}

}

此时,需要创建一个扩展套餐的抽象装饰器:

public class BattercakeDecorator extends Battercake{

    private Battercake battercake;

    public BattercakeDecorator(Battercake battercake) {
        this.battercake = battercake;
    }

    protected String getMsg(){ return this.battercake.getMsg();}

    public int getPrice(){ return this.battercake.getPrice();}
}
// 鸡蛋装饰器
public class EggDecorator extends BattercakeDecorator{

    public EggDecorator(Battercake battercake) {
        super(battercake);
    }
    protected String getMsg(){ return super.getMsg() + "1个鸡蛋";}

    public int getPrice(){ return super.getPrice() + 1;}
}

// 香肠装饰器
public class SauageDecorator extends BattercakeDecorator{

    public SauageDecorator(Battercake battercake) {
        super(battercake);
    }
    protected String getMsg(){ return super.getMsg() + "1根香肠";}

    public int getPrice(){ return super.getPrice() + 2;}
}
// 测试类
public class Test {
    public static void main(String[] args) {
        Battercake battercake;
		// 基础套餐
        battercake = new BaseBattercake();
		// 加鸡蛋
        battercake = new EggDecorator(battercake);
		// 再加一个鸡蛋
        battercake = new EggDecorator(battercake);
		// 加香肠
        battercake = new SauageDecorator(battercake);

        System.out.println(battercake.getMsg() + ",总价" + battercake.getPrice());

    }
}

在这里插入图片描述

三、实例2-日志

假如说我们现有的框架是使用Slf4j+log4j2 实现的,但是现有的日志体系打印出的结果是一段没有任何格式的字符串:

Logger logger = LoggerFactory.getLogger(clazz);
logger.info("测试内容");

我们想将打印的结果转成json格式,就需要采用装饰器模式了。

定义装饰器类,实现顶层Logger接口:

public class LoggerDecorator implements Logger {

    protected Logger logger;

    public LoggerDecorator(Logger logger) {
        this.logger = logger;
    }

    public void info(String s) {

    }
    // 省略其他实现
}

创建装饰器实现类:

public class JsonLogger extends LoggerDecorator {
    public JsonLogger(Logger logger) {
        super(logger);
    }

    @Override
    public void info(String s) {
        JSONObject result = newJsonObject();
        result.put("message",s);
        logger.info(result.toString());
    }

    @Override
    public void error(String s) {
        JSONObject result = newJsonObject();
        result.put("message",s);
        logger.info(result.toString());
    }
    @Override
    public void error(String s, Throwable e){
        JSONObject result = newJsonObject();
        result.put("exception",e.getClass().getName());
        String trace = Arrays.toString(e.getStackTrace());
        result.put("starckTrace",trace);
        logger.info(result.toString());
    }

    private JSONObject newJsonObject(){
        return new JSONObject();
    }
}

在JsonLogger中,对于Logger的各种接口,我们都用JsonObject进行封装,最终还是调用logger.info,只是这个字符串被我们装饰过

定义一个 工厂类,方便使用:

public class JsonLoggerFactory {

    public static JsonLogger getLogger(Class clazz){
        Logger logger = LoggerFactory.getLogger(clazz);
        return new JsonLogger(logger);
    }
}

public class Test {
    private static final Logger logger = JsonLoggerFactory.getLogger(Test.class);

    public static void main(String[] args) {
        logger.error("系统错误");

        try {
            int i = 1/0;

        } catch (Exception e) {
            logger.error("异常", e);
        }

    }
}

在这里插入图片描述

四、JDK中IO流对装饰者模式的使用

IO流中的包装类使用到了装饰者模式。BufferedInputStream,BufferedOutputStream,BufferedReader,BufferedWriter。

我们以BufferedWriter举例来说明,先看看如何使用BufferedWriter

public class Demo {
	public static void main(String[] args) throws Exception{
		//创建BufferedWriter对象
		//创建FileWriter对象
		FileWriter fw = new FileWriter("C:\\Users\\Think\\Desktop\\a.txt");
		BufferedWriter bw = new BufferedWriter(fw);
		//写数据
		bw.write("hello Buffered");
		bw.close();
	}
}

使用起来感觉确实像是装饰者模式,接下来看它们的结构:

在这里插入图片描述
BufferedWriter使用装饰者模式对Writer子实现类进行了增强,添加了缓冲区,提高了写数据的效率。

参考资料

http://www.uml.org.cn/sjms/202105262.asp

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

设计模式之【装饰者模式】,实现“穿衣打扮”自由原来这么简单 的相关文章

  • VirtualBox调整分分辨率

    以前在windows或者macOS基本都是使用vmware来玩虚拟机的 到linux环境 xff0c 想体验一下VirtualBox 结果用的时间发现分辨率都是调不出来 记录一下我的操作方法 环境 宿主机 xff1a archlinux 4
  • VirtualBox宿主机复制粘贴时有时无

    现象 首先我已经安装了增强功能 xff0c 共享剪切板选择的也是双向 虚拟机刚开机剪切板功能是正常的 可以双向复制 但是用了一段时间以后 xff0c 就会发现从宿主机得到到虚拟机就不行了 但是可以从虚拟机复制到宿主机 环境 我的宿主机是 a
  • HBuilderX运行微信小程序不报错也打不开

    我主用的是linux操作系统 xff0c 因为要开发微信小程序 所以在虚拟机中安装了一个windows7系统 环境 虚拟机中的windows7 HbuilderX 43 微信开发者工具 现象 使用HbuilderX开发uni app并想在微
  • Virtualbox虚拟机与主机相互访问

    刚从vmware切换到Virtualbox有些地方还是不太熟悉 网络连接这块就被卡了一下 xff0c 后来发现其实很简单 xff0c 是我想多了 环境 主机 xff1a archlinux 虚拟机 xff1a windows 10 软件版本
  • maven打包报内存不足,配置maven vm options

    服务器上内存所剩余不多了 xff0c 用mvn打包报内存不足 解决办法就要把vm options中内存设置小一些 mvn jvm config文件 xff1a 从 Maven 3 3 1 43 开始 xff0c 您可以通过 maven pr
  • archlinux滚动更新导致virtualbox虚拟机无法启动

    今天遇到一个奇怪的问题 xff0c 滚动更新以后 xff0c pacman Syyu 虚拟机无法正常启动了 虚拟机启动以后一直显示Starting virtual machine 并且卡在20 不动了 原因分析 我怀疑是更新系统后 xff0
  • 设计模式之【工厂模式】,创建对象原来有这么多玩法

    文章目录 一 什么是工厂模式1 工厂模式的意义2 什么时候应该用工厂模式 二 简单工厂模式1 实例 xff08 1 xff09 使用简单工厂进行优化 xff08 2 xff09 静态工厂 xff08 3 xff09 使用map来去除if x
  • archlinux安装node.js长期支持版本

    默认archlinux安装的node js版本是18 我今天在遇到项目时报错了 xff0c 项目中用到一个组件不支持18 那么如果安装 低版本的Node呢 xff1f 先在软件仓库中搜索一下 xff1a pacman Ss span cla
  • windows开机启动目录

    设置windows开机启动有很多种方法 xff0c 最简单的方法恐怕就是把快捷方式放到启动目录吧 windows开机启动目录 按下 win 43 R 打开运行输入 xff1a shell span class token operator
  • linux 灰度显示grayscale

    之前我在使用macOS的时候 xff0c 非常喜欢使用灰度显示界面 到底是什么原因让我有这么 变态 的需求 下面听听我的故事 xff0c 当时不重要 xff0c 可以跳过 是这样的 xff0c 因为我是程序员 xff0c 算上电脑 43 手
  • charles iOS手机抓包

    iOS手机如何抓包 下载charles https www charlesproxy com latest release download do 注册码 Registered Name span class token builtin c
  • redis cli笔记

    此篇为人个笔记 xff0c 基本是个人常用命令 xff0c 仅供参考 基础操作 redis cli redis span class token number 127 0 span 0 1 637 span class token oper
  • Jsoup通过curl Request设置header

    使用Charles时发现它有一个功能非常的方便 Copy cURL Request菜单 可以把请求中的header信息自动拼接成curl的参数 其实有很多的软件都具备这样的功能 那么如何把这些header信息直接 放到Jsoup的heade
  • 【Java题】用户数据中的身高体重收集

    题目要求 用户数据中的身高体重收集 应用程序中用户数据收集 xff0c 非常重要 xff0c 是大数据重要来源之一 在某APP场景中收集用户的身高体重数据 xff0c 身高单位 米 xff0c 体重单位kg xff0c 要求用户输入的任何数
  • archlinux i3wm通知管理

    通知管理 Dunst 是大多数桌面环境提供的通知守护程序的轻量级替代品 它非常可定制 xff0c 不依赖于任何工具包 xff0c 因此适合那些我们都喜欢定制到完美的以窗口管理器为中心的设置 官网如下 xff1a https dunst pr
  • i3wm 获取window class

    在i3wm中如果你想让一个程序固定在某个workspace中打开可以如下设置 span class token comment 打开virtual box直接进入第10个桌面 span assign span class token pun
  • xrandr修改分辨率与刷新率

    查询当前显示器信息 直接运行xrandr即可 我的执行结果如下 xff0c 信息过多 xff0c 我则把关键部分放出来 xff1a itkey 64 vivobook screenlayout xrandr span class token
  • 设计模式之【适配器模式】,两个人之间确实需要月老的搭线~

    文章目录 一 什么是适配器模式1 适配器模式使用场景2 代理 桥接 装饰器 适配器 4 种设计模式的区别3 适配器模式结构 二 类适配器1 实例 三 对象适配器1 实例 四 接口适配器1 实例 五 源码中的应用 一 什么是适配器模式 适配器
  • linux黑客帝国cmatrix

    装X神器 xff0c 黑客帝国 xff01 xff01 xff01 安装方法 我这里以archlinux为例 span class token function sudo span pacman S cmatrix 效果演示
  • mysql字段userid逗号分开保存按userid查询

    我的sql水平一般 xff0c 仅是一个笔记 无法保存是最优解 仅供参考 场景 有一张消息表 xff0c 其中有一个收信人字段中把多个用户以 分隔保存信息 我需要根据userid来查询信息 为了方便理解 xff0c 我减化一下表结构 我的表

随机推荐

  • windows11 + linux 蓝牙连接问题

    我主要使用Linux办公 xff0c 因为一些特殊情况需要到windows系统下测试 这时就会发现蓝牙键盘就需要重新连接 xff0c 只要切一次系统就要重新连接一次非常的麻烦 今天要多次往返这两个系统 xff0c 所以我决定解决一下这个问题
  • archlinux音量管理

    我用的i3wm 平时音量调整 xff0c 我是直接使用键盘自带的多媒体键实现的 xff0c 所以一直也懒得折腾 多媒体键盘调整音量的缺点就是无法细力度调整 xff0c 导致使用耳机听歌时 xff0c 要么声音听不到 xff0c 要么就是听不
  • git 命令行版本初始化

    假设你已经在网页上创建了一个如下的版本库 http git ycmit cn r manuli api git 然后我们需要把本地已经有的文件上传到版本库 xff0c 执行以下命令即可 span class token function g
  • mysql转SQL Server

    使用工具 Navicat 点击 工具 61 数据传输 即可完成
  • jdbc SQLServer Error: “The server selected protocol version TLS10

    最近在尝试在SQL Server 环境下开发新项目 xff0c 遇到了一些坑 xff0c 记录一下 报错信息 com span class token punctuation span microsoft span class token
  • windows11右键菜单变回windows10风格

    windows11的右键菜单会折叠一部分 xff0c 虽然美观了不少 但是总感觉效率反而更低了 xff0c 能不能设置回windows10风格的呢 xff1f 操作 Windows 43 R xff0c 输入 regedit 并按 Ente
  • C# winform使用SQLite

    本文仅是一个笔记 xff0c 仅供参考 SQLite SQLite是遵守ACID的关系数据库管理系统 xff0c 它包含在一个相对小的C程序库中 与许多其它数据库管理系统不同 xff0c SQLite不是一个客户端 服务器结构的数据库引擎
  • C# SQLite Database Locked exception

    现象 在查询时没有问题 xff0c 但是在Insert时会报数据库被锁定 原因分析 可能是代码某个地方连接着数据库 xff0c 忘记关闭了 解决办法 原理我还没有完全搞懂 xff0c 不过根据下面的写法确实解决了问题 在某个地方 xff0c
  • 设计模式之【桥接模式】,多用组合少用继承

    文章目录 一 什么是桥接模式1 使用场景2 代理 桥接 装饰器 适配器 4 种设计模式的区别3 桥接模式的优缺点4 桥接模式的四种角色 二 实例桥接模式优化代码 三 源码中使用的桥接模式1 桥接模式在JDBC中的应用 一 什么是桥接模式 桥
  • C# 无操作则退出登陆

    span class token keyword using span span class token namespace System span span class token punctuation span span class
  • C#调用explorer.exe打开指定目录

    需求 C 程序上有一个按钮 xff0c 点击打开电脑上对应的目录即可 代码 span class token comment 打开帮助文档 span System span class token punctuation span Diag
  • windows中ncdu替代者TreeSize

    平时开发大部分时间使用linux或者macOS系统 xff0c 近期因为要开发C 程序不得不使用windows系统 在linux或者macOS常用的ncdu来查看文件目录体积大小 到windows中无法使用 xff0c 很不习惯 就想找一个
  • Visual Studio中vim模拟器

    简介 Vim 仿真 这是 Visual Studio 2015 及更高版本的 Vim 仿真层 它将 Vim 熟悉的键绑定体验直接集成到 Visual Studio 的编辑器中 细节 GitHub 上提供了该项目和问题跟踪的完整源代码 htt
  • mybatis小示例

    一般使用mybatis的环境 xff0c 大多都是别人已经配置好的 直接用就好了 xff0c 如何自己搭建呢 xff1f 其实很简单 看官方的文档就可以解决了 主要为了学习mybatis最基础的配置 我文章中的方法不基于spring 一般很
  • spring boot集成mybatis报错 java.lang.IllegalStateException: No supported DataSource type found

    背景 我参考MyBatis Spring的文档搭建环境 https mybatis org spring zh getting started html 完全按文档上操作 xff0c 还是报以下错误 报错信息 Error starting
  • Spring boot mybatis 简单示例

    我在Spring boot中集成mybatis竟然花了不少时间 xff0c 真没想到 对着官网的文档做 xff0c 竟然还花了这么多时间 所以我把过程尽可能的详细记录下来 xff0c 给有需要的朋友 需求 在spring boot 中使用m
  • spring boot + mybatis+ mysql环境搭建

    最近在尝试从0开始搭建框架 xff0c 结果在mybatis这块就踩了很多坑 于是就决定写篇文章记录一下 要求 尽可能的简单 xff0c 减少依赖 实战 新建spring boot项目 基于spring boot 的 xff0c 所以第一步
  • javascript字符串转对象

    永远不要使用 eval xff01 eval 是一个危险的函数 xff0c 它使用与调用者相同的权限执行代码 如果你用 eval 运行的字符串代码被恶意方 xff08 不怀好意的人 xff09 修改 xff0c 您最终可能会在您的网页 扩展
  • fastjson 属性排序

    fastjson把bean转成json字符串 xff0c 默认的顺序并不是按你实体类属性写的顺序来的 虽然属性顺序一般不会影响功能 xff0c 但是强迫症忍不了 因为java中通过反射机制是无法取到属性的顺序的 xff08 我猜测 xff0
  • 设计模式之【装饰者模式】,实现“穿衣打扮”自由原来这么简单

    文章目录 一 什么是装饰者模式1 装饰者模式原理2 装饰者模式四大角色3 代理 桥接 装饰器 适配器 4 种设计模式的区别4 装饰者模式的应用场景5 装饰者模式和代理模式的对比6 装饰者模式优缺点7 抽象装饰器 xff08 Decorato