装饰者模式(decorator)
文章说明
- 该文章为
《Head First 设计模式》
的学习笔记 - 非常推荐大家去买一本
《Head First 设计模式》
去看看,节奏轻松愉悦,讲得通俗易懂,非常适合想要学习、了解、应用设计模式以及OO设计原则的小白。
提出问题
- 现在星巴兹咖啡连锁店扩展得非常迅速,他们原来的类设计如下:
- 在购买咖啡时,可以加入各种调料,例如:摩卡(Mocha)、豆浆(Soy)、牛奶(Milk)等。根据所加入的调料收取不同的费用。
- 该怎么设计一个富有弹性的OO系统呢?
有可能出现的需求
- 调料价钱改变
- 出现新的调料
- 开发出新的饮料
- 顾客要双倍摩卡咖啡,怎么办?
- 饮料打折
- 星巴兹决定将咖啡的容量分为大、中、小杯
- 等等~~
认识装饰者模式
以饮料为主体,然后在运行时以调料来"装饰"饮料。
比方说,如果顾客想要摩卡和牛奶浓缩咖啡:
- 拿一个
浓缩咖啡(Espresso)
对象 - 以
摩卡(Mocha)
对象装饰它 - 以
牛奶(Milk)
对象装饰它 - 调用
cost()
方法,并依赖委托(delegate)
将调料的价钱加上去
- 双倍牛奶的浓缩咖啡的装饰如图:
模式的定义
动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案。
涉及的OO设计原则
- 封装变化
- 多用组合,少用继承
- 针对接口编程,不针对实现编程
- 为交互对象之间的松耦合设计而努力
- 对扩展开放,对修改关闭
开放-关闭原则
最重要的设计模式之一:
- 类应该对扩展开放,对修改关闭
要点
- 我们的目标是允许类容易扩展,在不修改现有代码的情况下,就可以搭配新的行为。这样的设计具有弹性可以应对改变,可以接受新的功能来应对改变的需求。
- 在选择需要被扩展的代码部分时要小心。每个地方都采用开放-关闭原则,是一种浪费,也没必要,还会导致代码变得复杂。
类图
要点
- 装饰者和被装饰者必须是一样的类型,也就是有共同的超类
- 我们利用继承达到"类型匹配",而不是用继承获得行为
- 新行为是由组合对象得来的
- 继承Beverage抽象类,是为了有正确的类型,而不是继承他的行为。行为来自
装饰者(调料)
和基础组件(饮料)
,或与其他装饰者之间的组合关系 - 如果依赖继承,那么类的行为只能在编译时静态决定
部分代码的展示
抽象的组件:Beverage.java
/**
* 饮料类
* 抽象的组件
*/
public abstract class Beverage {
String description = "Unknown Beverage";
public String getDescription() {
return description;
}
public abstract double cost();
}
抽象的装饰者:CondimentDecorator.java
/**
* 调料类
* 抽象装饰者
*/
public abstract class CondimentDecorator extends Beverage {
@Override
public abstract String getDescription();
}
具体的组件们:DarkRoast.java, HouseBlend.java, Espresso.java
/**
* 具体的组件们
* 现在不需要管调料的价格
*/
public class DarkRoast extends Beverage {
public DarkRoast() { // 深度烘焙咖啡类
description = "Dark Roast Coffee";
}
@Override
public double cost() { return .99; }
}
public class Espresso extends Beverage {
public Espresso() { // 浓缩咖啡类
description = "Espresso";
}
@Override
public double cost() { return 1.99; }
}
public class HouseBlend extends Beverage {
public HouseBlend() { // 综合咖啡类
description = "House Blend Coffee";
}
@Override
public double cost() { return .89; }
}
具体的装饰者:Mocha.java, ...
这里只展示一了个调料
/**
* 摩卡调料类
* 具体的装饰者
*/
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();
}
}
测试类:StarbuzzCoffee.java
public class StarbuzzCoffee {
public static void main(String[] args) {
// 不加调料的深度烘焙咖啡
Beverage beverage1 = new DarkRoast();
System.out.println(beverage1.getDescription() +
", price: $" + beverage1.cost());
// 加豆浆的综合咖啡
Beverage beverage2 =
new Soy(new HouseBlend());
System.out.println(beverage2.getDescription() +
", price: $" + beverage2.cost());
// 加双份摩卡和牛奶的浓缩咖啡
Beverage beverage3 =
new Mocha(new Mocha(new Milk(new Espresso())));
System.out.println(beverage3.getDescription() +
", price: $" + beverage3.cost());
}
}
运行结果:
Dark Roast Coffee, price: $0.99
House Blend Coffee, Soy, price: $1.04
Espresso, Milk, Mocha, Mocha, price: $2.49
扩展
怎么实现之前所说,当星巴兹决定在菜单上加上咖啡的容量大小,顾客可以选择小杯子(tall)、中杯(grande)、大杯(venti),也希望调料根据咖啡容量收费。
- 首先对
Beverage
类进行改造,添加size
字段并且实现setSize()
和setSize()
方法,像这样:
public abstract class Beverage {
// 只展现新加入的代码
public enum Size { TALL, GRANDE, VENTI };
Size size = Size.TALL;
public void setSize(Size size) {
this.size = size;
}
public Size getSize() { return size; }
}
- 然后对组件即饮料进行改造,修改
cost()
方法,根据beverage.getSize()
字段调整饮料价格
public double cost() {
double cost = beverage.cost();
if (beverage.getSize() == Size.TALL)
cost += .10;
else if (beverage.getSize() == Size.GRANDE)
cost += .15;
else if (beverage.getSize() == Size.VENTI)
cost += .20;
return cost;
}
装饰者模式:Java I/O
类图
- 可以任意组合你想要的
I/O流
,也可以编写自己的I/O流
做为装饰者,可见其可扩展性以及强大的功能 - 缺点:设计中有大量的小类,数量庞大,可能对使用该API的程序员造成困扰,至少我该开始接触是这样的," 这是俄罗斯套娃吗? "
黑暗面
除了上面所说的可能会对不熟悉装饰者模式的程序员带来困扰外,还有类型问题
- 装饰者的优点是,可以透明地插入装饰者,客户端程序甚至都不知道它是在装饰者打交道。有些代码会依赖特定的类型,而这样的代码已导入装饰者,嘭!出状况了。
- 还有一个问题,在实例化组件的时候,增加了代码的复杂度。一旦使用装饰者模式,不只需要实例化组件,还要把此组件包装进装饰者中,繁琐。
好消息
好消息是工厂(Factory)模式
和生成器(Builder)模式
对第二个问题即(实例化组件让代码变得复杂)有很大的帮助。