装饰者模式动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案。

星巴兹是以扩张速度最快而闻名的咖啡连锁店。由于扩张速度太快,他们准备更新订单系统,以合乎他们的饮料供应要求。

他们原先的类设计是这样的:

// Beverage(饮料)是抽象的,店内所提供的饮料都必须继承自此类
public abstract class Beverage {
// 由每个子类设置,用来描述饮料,如“超优深焙咖啡豆”
protected String description;
public String getDescription(){
return description;
} // cost()方法是抽象的,子类必须实现cost()来返回饮料的价格
public abstract double cost();
}

购买咖啡时,也可以要求在其中加入各种调料,例如蒸奶、豆浆、摩卡或覆盖奶泡。星巴兹会根据所加入的调料收取不同的费用。所以订单系统必须考虑到这些调料部分。

订单系统的第一次尝试

public class HouseBlend extends Beverage{
@Override
public double cost() {
return 10.0;
}
} public class HouseBlendWithSteamedMilk extends Beverage{
@Override
public double cost() {
return 10.5;
}
} public class HouseBlendWithSteamedMilkandMocha extends Beverage{
@Override
public double cost() {
return 11.5;
}
}

很明显,星巴兹为自己制造了一个维护恶梦。如果牛奶的价格上涨,怎么办?新增一种焦糖调料风味时,怎么办?

利用实例变量和继承追踪这些调料

先从基类下手,加上实例变量代表是否加上调料(牛奶、豆浆、摩卡、奶泡…)

public abstract class Beverage {
protected String description;
// 各种调料的布尔值
private boolean milk;
private boolean soy;
private boolean mocha;
private boolean whip; public String getDescription() {
return description;
} // 现在,Beverage类中的cost()不再是一个抽象方法,
// 我们提供了cost()的实现,让它计算要加入各种饮料的调料价格。
// 子类仍将覆盖cost(),但是会调用超类的cost(),计算出基本饮料加上调料的价格。
public double cost() {
double cost = 0.0;
if (hasMilk()) {
cost += 1.0;
}
if (hasMocha()) {
cost += 2.5;
}
if (hasSoy()) {
cost += 1.5;
}
if (hasWhip()) {
cost += 2.0;
}
return cost;
} // 以下方法取得和设置调料的布尔值
public boolean hasMilk() {
return milk;
} public void setMilk(boolean milk) {
this.milk = milk;
} public boolean hasSoy() {
return soy;
} public void setSoy(boolean soy) {
this.soy = soy;
} public boolean hasMocha() {
return mocha;
} public void setMocha(boolean mocha) {
this.mocha = mocha;
} public boolean hasWhip() {
return whip;
} public void setWhip(boolean whip) {
this.whip = whip;
}
}

现在加入子类,每个类代表菜单上的一种饮料:

public class HouseBlend extends Beverage{
public HouseBlend(){
description = "HouseBlend";
} // 子类的cost()方法需要计算该饮料的价钱,然后通过调用超类的cost()实现,加入调料的价钱
@Override
public double cost() {
return 10.0 + super.cost();
}
}

以上设计的潜在问题

当哪些需求或因素改变时会影响这个设计?

  1. 调料价格的改变会使我们更改现有代码;
  2. 一旦出现新的调料,我们就需要加上新的方法,并改变超类中的cost()方法;
  3. 以后可能会开发出新饮料。对这些饮料而言(例如:冰茶),某些调料可能并不适合,但是在这个设计方式中,Tea(茶)子类仍将继承那些不适合的方法,例如:hasWhip()(加奶泡);
  4. 万一顾客想要双倍摩卡咖啡,怎么办?

继承与复用

尽管继承威力强大,但它并不总是能够实现最有弹性和最好维护的设计。

利用组合(composition)和委托(delegation)可以在运行时具有继承行为的效果。

