装饰者模式

装饰者模式

小蓝杯、谁不爱

当我知道“小蓝杯”瑞幸咖啡前段时间(2019.6)在美国上市了,然后就去买了一杯试了下(首次免费),表示喝不习惯。今天我们就来研究下,怎么计算咖啡的金额。

V1版本

我们知道,咖啡有很多种类,不同的种类售价是不一样的,基于我们面向对象的知识,非常自然的会抽象出来一个饮料基类(除了咖啡,其他饮料也包括进来了),然后提供一个抽象的计算价格的方法,具体的咖啡类实现自己的价格计算,基于此,我们来实现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版,咖啡组合调料。

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版本

现在,我们换一种思路来解决问题,看看今天的主角 装饰者模式 是怎么解决这个问题的。这就是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元

就是这么优雅,就是这么艺术范。

V3版UML图
在这里插入图片描述

如果要新增咖啡类型,只需要扩展饮料子类,它可以被各种调料装饰者装饰,不会修改现有其他类,同理,若要新增调料,只需要扩展调料装饰者子类,它就可以去装饰其他饮料类型,不会修改现有其他类,满足开闭原则。

定义

理解了上面的这个例子,我们来看下装饰者的官方定义:
装饰者模式动态的将责任附加到对象上,若要扩展功能,装饰者提供了比继承更有弹性的替代方案。

实际上,装饰者模式这招,还是从设计原则(组合/聚合原则和依赖倒置原则)的内功创造出来的。内功是关键,什么时候达到无招胜有招,招式就可随手拈来。

我们用代码来实现下
抽象类型

/**
 * 抽象组件
 */
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的装饰者

在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());
    }
}

装饰效果:

毛坯房 被涂成蓝色  挂上红色的窗帘 

一不小心就变成设计师了,真棒。

观察下UML图
在这里插入图片描述

有没有感觉很清晰了。

装饰者模式这个招式,和后面我们要学习的适配器模式、代理模式的招式有异曲同工之妙(使用包装),所以理解了一个,对其他的也就非常容易理解了。

再次强调,招式不是关键,内功才是。

源码

https://gitee.com/cq-laozhou/design-pattern

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值