当我知道“小蓝杯”瑞幸咖啡前段时间(2019.6)在美国上市了,然后就去买了一杯试了下(首次免费),表示喝不习惯。今天我们就来研究下,怎么计算咖啡的金额。
我们知道,咖啡有很多种类,不同的种类售价是不一样的,基于我们面向对象的知识,非常自然的会抽象出来一个饮料基类(除了咖啡,其他饮料也包括进来了),然后提供一个抽象的计算价格的方法,具体的咖啡类实现自己的价格计算,基于此,我们来实现V1版本。
饮料基类
/**
* 饮料基类
*/
public abstract class Beverage {
private String description;
/**
* 计算价格方法
* @return
*/
public abstract double cost();
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
脱因咖啡实现
/**
* 脱因咖啡
*/
public class Decaf extends Beverage {
@Override
public double cost() {
return .82d;
}
}
深度烘焙咖啡实现
/**
* 深度烘焙咖啡
*/
public class DarkRoast extends Beverage {
@Override
public double cost() {
return 1.05d;
}
}
嗯,非常简单,一点不复杂。
然而,在购买咖啡时,也可以要求加入各种调料,如加奶、加糖、加摩卡(巧克力风味)等,有没有加盐的咖啡?额,没试过。
我们试试直接通过扩展子类的类别看看,简单的两种咖啡和三种调料会组合出来多少子类。加奶脱因咖啡,加糖脱因咖啡,加摩卡脱因咖啡,加奶加糖脱因咖啡,加奶加摩卡脱因咖啡,加糖加摩卡脱因咖啡,加奶加糖加摩卡脱因咖啡。同样,深度烘焙咖啡组合调料后也有7个子类。这样,两种咖啡混合三种调料,算上不加任何调料的类型,总共会有16种类型(2的4次方),
大家可以想象,如果新增一种咖啡,或者新增一种调料,子类会指数级增长,就问你怕不怕。
所以,直接扩展子类的方式肯定是不科学的,那要如何解决这种问题呢?既然继承不合适,那么按照组合/聚合原则,我们使用组合的方式来重新设计V2版,咖啡组合调料。
首先,我们将调料单独提取出来,抽象出一个调料基类,它也有价格和说明方法。
/**
* 调料基类
*/
public interface FlavouringV2 {
String getName(); //调料名称
float cost(); //调料价格
}
然后提供调料的具体实现,如奶、糖、摩卡。
/**
* 奶
*/
public class MilkV2 implements FlavouringV2{
@Override
public String getName() {
return "白白的牛奶";
}
@Override
public float cost() {
return 1.2f;
}
}
/**
* 糖
*/
public class SugarV2 implements FlavouringV2{
@Override
public String getName() {
return "甜甜的糖";
}
@Override
public float cost() {
return 2.15f;
}
}
/**
* 摩卡
*/
public class MochaV2 implements FlavouringV2{
@Override
public String getName() {
return "摩卡-巧克力风味";
}
@Override
public float cost() {
return 1.15f;
}
}
在饮料基类中组合调料,因为调料可能是0或者多个,所以使用调料的列表,在计算价格中,加入调料的价格计算。
/**
* 饮料基类
*/
public abstract class BeverageV2 {
private String description;
private List<FlavouringV2> flavourings; //调料列表
public BeverageV2() {
flavourings = new ArrayList<>();
}
/**
* 添加调料
* @param flavouring
*/
public void addFlavouring(FlavouringV2 flavouring){
flavourings.add(flavouring);
}
/**
* 计算调料价格
* @return
*/
public float costFlavouring(){
//将所有调料的价格汇总返回
return flavourings.stream().map(f -> f.cost()).reduce((c1,c2) -> c1+c2).orElse(0.0f);
}
/**
* 计算价格方法
* @return
*/
public abstract float cost();
public String getDescription() {
String flavouringNames = combineFlavouringNames();
String concatString = flavouringNames.isEmpty() ? ", " : ",加了 ";
return description + concatString + flavouringNames;
}
private String combineFlavouringNames() {
return flavourings.stream().map(f -> f.getName()).reduce((name1,name2) -> name1+","+name2).orElse("");
}
public void setDescription(String description) {
this.description = description;
}
}
具体的饮料类(咖啡),需要调用父类的计算调料价格的方法。
/**
* 脱因咖啡
*/
public class DecafV2 extends BeverageV2 {
public DecafV2() {
setDescription("脱因咖啡");
}
@Override
public float cost() {
return 6.82f + costFlavouring();
}
}
/**
* 深度烘焙咖啡
*/
public class DarkRoastV2 extends BeverageV2 {
public DarkRoastV2() {
setDescription("深度烘焙咖啡");
}
@Override
public float cost() {
return 9.05f + costFlavouring();
}
}
好了,我们来测试一波
/**
* V2版测试
*/
public class CafeMainV2 {
public static void main(String[] args) {
//点一杯脱因咖啡,什么都不加
BeverageV2 cafe1 = new DecafV2();
System.out.println(cafe1.getDescription() + " 需要" +cafe1.cost()+"元");
//点一杯脱因咖啡,加奶
BeverageV2 cafe2 = new DecafV2();
cafe2.addFlavouring(new MilkV2());
System.out.println(cafe2.getDescription() + " 需要" +cafe2.cost()+"元");
//点一杯脱因咖啡,加奶,加糖
BeverageV2 cafe3 = new DecafV2();
cafe3.addFlavouring(new MilkV2());
cafe3.addFlavouring(new SugarV2());
System.out.println(cafe3.getDescription() + " 需要" +cafe3.cost()+"元");
}
}
结果
脱因咖啡, 需要6.82元
脱因咖啡,加了 白白的牛奶 需要8.02元
脱因咖啡,加了 白白的牛奶,甜甜的糖 需要10.17元
妥妥的没问题。
V2版UML图
如果,现在添加一种咖啡饮料,只需要扩展新的饮料子类,它可以组合各种调料,而不需要修改任何其他类;
同理,若要新添加一种调料,只需要扩展新的调料子类,它可以被加入到各种饮料中,而不需要修改任何其他类,因此符合开闭原则。
这里再一次验证了设计原则的重要性,设计原则是内功,设计模式是招式。
上面这个UML图看起来像是组合了策略模式和观察者模式,发现这两种招式一融合,就创建一个新的招式,这个新的招式叫"桥接模式"。
哈哈,这个模式可不是今天要给大家介绍的。
现在,我们换一种思路来解决问题,看看今天的主角 装饰者模式 是怎么解决这个问题的。这就是V3版。
我们以饮料为主体,使用调料来装饰饮料,比如我们要点一杯加奶的脱因咖啡,那么首先我们有一个DecafV3对象,然后使用MilkDecorator对象去装饰它(包装),调用cost方法时,先调用外部装饰者的cost方法,它内部调用被装饰者的cost方法,会形成一条与装饰顺序相反的调用链,将价格累积起来。
看看装饰者模式实现的代码
饮料主体结构一样:
/**
* 饮料基类
*/
public abstract class BeverageV3 {
private String description;
/**
* 计算价格方法
* @return
*/
public abstract float cost();
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
/**
* 脱因咖啡
*/
public class DecafV3 extends BeverageV3 {
public DecafV3() {
setDescription("脱因咖啡");
}
@Override
public float cost() {
return .82f;
}
}
/**
* 深度烘焙咖啡
*/
public class DarkRoastV3 extends BeverageV3 {
public DarkRoastV3() {
setDescription("深度烘焙咖啡");
}
@Override
public float cost() {
return 1.05f;
}
}
饮料装饰者抽象,装饰者本身的类型要与饮料主体的类型一致,因此需要继承至饮料基类。同时其内部需要包装被装饰者对象。
/**
* 调料装饰者抽象
*/
public abstract class FlavouringDecoratorV3 extends BeverageV3 {
BeverageV3 beverage; //被装饰对象
public FlavouringDecoratorV3(BeverageV3 beverage) {
this.beverage = beverage;
}
public BeverageV3 getBeverage() {
return beverage;
}
public void setBeverage(BeverageV3 beverage) {
this.beverage = beverage;
}
@Override
public String getDescription() {
return "加" + super.getDescription() + ", " + beverage.getDescription();
}
}
具体装饰者,会调用被装饰者的方法。
/**
* 糖装饰者
*/
public class SugarDecoratorV3 extends FlavouringDecoratorV3{
public SugarDecoratorV3(BeverageV3 beverage) {
super(beverage);
setDescription("甜甜的糖");
}
@Override
public float cost() {
return 2.15f + beverage.cost();
}
}
/**
* 奶装饰者
*/
public class MilkDecoratorV3 extends FlavouringDecoratorV3{
public MilkDecoratorV3(BeverageV3 beverage) {
super(beverage);
setDescription("白白的奶");
}
@Override
public float cost() {
return 1.2f + beverage.cost();
}
}
测试走起
/**
* V3版测试
*/
public class CafeMainV3 {
public static void main(String[] args) {
//点一杯脱因咖啡,什么都不加
BeverageV3 cafe1 = new DecafV3();
System.out.println(cafe1.getDescription() + " 需要" +cafe1.cost()+"元");
//点一杯脱因咖啡,加奶
BeverageV3 cafe2 = new DecafV3();
FlavouringDecoratorV3 milkDecorator = new MilkDecoratorV3(cafe2);
System.out.println(milkDecorator.getDescription() + " 需要" +milkDecorator.cost()+"元");
//点一杯脱因咖啡,加奶,加糖
BeverageV3 cafe3 = new DecafV3();
milkDecorator = new MilkDecoratorV3(cafe3);
FlavouringDecoratorV3 sugarDecorator = new SugarDecoratorV3(milkDecorator);
System.out.println(sugarDecorator.getDescription() + " 需要" +sugarDecorator.cost()+"元");
}
}
输出结果
脱因咖啡 需要0.82元
加白白的奶, 脱因咖啡 需要2.02元
加甜甜的糖, 加白白的奶, 脱因咖啡 需要4.17元
就是这么优雅,就是这么艺术范。
如果要新增咖啡类型,只需要扩展饮料子类,它可以被各种调料装饰者装饰,不会修改现有其他类,同理,若要新增调料,只需要扩展调料装饰者子类,它就可以去装饰其他饮料类型,不会修改现有其他类,满足开闭原则。
理解了上面的这个例子,我们来看下装饰者的官方定义:
装饰者模式动态的将责任附加到对象上,若要扩展功能,装饰者提供了比继承更有弹性的替代方案。
实际上,装饰者模式这招,还是从设计原则(组合/聚合原则和依赖倒置原则)的内功创造出来的。内功是关键,什么时候达到无招胜有招,招式就可随手拈来。
我们用代码来实现下
抽象类型
/**
* 抽象组件
*/
public interface Component {
void methodA();
}
具体实现
/**
* 具体组件
*/
public class ConcreteComponent implements Component {
@Override
public void methodA() {
System.out.println("具体组件实现方法A");
}
}
装饰者抽象
/**
* 抽象装饰者
*/
public abstract class Decorator implements Component{
Component component; //被装饰对象
public Decorator(Component component) {
this.component = component;
}
}
具体装饰者
/**
* 具体装饰者A
*/
public class ConcreteDecoratorA extends Decorator {
public ConcreteDecoratorA(Component component) {
super(component);
}
@Override
public void methodA() {
System.out.println("具体装饰者A开始装饰.." );
component.methodA();
System.out.println("具体装饰者A装饰结束.." );
}
}
/**
* 具体装饰者B
*/
public class ConcreteDecoratorB extends Decorator {
public ConcreteDecoratorB(Component component) {
super(component);
}
@Override
public void methodA() {
System.out.println("具体装饰者B开始装饰.." );
component.methodA();
System.out.println("具体装饰者B装饰结束.." );
}
}
测试
/**
* 测试
*/
public class DecoratorMain {
public static void main(String[] args) {
Component component =
new ConcreteDecoratorB(new ConcreteDecoratorA(new ConcreteComponent()));
component.methodA();
}
}
输出
具体装饰者B开始装饰..
具体装饰者A开始装饰..
具体组件实现方法A
具体装饰者A装饰结束..
具体装饰者B装饰结束..
UML图
在JDK中,我们的I/O框架就大量使用了装饰者模式。
其中InputStream为抽象组件,FileInputStream、ByteArrayInputStream为具体组件。
FilterInputStream就是我们的装饰者父类了,BufferedInputStream、DataInputStream为实现了具体装饰功能的装饰者。
看看UML图(部分)
我们来试一下,扩展个小写装饰器,来对读取的字母装换成小写。
/**
* 小写装饰器
*/
public class LowerCaseInputStream extends FilterInputStream {
protected LowerCaseInputStream(InputStream in) {
super(in);
}
@Override
public int read() throws IOException {
int c = super.read();
return (c == -1 ? c : Character.toLowerCase(c));
}
}
测试
/**
* IO测试
*/
public class IOMain {
public static void main(String[] args) throws IOException {
InputStream is =
new LowerCaseInputStream(new BufferedInputStream(new StringBufferInputStream("THIS is The TEST text")));
int c ;
while((c = is.read()) > 0){
System.out.print((char)c);
}
}
}
按照惯例,我们继续来看一个示例,加深理解。示例来源:https://java2blog.com/decorator-design-pattern/
说,一个房间,我们可以通过颜色和窗帘去装饰它,当我使用不同装饰器的时候,看到的房间效果是不一样的。
废话少说,直接上代码。
抽象的组件(房间)
/**
* 房间
*/
public interface Room {
String showRoom();
}
具体组件(一个毛坯房)
/**
* 毛坯房
*/
public class SimpleRoom implements Room{
@Override
public String showRoom() {
return "毛坯房";
}
}
装饰者父类
/**
* 房间装饰器(装饰器父类)
*/
public class RoomDecorator implements Room {
//被装饰的房间
protected Room specialRoom;
public RoomDecorator(Room specialRoom) {
this.specialRoom = specialRoom;
}
@Override
public String showRoom() {
return specialRoom.showRoom();
}
}
颜色装饰器
/**
* 颜色装饰者
*/
public class ColorDecorator extends RoomDecorator{
public ColorDecorator(Room specialRoom) {
super(specialRoom);
}
@Override
public String showRoom() {
return specialRoom.showRoom() + addColors();
}
private String addColors() {
return " 被涂成蓝色 ";
}
}
窗帘装饰器
/**
* 窗帘装饰者
*/
public class CurtainDecorator extends RoomDecorator {
public CurtainDecorator(Room specialRoom) {
super(specialRoom);
}
@Override
public String showRoom() {
return specialRoom.showRoom() + addCurtains();
}
private String addCurtains() {
return " 挂上红色的窗帘 ";
}
}
测试
/**
* 测试
*/
public class DecoratorDesignPatternMain {
public static void main(String args[]) {
Room room = new CurtainDecorator(new ColorDecorator(new SimpleRoom()));
System.out.println(room.showRoom());
}
}
装饰效果:
毛坯房 被涂成蓝色 挂上红色的窗帘
一不小心就变成设计师了,真棒。
有没有感觉很清晰了。
装饰者模式这个招式,和后面我们要学习的适配器模式、代理模式的招式有异曲同工之妙(使用包装),所以理解了一个,对其他的也就非常容易理解了。
再次强调,招式不是关键,内功才是。
https://gitee.com/cq-laozhou/design-pattern