利用继承设计子类的行为,是在编译时静态决定的,而且所有的子类都会继承到相同的行为。然而,如果能够利用组合的做法拓展对象的行为,就可以在运行时动态地进行扩展。可以利用此技巧把多个新职责,甚至是设计超类时还没有想到的职责加在对象上。而且,可以不用修改原来的代码。

通过动态地组合对象,可以写新的代码添加新功能,而无须修改现有代码。既然没有改变现有的代码,那么引进bug或产生意外副作用的机会将大幅度减少。

设计原则:类应该对扩展开放,对修改关闭。

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

认识装饰者模式

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

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

  1. 拿一个深焙咖啡(DarkRoast)对象;
  2. 以摩卡(Mocha)对象装饰它;
  3. 以奶泡(Whip)对象装饰它;
  4. 调用cost()方法,并依赖委托(delegate)将调料的价格加上去。

以装饰者模式构造饮料订单

  1. 以DarkRoast对象开始。

    DarkRoast继承自Beverage,且有一个用来计算饮料价钱的cost()方法。

  2. 顾客想要摩卡(Mocha),所以建立一个Mocha对象,并用它将DarkRoast对象包(wrap)起来。



    Mocha对象是个装饰者,它的类型“反映”了它所装饰的对象(本例中,就是Beverage)。所谓的“反映”,指的就是两者类型一致。

    所以Mocha也有一个cost()方法。通过多态,也可以把Mocha所包裹的任何Beverage当成是Beverage(因为Mocha是Beverage的子类型)。
  3. 顾客也想要奶泡(Whip),所以需要建立一个Whip装饰者,并用它将Mocha对象包起来。别忘了,DarkRoast继承自Beverage,且有一个cost()方法,用来计算饮料价钱。



    Whip是一个装饰者,所以它也反映了DarkRoast类型,并包括一个cost()方法。

    所以,被Mocha和Whip包起来的DarkRoast对象仍然是一个Beverage,仍然可以具有DarkRoast的一切行为,包括调用它的cost()方法。
  4. 现在,该是为顾客算钱的时候了。通过调用最外圈装饰者(Whip)的cost()就可以办得到。Whip的cost()会先委托它装饰的对象(也就是Mocha)计算出价钱,然后再加上奶泡的价钱。

定义装饰者模式

我们目前所知道的一切:

  1. 装饰者和被装饰者有相同的超类型;
  2. 你可以用一个或多个装饰者包装一个对象;
  3. 既然装饰者和被装饰者对象有相同的超类型,所以在任何需要原始对象(被包装的)的场合,可以用装饰过的对象代替它;
  4. 装饰者可以在所委托被装饰者的行为之前或之后,加上自己的行为,以达到特定的目的。
  5. 对象可以在任何时候被装饰,所以可以在运行时动态地、不限量地用你喜欢的装饰者来装饰对象。

    以下是实际应用装饰者模式的样例:
// 基础组件
public abstract class Component {
public void methodA(){
}
} // 我们将要动态地加上新行为的对象,它扩展自Component
public class ConcreteComponent extends Component {
public void methodA(){
}
} // 这是装饰者共同实现的接口
public abstract class Decorator extends Component {
public void methodA(){
}
} // 每个装饰者都“有一个”(包装一个)组件,也就是说,
// 装饰者有一个实例变量以保存某个Component的引用。
public class ConcreteDecoratorA extends Decorator {
// wrappedObj是装饰者包着的Component
Component wrappedObj;
public void methodA(){
}
}

装饰我们的饮料

