装饰者模式

文章说明

  • 该文章为《Head First 设计模式》的学习笔记
  • 非常推荐大家去买一本《Head First 设计模式》去看看,节奏轻松愉悦,讲得通俗易懂,非常适合想要学习、了解、应用设计模式以及OO设计原则的小白。

提出问题

  • 现在星巴兹咖啡连锁店扩展得非常迅速,他们原来的类设计如下:
«abstract» Beverage description getDescription() cost() HouseBlend cost() DarkRoast cost() Espresso cost()
  • 在购买咖啡时,可以加入各种调料,例如:摩卡(Mocha)、豆浆(Soy)、牛奶(Milk)等。根据所加入的调料收取不同的费用。
  • 该怎么设计一个富有弹性的OO系统呢?

有可能出现的需求

  • 调料价钱改变
  • 出现新的调料
  • 开发出新的饮料
  • 顾客要双倍摩卡咖啡,怎么办?
  • 饮料打折
  • 星巴兹决定将咖啡的容量分为大、中、小杯
  • 等等~~

认识装饰者模式

以饮料为主体,然后在运行时以调料来"装饰"饮料
比方说,如果顾客想要摩卡和牛奶浓缩咖啡:

  1. 拿一个浓缩咖啡(Espresso)对象
  2. 摩卡(Mocha)对象装饰它
  3. 牛奶(Milk)对象装饰它
  4. 调用cost()方法,并依赖委托(delegate)将调料的价钱加上去
  • 双倍牛奶的浓缩咖啡的装饰如图:
    双倍牛奶的浓缩咖啡

模式的定义

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

涉及的OO设计原则

  • 封装变化
  • 多用组合,少用继承
  • 针对接口编程,不针对实现编程
  • 为交互对象之间的松耦合设计而努力
  • 对扩展开放,对修改关闭

开放-关闭原则

最重要的设计模式之一:

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

要点

  • 我们的目标是允许类容易扩展,在不修改现有代码的情况下,就可以搭配新的行为。这样的设计具有弹性可以应对改变,可以接受新的功能来应对改变的需求。
  • 在选择需要被扩展的代码部分时要小心。每个地方都采用开放-关闭原则,是一种浪费,也没必要,还会导致代码变得复杂

类图

«abstract» Beverage description getDescription() cost() HouseBlend cost() DarkRoast cost() Espresso cost() «abstract» CondimentDecorator getDescription() Milk cost() getDescription() Mocha Beverage bevaege cost() getDescription() Soy Beverage bevaege cost() getDescription() is-a && component

要点

  • 装饰者被装饰者必须是一样的类型,也就是有共同的超类
  • 我们利用继承达到"类型匹配",而不是用继承获得行为
  • 新行为是由组合对象得来的
  • 继承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

类图

«abstract» InputStream FileInputStream StringBufferInputStream ByteArrayInputStream FilterInputStream DataInputStream LineNumberInputStream BufferedInputStream PushbackInputStream
  • 可以任意组合你想要的I/O流,也可以编写自己的I/O流做为装饰者,可见其可扩展性以及强大的功能
  • 缺点:设计中有大量的小类数量庞大,可能对使用该API的程序员造成困扰,至少我该开始接触是这样的," 这是俄罗斯套娃吗? "

黑暗面

除了上面所说的可能会对不熟悉装饰者模式的程序员带来困扰外,还有类型问题

  • 装饰者的优点是,可以透明地插入装饰者,客户端程序甚至都不知道它是在装饰者打交道。有些代码会依赖特定的类型,而这样的代码已导入装饰者,嘭!出状况了。
  • 还有一个问题,在实例化组件的时候,增加了代码的复杂度。一旦使用装饰者模式,不只需要实例化组件,还要把此组件包装进装饰者中,繁琐

好消息

好消息是工厂(Factory)模式生成器(Builder)模式对第二个问题即(实例化组件让代码变得复杂)有很大的帮助。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值