装饰者(Decorate)模式
1. 欢迎来到PeiTea
全国最大的的奶茶店——PeiTea,它们现在需要一个奶茶订单系统,经历了重重的考验,我最终拿到了这一笔收益不菲的订单,现在PeiTea一期要求必须马上上线一个订单功能,要求满足以下需求:
- 用户在购买奶茶时可以加入各种小料,例如:芋圆,珍珠,奶豆腐,芝士等;
- 系统可以根据不同的奶茶品种和小料,自动换算出对应的金额;
- 系统最好还可以支持动态打印小票,可以将茶底和小料打印出来方便制作人员制作。
PeiTea这边提供了他们之前使用的订单代码,类设计如下图:
2. 问题分析
这样的类设计有什么问题?
每一种奶茶都对应了一个唯一的对象,如果新推出了一个新品种的奶茶,就需要额外维护新的奶茶对象;
如果某一样小料的价格有变动,那样需要修改所有涉及到这个小料的奶茶对象.
我盯着代码研究了一会,调整了一下类设计,如下图:
***这样的类设计还是存在一些问题的,你知道是什么问题么?(答案在文末)***
3.认识装饰者模式
我们现在了解到,以上两种类设计方式没有办法完全解决类爆炸,设计死板,当有新小料加入时不适用等问题!
现在,我决定采用一种不一样的设计方法,以奶茶为主体,然后在制作过程中,可以用各种小料来装饰她。比如说:
- 我需要一杯绿茶为茶底的芋圆芝士奶茶;
- 我们先创建一个绿茶茶底的奶茶对象
- 然后我们用芋圆对象去装饰它
- 最后我们用芝士对象去装饰它
图示:
当需要计算价格时,只需要调用最外层的装饰者即可得出当前商品的总价。
我们先调用Cheese对象的getPrice()方法,Cheese对象会委托TaroBalls对象计算出他的价格,再加上自己的价格,然后将总价返回出去:
同上原理,TaroBalls对象会继续委托GreenTea对象计算他的价格,再加上自己的价格,将总价返回给Cheese对象;
一层一层的调用,直到调用到最底层为止。
4. 定义装饰者模式
- 我们先来整理一下我们目前所能知道的一切:
- 装饰者和被装饰对象都具有相同的类型;
- 我们可以用一个或者多个装饰者来装饰一个对象;
- 因为装饰者和被装饰对象具有相同的类型,所以当我们在任意一个需要原始对象(被装饰者)的地方,都可以直接用装饰他的对象替换;
- 对象可以在任何时候被装饰且不限量。
- 装饰者模式说明:
装饰者模式动态的将责任附加到对象上,如要扩展功能,装饰者提供了比继承更有弹性的替代方案。
5.现在来开始构建我们的代码吧
5.1 写奶茶茶底的代码
public interface BaseDrink {
/**
* 打印饮品描述
*/
String printDescription();
/**
* 获得当前饮品价格
*/
int getPrice();
}
public class BlackTea implements BaseDrink {
private final int price = 13; //定义红茶茶底的价格
private final String description = "红茶底"; //定义红茶茶底的描述
@Override
public String printDescription() {
return this.description;
}
@Override
public int getPrice() {
return this.price;
}
}
public class GreenTea implements BaseDrink {
private final int price = 12; //定义绿茶茶底的价格
private final String description = "绿茶底"; //定义绿茶茶底的描述
@Override
public String printDescription() {
return this.description;
}
@Override
public int getPrice() {
return this.price;
}
}
5.2 写小料的代码
public class Cheese implements BaseDrink {
private final int price = 4;//定义芝士的价格
private final String description = "加芝士";//定义芝士的描述
/**
* 要让Cheese对象拥有一个可以引用的BaseDrink对象,步骤如下:
* 1. 用一个实例变量记录奶茶,也就是被装饰者
* 2. 让被装饰者记录到实例变量中,这里的实现方式为通过构造器记录
*/
private BaseDrink baseDrink;
public Cheese(BaseDrink baseDrink){
this.baseDrink = baseDrink;
}
@Override
public String printDescription() {
return this.baseDrink.printDescription()+" "+this.description;
}
@Override
public int getPrice() {
return this.baseDrink.getPrice()+this.price;
}
}
// 奶豆腐,芋圆,珍珠的代码和芝士的代码基本一致,这里就不重复写入
5.3 卖奶茶咯
public class MilkTeaStore {
public static void main(String[] args) {
//需要一个绿茶奶茶,加一份珍珠和一份芝士
GreenTea greenTea = new GreenTea();
Pearl pearl = new Pearl(greenTea);
Cheese cheese = new Cheese(pearl);
System.out.println("您点了"+cheese.printDescription()+",总价为:"+cheese.getPrice());
//还需要一个红茶奶茶,加双份芋圆和一份奶豆腐
BlackTea blackTea = new BlackTea();
TaroBalls taroBalls1 = new TaroBalls(blackTea);
TaroBalls taroBalls2 = new TaroBalls(taroBalls1);
MilkTofu milkTofu = new MilkTofu(taroBalls2);
System.out.println("您点了"+milkTofu.printDescription()+",总价为:"+milkTofu.getPrice());
}
}
运行效果如下:
5.4 小问题
- 当我点了俩份芋圆时,打印出来的小票是
加芋圆 加芋圆
,如何通过装饰者模式,让他们变成加芋圆 x2
呢?- 奶茶都有小杯,中杯,大杯之分,三种分量的奶茶显然不能是同一种价格,所以如何通过装饰者模式来实现呢?
6. JDK中的装饰者模式
JDK中最出名的装饰者模式就在java.io包中,像BufferedInputStream和DataInputStream对象都扩展自FilterInputStream对象,而FileterInputStream对象时实现了InputStream接口的抽象的装饰对象。
6.1 编写属于自己的JAVA I/O对象
/**
* @author iverson
* @Package: decorate.test2
* @description: 小写流,将所有大写字母转为小写字母
* @date 2021/3/5 1:06
*/
public class LowerCaseInputStream extends FilterInputStream {
public LowerCaseInputStream(InputStream inputStream){
super(inputStream);
}
@Override
public int read() throws IOException {
int c = super.read();
return (c == -1? c : Character.toLowerCase((char)c));
}
}
public class main {
public static void main(String[] args) {
int c ;
try {
InputStream in = new LowerCaseInputStream(new BufferedInputStream(new FileInputStream("src/decorate/test2/test.txt")));
while ((c = in.read())>=0){
System.out.print((char)c);
}
}catch (IOException e){
e.printStackTrace();
}
}
}
运行图示:
7. 总结
装饰模式中的设计原则
- 开闭原则:对类的扩展开发,对修改关闭;在不修改现有代码的情况下,使类具有新的行为。
装饰模式的要点
- 装饰者和被装饰者类要具有相同的类型;
- 装饰者可以在被装饰者的行为前面或者后面加上自己的行为,甚至可以直接替换掉被装饰者的整个行为,从而达到特定的目的;
- 可以用无数个装饰者来装饰一个被装饰者;
- 装饰者会导致设计中出现许多的小对象,容易让程序复杂化。’
8. 问题简答
问题1:
- 调整价钱还是会较大幅度的调整我们的代码;
- 一旦有了新的小料,就需要在每个类中加上新的方法,还需要修改getPrice()方法
- 无法满足客户要两份同一个小料的需求;
- 有的奶茶不需要一些小料还是会被强制实现一些不必要的方法。
问题2:
- 我们可以写一个装饰类,用来解析最后返回的字符串(将printDescription()方法的返回值更改为ArrayList会让此方法更容易实现)
问题3:
- 同上。
欢迎各位提出您的意见或建议,您的关注和点赞是我更新的最大的动力!谢谢各位~