// Beverage相当于抽象的Component类
public abstract class Beverage {
String description = "Unknow Beverage"; public String getDescription() {
return description;
} // cost()必须在子类实现
public abstract double cost();
} // 让Espresso扩展自Beverage类,因为Espresso是一种饮料(浓缩咖啡)
public class Espresso extends Beverage{
public Espresso(){
// 饮料的描述。description实例变量继承自Beverage
description = "Espresso";
} // 浓缩咖啡的价格
@Override
public double cost() {
return 10.0;
}
} // 另一种饮料
public class HouseBlend extends Beverage{
public HouseBlend(){
description = "HouseBlend";
} @Override
public double cost() {
return 10.0;
}
} // 必须让CondimentDecorator能取代Beverage,所以将CondimentDecorator扩展自Beverage类
public abstract class CondimentDecorator extends Beverage{
// 所有的调料装饰者都必须重新实现getDescription()方法
public abstract String getDescription();
} // 摩卡是一个装饰者,所以让它扩展自CondimentDecorator
public class Mocha extends CondimentDecorator{
// 要让Mocha能够引用一个Beverage,做法如下:
// 1.用一个实例变量记录饮料,也就是被装饰者
// 2.想办法让被装饰者(饮料)被记录到实例变量中。
// 这里的做法是:把饮料当作构造器的参数,再由构造器将此饮料记录在实例变量中
Beverage beverage; public Mocha(Beverage beverage){
this.beverage = beverage;
} // 我们希望叙述不只是描述饮料(例如“DarkRoast”),而是完整地连调料都描述出来(例如“DarkRoast, Mocha”)。
// 所以首先利用委托的做法,得到一个叙述,然后在其后加上附加的叙述(例如“Mocha”)
public String getDescription(){
return beverage.getDescription() + ", Mocha";
} @Override
public double cost() {
return 3.5 + beverage.cost();
}
} // 测试代码:
// 订一杯Espresso,不需要调料,打印出它的描述与价钱
Beverage beverage = new Espresso();
System.out.println(beverage.getDescription() + " $" + beverage.cost()); // 制造出一个DarkRoast对象
Beverage beverage2 = new DarkRoast();
// 用Mocha装饰它
beverage2 = new Mocha(beverage2);
// 用第二个Mocha装饰它
beverage2 = new Mocha(beverage2);
// 用Milk装饰它
beverage2 = new Milk(beverage2);
System.out.println(beverage2.getDescription() + " $" + beverage2.cost());

《Head first设计模式》之装饰者模式的更多相关文章

  1. Java 设计模式泛谈&装饰者模式和单例模式

    设计模式(Design Pattern) 1.是一套被反复使用.多人知晓的,经过分类编目 的 代码设计经验总结.使用设计模式是为了可重用代码,让代码更容易维护以及扩展. 2.简单的讲:所谓模式就是得到 ...

  2. C#设计模式(9)——装饰者模式(Decorator Pattern)

    一.引言 在软件开发中,我们经常想要对一类对象添加不同的功能,例如要给手机添加贴膜,手机挂件,手机外壳等,如果此时利用继承来实现的话,就需要定义无数的类,如StickerPhone(贴膜是手机类).A ...

  3. 设计模式之装饰者模式-java实例

    设计模式之装饰者模式 需求场景 我们有了别人提供的产品,但是别人提供的产品对我们来说还不够完善,我们需要对这个产品的功能进行补强,此时可以考虑使用装饰者模式. 我们已经有了产品,而且这个产品的功能非常 ...

  4. Java设计模式 - - 单例模式 装饰者模式

    Java设计模式 单例模式 装饰者模式 作者 : Stanley 罗昊 [转载请注明出处和署名,谢谢!] 静态代理模式:https://www.cnblogs.com/StanleyBlogs/p/1 ...

  5. python 设计模式之装饰器模式 Decorator Pattern

    #写在前面 已经有一个礼拜多没写博客了,因为沉醉在了<妙味>这部小说里,里面讲的是一个厨师苏秒的故事.现实中大部分人不会有她的天分.我喜欢她的性格:总是想着去解决问题,好像从来没有怨天尤人 ...

  6. PHP设计模式之装饰器模式(Decorator)

    PHP设计模式之装饰器模式(Decorator) 装饰器模式 装饰器模式允许我们给一个类添加新的功能,而不改变其原有的结构.这种类型的类属于结构类,它是作为现有的类的一个包装 装饰器模式的应用场景 当 ...

  7. 实践GoF的23种设计模式:装饰者模式

    摘要:装饰者模式通过组合的方式,提供了能够动态地给对象/模块扩展新功能的能力.理论上,只要没有限制,它可以一直把功能叠加下去,具有很高的灵活性. 本文分享自华为云社区<[Go实现]实践GoF的2 ...

  8. Java 的设计模式之一装饰者模式

    刚开始接触装饰者的设计模式,感觉挺难理解的,不够后来花了一个晚上的时间,终于有头绪了 装饰者设计模式:如果想对已经存在的对象进行装饰,那么就定义一个类,在类中对已经有的对象进行功能的增强或添加另外的行 ...

  9. Head First 设计模式 --3 装饰者模式 开闭原则

    装饰者模式:动态的将责任附加到对象上,若要扩展功能,装饰者提供了比集成更有弹性的替代方案.设计原则:1:封装变化2:多用组合,少用继承3:针对接口编程,不针对实现编程4:为对象之间的松耦合设计而努力5 ...

  10. [设计模式] 9 装饰者模式 Decorator

    转:http://www.jellythink.com/archives/171#prettyPhoto 什么是装饰模式? 在GOF的<设计模式:可复用面向对象软件的基础>一书中对装饰模式 ...

