博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
HeadFirst设计模式(三) - 装饰者模式
阅读量:6188 次
发布时间:2019-06-21

本文共 9883 字,大约阅读时间需要 32 分钟。

hot3.png

装饰对象

    我们即将讨论典型的继承滥用问题。并学到如何使用对象组合的方式,做到在运行时装饰类。

    用一个简单的需求来描述问题,星巴兹(Starbuzz)需要准备订单系统,这是他们的第一个尝试,类设计是这样的:

    Beverage(饮料)是一个抽象类,店内所提供的饮料都必须继承自此类,这个类有一个description(描述)的实例变量,可以由每个子类设置,用来描述饮料,例如:超优深培咖啡豆(Dark Roast)等。cost()方法是抽象的,子类必须定义自己的实现。每个子类都实现cost()来返回饮料的价钱。购买咖啡时,也可以要求在其中加入各种调料,例如:蒸奶、豆浆、摩卡或覆盖奶泡。咖啡店会根据加入的调料收取不同的费用。

233239_Loz8_2450666.png

    代码与客户端调用如下:

package cn.net.bysoft.decorator;// 饮料的基类。public abstract class Beverage {    // 计算价格的方法。    public abstract double cost();    // 饮料描述的getter and setter。    public String getDescription() {        return description;    }    public void setDescription(String description) {        this.description = description;    }    // 饮料的描述。    private String description = "";}
package cn.net.bysoft.decorator;// 深焙咖啡对象。public class DarkRoast extends Beverage {    @Override    public double cost() {        // 牛奶 + 糖 + 深焙咖啡豆 = 1$         return 1.00D;    }}
package cn.net.bysoft.decorator;// 浓缩咖啡对象。public class Espresso extends Beverage {    @Override    public double cost() {        // 浓缩咖啡价格是1.15$。        return 1.15D;    }    }
package cn.net.bysoft.decorator;public class Client {    public static void main(String[] args) {        // 来一杯浓缩咖啡。        Beverage espresson = new Espresso();        System.out.println("浓缩咖啡的价格是:" + espresson.cost() + "$");                // 来一杯深焙咖啡。        Beverage darkRoast = new DarkRoast();        System.out.println("深焙咖啡的价格是:" + darkRoast.cost() + "$");                /**         * output:         * 浓缩咖啡的价格是:1.15         * 深焙咖啡的价格是:1.0         * */    }}

    很明显,咖啡店为自己制造了一个维护的噩梦,看类图,假如咖啡店有82种咖啡,如果牛奶的价钱上扬,需要改动所有与牛奶有关的咖啡的价格,如果咖啡豆价格上调的话……如果新增一种所有饮料都试用的焦糖风味,所有类都要改变……

    这时候,有些人已经想到了解决办法,利用实例变量与继承,就可以追踪这些调料。

233350_7yrp_2450666.png

    好吧,就来试试看。先从Beverage基类下手,加上实例变量代表是否加上调料,现在,Beverage类中的cost()不再是一个抽象方法,我们提供了cost()的实现,让他计算要加入各种饮料的调料价钱。子类仍覆盖cost(),但是会调用超类的cost(),计算出基本饮料加上调料的价钱。看一下代码如何实现:

package cn.net.bysoft.decorator;// 饮料的基类。public abstract class Beverage {    // 基类计算所有调料的价钱。    public double cost() {        double price = 0.0D;        if (hasMilk()) {            price += 0.2D;        }        if (hasSoy()) {            price += 0.2D;        }        if (hasMocha()) {            price += 0.3D;        }        if (hasWhip()) {            price += 0.3D;        }        return price;    }    // 饮料描述的getter and setter。    public String getDescription() {        return description;    }    public boolean hasMilk() {        return milk;    }    public void setMilk(boolean milk) {        this.milk = milk;    }        // getter and setter...    // 饮料的描述。    private String description = "";    private boolean milk = false;    private boolean soy = false;    private boolean mocha = false;    private boolean whip = false;}
package cn.net.bysoft.decorator;// 深焙咖啡对象。public class DarkRoast extends Beverage {    @Override    public double cost() {        // 深焙咖啡使用牛奶。        double price = 0.0D;        super.setMilk(true);        // 计算了牛奶的价钱,在加上深焙咖啡的原料。        // 0.2 + 1.05 = 1.25;        price = super.cost() + 1.05;        return price;    }}
package cn.net.bysoft.decorator;public class Client {    public static void main(String[] args) {        // 来一杯深焙咖啡。        Beverage darkRoast = new DarkRoast();        System.out.println("深焙咖啡的价格是:" + darkRoast.cost() + "$");                /**         * output:         * 深焙咖啡的价格是:1.25$         * */    }}

    这种做法,非常容易的就解决的前一种设计的问题,咖啡店很满意,开始使用修改后的订单系统。

    过了一阵子,发现这种设计并不能满足日常应用,比如:

  • 调料价格的改变还是要修改现有代码;

  • 一旦出现新的调料,就需要加上新的hasXXX方法,并改变超类中的cost()方法;

  • 以后可能会开发出新饮料,对于这些饮料(例如冰茶等),某些调料可能并不合适,但是在这个设计方式汇总,Tea(茶)子类仍然继承哪些不合适的方法,比如hasWhip(加奶泡);

  • 万一顾客想要双倍的摩卡或者双倍的糖怎么办;

    此刻,就面临了最重要的设计原则之一:

设计原则

类应该对扩展开放,对修改关闭。

    我们的目标是允许类容易扩展,在不修改现有代码的情况下,就可以配置新的行为。如能实现这样的目标,有什么好处呢?这样的设计具有弹性可以应对改变,可以接受新的功能来应对需求变更。

认识装饰者模式

    好了,我们已经了解到利用继承无法完全解决问题,在咖啡馆遇到的问题有:类数量爆炸、设计死板,以及基类加入的新功能并不适用于所有的子类。

    所以,在这里要采用不一样的做法:我们要以饮料为主题,然后在运行时以调料来“装饰”(decorate)饮料。比方说,如果顾客想要摩卡和奶泡的深培咖啡,那么,要做的是:

  1. 拿一个深培咖啡(DarkRoast)对象;

  2. 以摩卡(Mocha)对象装饰它;

  3. 以奶泡(WHip)对象装饰它;

  4. 调用cost()方法,并依赖委托(delegate)将调料的价钱加上去;

    这样就完工了一个深培咖啡对象,但是如何“装饰”一个对象,而“依赖委托”又要如何与此搭配使用呢?

  • 装饰者(调料)和被装饰者(饮料)都有相同的超类;

  • 可以用一个或多个装饰者(调料)包装一个被装饰者(饮料);

  • 既然装饰者和被装饰者都有相同的超类,那么在任何需要原始对象,也就是被装饰者(饮料)的场合,都可以使用装饰过的对象(饮料)代替它;

  • 装饰者可以在所委托的被装饰者的行为之前或之后,加上自己的行为,以达到特定的目的;

  • 对象可以在任何时候被装饰,所以可以在运行时动态地、不限量地用你喜欢的装饰者来装饰对象;

    现在,就来卡看装饰者模式的定义,并写一些代码,了解它到底是怎么工作的吧!先来看看装饰者模式的类图:

210430_1WYF_2450666.png

  • Component(组件)类:每个组件都可以单独使用,或者被装饰者包装起来使用。

  • ConcreteComponent(被装饰组件)类:我们将要动态地加上新行为的对象,它拓展自Component类。

  • Decorator(装饰)类:每一个装饰者都“有一个”(包装一个)组件,也就是说,装饰者有一个实例变量以保存某个Component的引用。

  • ConcreteDecoratorXX类:有一个实例变量可以记录所装饰的事物,还可以加上新的方法。

    现在,让咖啡店的订单系统也符合此框架:

211304_BVT6_2450666.png

    左侧4个类HouseBlend、DarkRoast、Decaf、Espresso是具体的组件类,而右下角的4个类Milk、Mocha、Soy、Whip是具体的装饰者类,这么一看并没有感觉到装饰者有多厉害,让我们用代码来实现以后在看看效果,现在的需求为:“来一杯双倍摩卡豆浆奶泡拿铁咖啡”,使用订单系统得到正确的价钱:

package cn.net.bysoft.decorator;// 饮料的基类。public abstract class Beverage {    // 饮料的说明。    String description = "Unknow Beverage";    public String getDescription() {        return description;    }        public abstract double cost();}

    第一个与我们见面的类是饮料的基类,有两个方法,分别是getDescription()和cost(),用来返回饮料说明和价格。

package cn.net.bysoft.decorator;// 调料对象。public abstract class CondimentDecorator extends Beverage {    // 饮料描述。    public abstract String getDescription();}

    第二个出现的类是调料的抽象类,首先,必须让该类能取代Beverage,所以继承自Beverage类。

    它的方法getDescription()必须让所有的调料类都实现。

    现在,基类已经有了,开始实现一些饮料吧!先从浓缩咖啡开始。

package cn.net.bysoft.decorator;// 浓缩咖啡对象。public class Espresso extends Beverage {    public Espresso() {        super.description = "Espresso Coffee";    }    @Override    public double cost() {        return 1.99D;    }}

    首先,让Espresso拓展自Beverage类,因为浓缩咖啡也是一种饮料。然后设置浓缩咖啡的说明属性description,最后使用cost()方法返回价格。

    再实现一个黑咖啡(HouseBlend)和深焙咖啡(DarkRoast),代码同上,其余的饮料都一样。

package cn.net.bysoft.decorator;// 黑咖啡对象。public class HouseBlend extends Beverage {    public HouseBlend() {        super.description = "House Blend Coffee";    }    @Override    public double cost() {        return .89;    }}
package cn.net.bysoft.decorator;// 深焙咖啡对象。public class DarkRoast extends Beverage {    public DarkRoast() {        super.description = "DarkRoast Coffee";    }    @Override    public double cost() {        return .89;    }}

    写好具体的饮料对象后,就可以着手调料类的编写了:

package cn.net.bysoft.decorator;public class Mocha extends CondimentDecorator {    // 需要装饰的类。    Beverage beverage;    public Mocha(Beverage beverage) {        this.beverage = beverage;    }    @Override    public String getDescription() {        return beverage.getDescription() + ", Mocha";    }    @Override    public double cost() {        return .20 + beverage.cost();    }}

    摩卡(Mocha)是一个装饰者,拓展自CondimentDecorator类,也就是说,摩卡也是一个Beverage类。

    摩卡对象中有一个Beverage对象用于存放要封装的具体饮料类,目前有Espresso和HouseBlend两个饮料。

    在构造函数中把具体饮料当作参数传递到Mocha中。

    在描述时,不只是调用传递进来的饮料的描述,还可以加入自己想要加入的描述(例如“DarkRoast, Mocha”)。

    最后cost()方法,首先调用传递进来的饮料的cost()方法,获得价格,在加上Mocha自己的钱,得到最终结果。

    在最后测试之前,实现奶泡条件,完成需求:

package cn.net.bysoft.decorator;// 奶泡调料public class Whip extends CondimentDecorator {    // 需要装饰的类。    Beverage beverage;    public Whip(Beverage beverage) {        this.beverage = beverage;    }    @Override    public String getDescription() {        return beverage.getDescription() + ", Whip";    }    @Override    public double cost() {        return .20 + beverage.cost();    }}

    下面就是使用订单的一些测试代码:

package cn.net.bysoft.decorator;public class Client {    public static void main(String[] args) {        // 订一杯浓缩咖啡,不需要调料,打印价格。        Beverage beverage = new Espresso();        System.out.println(beverage.getDescription() + " $" + beverage.cost());        System.out.println();        // 订一杯深焙咖啡,加入双倍的Mocha,在加入奶泡。        // 因为调料与饮料都是扩展自Beverage,所以可以使用下面的等式。        Beverage beverage2 = new DarkRoast();        beverage2 = new Mocha(beverage2);        beverage2 = new Mocha(beverage2);        beverage2 = new Whip(beverage2);        System.out                .println(beverage2.getDescription() + " $" + beverage2.cost());        System.out.println();        /**         * output:          * Espresso Coffee $1.99         *          * DarkRoast Coffee, Mocha, Mocha, Whip $1.49         * */    }}

    再来拓展一下需求,现在咖啡店决定开始在菜单上为所有饮料加入容量,提供小杯(Tall)、中杯(Grande)、大杯(Venti)饮料,每次加入摩卡和奶泡不是按照固定的0.20美金收费了,而是按照,小中大杯的咖啡加摩卡和奶泡时,判断size,然后分别加收0.1、0.15、0.2美金。

    如何改变装饰者类对应这样的需求呢?

    首先,在饮料基类中加入SIZE属性:

public enum BeverageSize {    TALL, GRANDE, VENTI}
public abstract class Beverage {    BeverageSize size = BeverageSize.TALL;    public void setSize(BeverageSize size) {        this.size = size;    }    public String getDescription() {        return description;    }        ...}

    然后,修改摩卡和奶泡的cost方法,修改之前将摩卡和奶泡类中的Beverage提取到基类CondimentDecorator中:

// 调料对象。public abstract class CondimentDecorator extends Beverage {    Beverage beverage;        public BeverageSize getSize() {        return beverage.getSize();    }    ...}
package cn.net.bysoft.decorator;public class Mocha extends CondimentDecorator {    public Mocha(Beverage beverage) {        super.beverage = beverage;    }    @Override    public double cost() {        double cost = beverage.cost();        if (getSize() == BeverageSize.TALL) {            cost += .10;        } else if (getSize() == BeverageSize.GRANDE) {            cost += .15;        } else if (getSize() == BeverageSize.VENTI) {            cost += .20;        }        return cost;    }    ...}

    奶泡的类,与摩卡类相同,最后进行测试:

public static void main(String[] args) {        // 订一杯深焙咖啡,加入双倍的Mocha,在加入奶泡。        Beverage beverage2 = new DarkRoast();        // 大杯饮料,每种调料0.15美金。        beverage2.setSize(BeverageSize.GRANDE);        beverage2 = new Mocha(beverage2);        beverage2 = new Mocha(beverage2);        beverage2 = new Whip(beverage2);        // 0.89+0.15+0.15+0.15 = 1.34        System.out.println(beverage2.getDescription() + " $"                + String.format("%.2f", beverage2.cost()));        System.out.println();        /**         * output: DarkRoast Coffee, Mocha, Mocha, Whip $1.34         * */    }

    以上就是装饰者模式的全部内容。

    另外,java.io包内的类大量的使用了装饰者模式,比如:

    FileInputStrame被BufferedInputStream装饰。

    而BufferedInputStream又被LineNumberInputStream装饰。

    具体的细节可以查看java的api和源码。

转载于:https://my.oschina.net/u/2450666/blog/662610

你可能感兴趣的文章
【对讲机的那点事】公网对讲关键指标之组呼建立时延
查看>>
SignalR主动通知订阅者示例
查看>>
CUBA Platform 7.0.3 发布,企业级应用开发平台
查看>>
MRoot 2.2 发布,全新 UI 界面,更好的集群
查看>>
vim python一键执行、高亮等一键安装
查看>>
《致云雀》(英)雪莱
查看>>
nginx 安装
查看>>
运用hanlp 通过 python 结合jpype 导出依存句法可视化
查看>>
报表的查询条件只能在数据上方吗?
查看>>
Python第三方库使用感言
查看>>
WPF中自定义MarkupExtension
查看>>
MySQL 8.0.12 基于Windows 安装教程
查看>>
Ansible文件内容修改lineinfile模块(学习笔记五)
查看>>
使用.NET Core搭建分布式音频效果处理服务(一)需求、问题和解决方案的几个坑...
查看>>
“ji32k7au4a83”被用作密码的次数不太正常
查看>>
Confluence 6 安全相关问题提交链接
查看>>
关于mvn install命令执行报错问题
查看>>
数据库事务特征、数据库隔离级别,以及各级别数据库加锁情况(含实操)--read uncommitted篇...
查看>>
职场 | 算法是怎样决定你的职业生涯的
查看>>
Android重拾设计模式系列——简单工厂模式
查看>>