设计模式:装饰者模式介绍及代码示例 && JDK里关于装饰者模式的应用
0、背景
来看一个项目需求:咖啡订购项目。
咖啡种类有很多:美式、摩卡、意大利浓咖啡;
咖啡加料:牛奶、豆浆、可可。
要求是,扩展新的咖啡种类的时候,能够方便维护,不同种类的咖啡需要快速计算多少钱,客户单点咖啡,也可以咖啡+料。
最差方案
直接想,就是一个咖啡基类,然后所有的单品、所有的组合咖啡都去继承这个基类,每个类都有自己的对应价格。
问题:那么多种咖啡和料的组合,都相当于是售卖的咖啡的一个子类,全都去实现基本就是一个全排列,显然又会类爆炸。并且,扩展起来,多一个调料,都要把所有咖啡种类算上重新组合一次。
改进方案
将调料内置到咖啡基类里,这样不会造成数量过多,当单品咖啡继承咖啡基类的时候,就都拥有了这些调料,同时,点没有点调料,要提供相应的方法,来计算是不是加了这个调料。
问题:这样的方式虽然改进了类爆炸的问题,但是属性内置导致了耦合性很强,如果删了一个调料呢?加了一个调料呢?每一个类都要改,维护量很大。
一、装饰者模式
装饰者模式:动态的将新功能附加到对象上,在对象功能扩展方面,比继承更有弹性。
具体实现起来是这样的,如下类图所示:
可以看到,在装饰者里面拥有一个 Component 对象,这是核心的部分。
也就是不像我们想的,给单品咖啡里加调料,而是反向思维,把单品咖啡拿到调料里来,决定对他的操作。
如果 ConcreteComponent 很多的话,甚至还可以再增加缓冲层。
用装饰者模式来解决上面的咖啡订单问题,类图设计如下,考虑到具体单品咖啡的种类,增加了一个缓冲层,最基本的抽象类叫 Drink:
其中 Drink 就相当于是前面的 Component,Coffee 是缓冲层,下面的不同 Coffee 就是上面的ConcreteConponent。
费用的计算方式一改正常思路的咖啡中,而是在调料中,因为 cost 在 Drink 类里也有,所以到最终的计算,其实是带上之前的 cost 结果,如果多种装饰者进行装饰,比如一个coffee加了很多料,那么其实是 递归的思路计算 最后的 cost 。
这样的话,增加一个单品咖啡,或者增加调料,都不用改变其他地方。
代码如下,类比较多,但是每个都比较简单:
/*
抽象类Drink,相当于Component;
getset方法提供给子类去设置饮品或调料的信息
但是:cost方法留给调料部分实现
*/
public abstract class Drink {
public String description;
private float price = 0.0f;
//价格方法
public abstract float cost();
public float getPrice() {
return price;
}
public void setPrice(float price) {
this.price = price;
}
public String getDescription() {
return description +":"+ price;
}
public void setDescription(String description) {
this.description = description;
}
}
接着就是Coffe缓冲层以及下面的实现类,相当于ConcreteComponent:
public class Coffee extends Drink{
@Override
public float cost() {
return super.getPrice();
}
}
public class MochaCoffee extends Coffee{
public MochaCoffee() {
setDescription(" 摩卡咖啡 ");
setPrice(7.0f);
}
}
public class USCoffee extends Coffee{
public USCoffee() {
setDescription(" 美式咖啡 ");
setPrice(5.0f);
}
}
public class ItalianCoffee extends Coffee {
public ItalianCoffee(){
setDescription(" 意大利咖啡 ");
setPrice(6.0f);
}
}
然后是装饰核心,Decorator,和Drink是继承+组合的关系:
/*
Decorator,反客为主去拿已经有price的drink,并加上佐料
加佐料的时候是拿去了Drink对象,但是也是给Drink进行
*/
public class Decorator extends Drink{
private Drink drink;
//提供一个构造器
public Decorator(Drink drink){
this.drink = drink;
}
@Override
public float cost() {
//计算成本,拿到佐料自己的价格+本来一杯Drink的价格
//这里注意调用的是drink.cost不是drink.getPrice,因为cost才是子类实现的,Drink类的getPrice方法默认是返回0
return super.getPrice() + drink.cost();
}
@Override
public String getDescription() {
//自己的信息+被装饰者coffee的信息
return description + " " + getPrice() + " &&" + drink.getDescription();
}
}
以及Decorator的实现类,也就是ConcreteDecorator:
public class Milk extends Decorator{
public Milk(Drink drink) {
super(drink);
setDescription(" 牛奶:");
setPrice(1.0f);
}
}
public class Coco extends Decorator{
public Coco(Drink drink) {
super(drink);
setDescription(" 可可:");
setPrice(2.0f);//调味品价格
}
}
public class Sugar extends Decorator {
public Sugar(Drink drink) {
super(drink);
setDescription(" 糖:");
setPrice(0.5f);
}
}
注意,对于具体的Decorator,这里就体现了逆向思维,拿到的 drink 对象,调用父类构造器得到了一个drink,然后 set 和 get 方法设置调料自己的price和description,父类的方法 cost 就会计算价钱综合。
那里面的 super.getPrice() + drink.cost() 中的 cost(),就是一个递归的过程。
最后我们来写一个客户端测试:
public class Client {
public static void main(String[] args) {
//1.点一个咖啡,用Drink接受,因为还没有完成装饰
Drink usCoffee = new USCoffee();
System.out.println("费用:"+usCoffee.cost()+" 饮品信息:"+usCoffee.getDescription());
//2.加料
usCoffee = new Milk(usCoffee);
System.out.println("加奶后:"+usCoffee.cost()+" 饮品信息:"+usCoffee.getDescription());
//3.再加可可
usCoffee = new Coco(usCoffee);
System.out.println("加奶和巧克力后:"+usCoffee.cost()+" 饮品信息:"+usCoffee.getDescription());
}
}
可以看到,调用的时候,加佐料只要在原来的 drink 对象的基础上,重新构造,将原来的 drink 放进去包装(装饰),最后就达到了效果。
并且,如果要扩展一个类型的 coffee 或者一个调料,只用增加自己一个类就可以。
二、装饰者模式在 JDK 里的应用
java 的 IO 结构,FilterInputStream 就是一个装饰者。
2.1 这里面 InputStream 就相当于 Drink,也就是 Component 部分;
2.2 FileInputStream、StringBufferInputStream、ByteArrayInputStream 就相当于是单品咖啡,也就是ConcreteComponent,是 InputStream 的子类;
2.3 而 FilterInputStream 就相当于 Decorator,继承 InputStream 的同时又组合了InputStream;
2.4 BufferInputStream、DataInputStream、LineNumberInputStream 相当于具体的调料,是FilterInputStream的子类。
我们一般使用的时候:
DataInputStream dataInputStream = new DataInputStream(new FileInputStream("D://test.txt"));
或者:
FileInputStream fi = new FileInputStream("D:\\test.txt");
DataInputStream dataInputStream = new DataInputStream(fi);
//具体操作
这里面的 fi 就相当于单品咖啡, datainputStream 就是给他加了佐料。
更贴合上面咖啡的写法,声明的时候用 InputStream 接他,就可以:
InputStream fi = new FileInputStream("D:\\test.txt");
fi = new DataInputStream(fi);
//具体操作
感觉真是完全一样呢。
设计模式:装饰者模式介绍及代码示例 && JDK里关于装饰者模式的应用的更多相关文章
- C++工厂方法模式讲解和代码示例
在C++中使用模式 使用示例: 工厂方法模式在 C++ 代码中得到了广泛使用. 当你需要在代码中提供高层次的灵活性时, 该模式会非常实用. 识别方法: 工厂方法可通过构建方法来识别, 它会创建具体类的 ...
- DOM4J介绍与代码示例【转载】
DOM4J是dom4j.org出品的一个开源XML解析包.Dom4j是一个易用的.开源的库,用于XML,XPath和XSLT.它应用于Java平台,采用了Java集合框架并完全支持DOM,SAX和JA ...
- DOM4J介绍与代码示例
DOM4J是dom4j.org出品的一个开源XML解析包.Dom4j是一个易用的.开源的库,用于XML,XPath和XSLT.它应用于Java平台,采用了Java集合框架并完全支持DOM,SAX和JA ...
- DOM4J介绍与代码示例(2)-XPath 详解
XPath 详解,总结 XPath简介 XPath是W3C的一个标准.它最主要的目的是为了在XML1.0或XML1.1文档节点树中定位节点所设计.目前有XPath1.0和 XPath2.0两个版本.其 ...
- 模仿VIMD的模式的简化代码示例
按numpad0来切换模式,按t显示不同的结果: Numpad0:: tfmode:=!tfmode aaa:=(tfmode=?"AAAA":"BBBB") ...
- PHP数组函数实现栈与队列的方法介绍(代码示例)
根据php提供的四个关于数组的函数: array_push(),array_pop(),array_unshift(),array_shift() 配合数组本身,一下子就实现了栈(stack)和队例( ...
- <代码整洁之道>、<java与模式>、<head first设计模式>读书笔记集合
一.前言 几个月前的看书笔记 ...
- 【嵌入式开发】裸机引导操作系统和ARM 内存操作 ( DRAM SRAM 类型 简介 | Logical Bank | 内存地址空间介绍 | 内存芯片连接方式 | 内存初始化 | 汇编代码示例 )
[嵌入式开发]ARM 内存操作 ( DRAM SRAM 类型 简介 | Logical Bank | 内存地址空间介绍 | 内存芯片连接方式 | 内存初始化 | 汇编代码示例 ) 一. 内存 ...
- 设计模式(九): 从醋溜土豆丝和清炒苦瓜中来学习"模板方法模式"(Template Method Pattern)
今天是五.四青年节,祝大家节日快乐.看着今天这标题就有食欲,夏天到了,醋溜土豆丝和清炒苦瓜适合夏天吃,好吃不上火.这两道菜大部分人都应该吃过,特别是醋溜土豆丝,作为“鲁菜”的代表作之一更是为大众所熟知 ...
随机推荐
- Java中lambda(λ)表达式的语法
举一个排序的例子,我们传入代码来检查一个字符串是否比另一个字符串短.这里要计算: first.length() - second.length() first和second是什么?他们都是字符串.Ja ...
- 好用的npm模块记录
标签: node node盛行的今天,前端开发已经离不开npm模块的使用,大名鼎鼎的如gulp,webpack等,此处不多说,除了它们有那么几个常用的npm模块是我喜欢并依赖它的,下面就是我平时工作中 ...
- 详解 awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}每个字段的意思
用这个列子说好了如果NF代表字段 那最后应该是7 才对啊 还有最后怎么都是1呢?END前面的是查看并发吧 后面是查看 tcp连接数 是这样吗? awk下标采用字符串来表示可能你在其它语言见 ...
- Bug--Tomcat Error start child
添加Quartz之后报错 下面的Cause by: More than one fragment with the name [spring_web] was found. This is not l ...
- 高度塌陷与BFC
高度塌陷的产生条件 子元素浮动,脱离文档流 子元素绝对定位或固定定位,脱离文档流 定位产生的高度塌陷只能通过加固定高度或更换其他方案解决塌陷,本文主要讨论浮动产生塌陷的解决方法. 高度塌陷的解决方法 ...
- List接口(动态数组)
List接口(动态数组) List集合类中元素有序且可重复 ArrayList(重要) 作为List接口的主要实现类 线程不安全的,效率高 底层使用Object[] elementData数组存储 A ...
- queue stack for STL
前不久发现自己vector有些不会了,于是想起了queue和stack. 有一个小故事,,,某天我跟自己打赌我queue没有写博园,结果打开一看竟然不知什么时候写过了,而且(QAQ)还有一定的浏览量了 ...
- PHP array_diff_uassoc() 函数
实例 比较两个数组的键名和键值(使用用户自定义函数比较键名),并返回差集: <?phpfunction myfunction($a,$b){if ($a===$b){return 0;}retu ...
- PHP xml_set_default_handler() 函数
定义和用法 xml_set_default_handler() 函数为 XML 解析器建立默认的数据处理器.高佣联盟 www.cgewang.com 该函数规定在只要解析器在 XML 文件中找到数据时 ...
- [转]Java CPU 100% 排查技巧
文章来源:微信公众号:猿天地 平时多积累一点,这样在遇到问题的时候就少句求人的话.如果在实际的开发中遇到CPU 100%问题,要怎么排查呢?如果你没有遇到过这个问题,请先自己思考10s,如果你遇到过, ...