随机推荐

  1. 输入n个学生,并且输入成绩,判断是否偏科

    H学校的领导主任决定分析一下今年所有N名学生的考试成绩,从中找出偏科的学生,考试成绩包含语文,数学,英语三门课程的分数,已知偏科的定义是:某一门课程的分数大于等于90,并且另外两门的分数小于等于70. ...

  2. [C++]最小生成树

    1. 最小生成树定义 树是指没有环路的图,生成树就是指一个图上面删除一些边,使它没有环路. 最小生成树就是指生成树中边权之和最小的那一种. 上图的最小生成树就是这样: 2. Prim 算法 2.1. ...

  3. Webpack实战(四):教教你如何轻松搞定-预处理器(loader)

    前面三节,我主要给大家分享了有关webpack的一些配置的知识点,如何打包js文件,而如果我们遇到其他类型的资源如图片.css.字体font等等,我们该如何处理呢?今天会介绍预处理器(loader), ...

  4. IDEA使用 磨刀霍霍向代码

    工欲善其事,必先利其器 ,当下有数不清的 Java 程序员将石器时代的 Eclipse 替换成了现代化的智能开发工具 InteliJ IDEA ,写代码的小日子过得不亦乐乎(玩笑话,两者各有千秋,看个 ...

  5. Ubuntu16手动安装OpenStack

    记录大佬的博客全文转载于https://www.voidking.com/dev-ubuntu16-manual-openstack-env/ 前言 <Ubuntu16安装OpenStack&g ...

  6. 异想家IDEA的偏好配置

    最好将配置文件位置改为软件安装目录下,因为只有自己用,易于便携. 修改bin目录下的idea.properties,注释#去掉修改idea.config.path.idea.system.path配置 ...

  7. linux C++类中成员变量和函数的使用

    1.undefined reference to XXX 问题原因 1)XXX所在的so库等未指定 2)XXX在类中实现的时候没有加上类::函数的格式 2. was not declared in t ...

  8. Day8-Python3基础-Socket网络编程

    目录: 1.Socket语法及相关 2.SocketServer实现多并发 Socket语法及相关 socket概念 socket本质上就是在2台网络互通的电脑之间,架设一个通道,两台电脑通过这个通道 ...

  9. Shell常用命令之ip

    前言 linux的ip命令和ifconfig类似,但前者功能更强大,并旨在取代后者.使用ip命令,只需一个命令,你就能很轻松地执行一些网络管理任务.ifconfig是net-tools中已被废弃使用的 ...

  10. YYC松鼠短视频系统上传视频会被压缩的问题如何解决?

    uni.chooseVideo({ count: 1, compressed: false, sourceType: ['album', 'camera'], success: (res) => ...