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);
//具体操作

感觉真是完全一样呢。

最新文章

  1. windows下ThinkPHP3.2.3使用memcache缓存
  2. jQuery工具函数(转)
  3. Android实用代码模块集锦
  4. 如何用Jquery判断在键盘上敲的哪个按键
  5. (ios实战) UINavigationBar 返回按钮 文本自定义实现
  6. python中Url链接编码处理(urlencode,urldecode)
  7. extjs form 取值 赋值 重置
  8. Yosemite重置Dock的命令
  9. shell脚本实现查找文件夹下重复的文件,并提供删除功能
  10. java基础程序题
  11. 我的2011年总结--大明zeroson程序猿一周年总结
  12. POJ 2348 Euclid's Game(简单博弈)
  13. 零基础如何一步一步开始搭建高性能直播平台?现以GitChat·架构来进行说明
  14. 学习笔记-JS公开课一
  15. Functional programming idiom
  16. python requests库爬取网页小实例:爬取网页图片
  17. 使用PHP、MySQL实现修改密码 + 防止通过url强行进入系统
  18. HTML/CSS基础知识(三)
  19. [js]Object.defineProperty等几个js特殊方法
  20. java类的设计原则

热门文章

  1. Java中Map的entrySet()详解
  2. centos 安装 nginx 及配置 的坑
  3. .log文件超过2.56MB?Pycharm的.log文件读取不完全?.log文件无法被调用?
  4. VuePress博客美化之reco主题
  5. Spring学习之Spring与Mybatis的两种整合方式
  6. 最新 laravel5.8 连接redis集群
  7. C/C++编程笔记:编写完成了一个C/C++程序,如何做一个界面出来?
  8. react - 多层级嵌套路由支持
  9. LINUX --- echo修改GPIO状态
  10. MapReduce之自定义OutputFormat