《HeadFirst设计模式》第三章-装饰者模式

本文深入解析装饰者设计模式,通过星巴兹咖啡系统案例,展示如何动态扩展对象功能,避免类爆炸,实现灵活添加调料,同时提供Java I/O类库中的装饰者模式应用实例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.声明

设计模式中的设计思想、图片和部分代码参考自《Head First设计模式》作者Eric Freeman & Elisabeth Freeman & Kathy Siezza & Bert Bates

在这里我只是对这本书进行学习阅读,并向大家分享一些心得体会。

2.装饰者模式的定义

装饰者模式指的是尽量不使用继承,且不必改变原类文件,就能够动态地扩展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。

3.需求案例

定义比较抽象,所以还是从一个案例入手。

星巴兹是非常著名的咖啡店,随着规模的壮大,店内的饮品种类也越来越丰富,于是星巴兹想要改进饮料系统。

开始时的设计:

但是事情没有那么简单,因为饮料需要添加各种调料,所以加上调料之后的类图变成了下面一坨:

这种设计简直不见看上去就就令人毛骨悚然,最重要的是,这种设计,根本无法维护,设想如果新添一种调料,那么需要添加多少个新的饮料类?或者,如果一种调料改变了价钱,那么又需要改变多少个类?

考虑到这些问题后,他们又拿出了新的方案。

4.初始的解决方案

父类结构如下:

子类继承图如下:

在父类的结构中,定义了是否添加各个调料的布尔值,这样调料就得以被子类复用了,因为子类只需要去负责设置这些调料是否添加即可,并且在价格的计算中,调料价格的计算也得到了复用,调料的价格交给了父类去计算,而子类只需要将自己的单价与父类计算出的调料做和即可。这样的设计,就避免了仅仅因为调料的不同,就需要设计不同类别的饮料的弊端。

父类代码如下:

//饮料的基类
public class Beverage {

	//饮料的名称
	String description = "未知饮料";
	
	//饮料可加的调料
	boolean milk; //是否加奶  单价1元
	boolean soy; //是否加soy  单价2元
	boolean mocha; //是否加摩卡  单价3元
	boolean whip; //是否加奶泡  单价4元
	
	//返回调料的名称
	String getDescription() {
		return description;
	}
	
	//返回调料的总价
	double cost() {
		int sum = 0;
		if(milk) {
			sum = sum + 1;
		}
		if(soy) {
			sum = sum + 2;
		}
		if(mocha) {
			sum = sum + 3;
		}
		if(whip) {
			sum = sum + 4;
		}
		return sum;
	}

	//对这些调料进行设置
	public boolean isMilk() {
		return milk;
	}

	public void setMilk(boolean milk) {
		this.milk = milk;
	}

	public boolean isSoy() {
		return soy;
	}

	public void setSoy(boolean soy) {
		this.soy = soy;
	}

	public boolean isMocha() {
		return mocha;
	}

	public void setMocha(boolean mocha) {
		this.mocha = mocha;
	}

	public boolean isWhip() {
		return whip;
	}

	public void setWhip(boolean whip) {
		this.whip = whip;
	}
}

某个子类的代码如下:

//炭烧饮料  单价10元
public class DarkRoast extends Beverage{

	//初始化饮料名称
	public DarkRoast() {
		description = "最优秀的炭烧";
	}
	
	//配料的价钱由父类cost方法决定,子类cost方法计算出本饮料的单价并做和。
	double cost() {
		return super.cost() + 10;
	}
}

测试代码如下:

public class Test {

	public static void main(String[] args) {
		DarkRoast dr = new DarkRoast();
		dr.setMilk(false);
		dr.setMocha(false);
		dr.setSoy(true);
		dr.setWhip(true);
		
		System.out.println("DarkRoast的价格是:"+dr.cost());
	}
}

测试结果如下:

DarkRoast的价格是:16.0

但是这样的设计仍然有一下几个弊端:

  • 调料价格的改变,需要我们去修改现有代码;

  • 一旦出现新的调料,需要我们加上新的set/has方法(是否添加此调料的方法),并且需要修改父类中的cost方法;

  • 如果以后出一种新的饮料,例如绿茶,但是对于绿茶来说,whip这个调料根本就没有set/has的意义,因为绿茶根本就不能加奶泡(我的意思是,绿茶加奶泡就没法喝了),所以对于绿茶来说,父类空间中,boolean whip这个变量就是无意义的;

  • 如果某个顾客需要一份双倍摩卡的咖啡,怎么办(毕竟父类的cost方法中,只能计算出一份摩卡的价格)。

那么问题该如何解决呢?

5.装饰者模式 

5.1装饰模式的解决思路

举例:以DarkRoast为例,如果一个顾客需要加Mocha和Whip的DarkRoast,那么这个过程可以这样设计:

5.2装饰者模式设计原则

装饰者和被装饰者对象需要有相同的超类型;

可以由一个或多个装饰者包装一个被装饰者;

既然装饰者和被装饰者有相同的超类型,所以在任何需要原始对象(被包装的)的场合,可以用装饰过的对象代替它;

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

对象可以在任何时候被装饰,所以可以在运行时动态地,不限量的用装饰者来装饰对象。

设计原则

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

5.3装饰者模式结构

装饰者模式:

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

5.4装饰者模式在此饮料框架下的类图

让调料继承Beverage是否合理?

答:刚开始我有这样的疑问,因为调料和饮料所属不同的类别,这样是否乱用了继承,其实调料继承饮料的目的在与想要得到类型匹配,而不是为了复用饮料的行为,如果只是想复用饮料的行为,那么当需要新行为时,还是得需要修改现在的代码。但是由于侧重点在于得到类型匹配,而且行为并不是通过继承得到的,而是通过组合得到(每一次装饰,都会得到新的行为),所以这样的设计还是较为合理的。

5.5代码设计

饮料的超类:(抽象组件)

//饮料的超类
public abstract class Beverage {

	//名称
	String description = "未知的饮料类型";

	//获得饮料类型的方法
	public String getDescription() {
		return description;
	}
	
	//获得花费
	public abstract double cost();
}

调料的超类:(抽象装饰者)

//调料的超类,为了让装饰者可以完全替代被装饰者,所以继承Beverage
public abstract class CondimentDecorator extends Beverage{

	//获得调料的描述
	public abstract String getDescription();
}

饮料实现类-浓缩咖啡(具体组件)

//浓缩咖啡
public class Espresso extends Beverage{
	
	public Espresso() {
		description = "浓缩咖啡";
	}

	//子类无需再管调料的价格,只关注自己的价格就可以
	@Override
	public double cost() {
		return 1.0;
	}
}

饮料实现类-HouseBlend(具体组件)

public class HouseBlend extends Beverage{
	
	public HouseBlend() {
		description = "House Blend 咖啡";
	}

	//子类无需再管调料的价格,只关注自己的价格就可以
	@Override
	public double cost() {
		return 2.0;
	}
}

饮料实现类-DarkRoast(具体组件)

//饮料实现类:DarkRoast
public class DarkRoast extends Beverage{
	
	public DarkRoast() {
		description = "DarkRoast";
	}

	//子类无需再管调料的价格,只关注自己的价格就可以
	@Override
	public double cost() {
		return 3.0;
	}
}

调料的实现类-豆浆(具体装饰者)

//豆浆
public class Soy extends CondimentDecorator{
	/**
	 * 这里将被装饰者的引用保持下来
	 * 目的:
	 * getDescription()时可以获取之前所有被装饰者的描述
	 * cost()时可以获取之前所有被装饰的价格
	 */
	Beverage beverage;
	
	public Soy(Beverage beverage) {
		this.beverage = beverage;
	}
	
	@Override
	public String getDescription() {
		//利用委托的办法获取之前的叙述,然后加上自己的描述
		//这样得到的描述就是 饮料+各种调料
		return beverage.getDescription() + ", 豆浆";
	}

	@Override
	public double cost() {
		// 同意利用委托获取的被装饰者的价格,然后加上自己的价格
		return beverage.cost() + 0.2;
	}
}

调料的实现类-摩卡(具体装饰者)

//摩卡
public class Mocha extends CondimentDecorator{

	/**
	 * 这里将被装饰者的引用保持下来
	 * 目的:
	 * getDescription()时可以获取之前所有被装饰者的描述
	 * cost()时可以获取之前所有被装饰的价格
	 */
	Beverage beverage;
	
	public Mocha(Beverage beverage) {
		this.beverage = beverage;
	}
	
	@Override
	public String getDescription() {
		//利用委托的办法获取之前的叙述,然后加上自己的描述
		//这样得到的描述就是 饮料+各种调料
		return beverage.getDescription() + ", 摩卡";
	}

	@Override
	public double cost() {
		// 同意利用委托获取的被装饰者的价格,然后加上自己的价格
		return beverage.cost() + 0.1;
	}
}

调料的实现类-奶泡(具体装饰者)

//奶泡
public class Whip extends CondimentDecorator{

	/**
	 * 这里将被装饰者的引用保持下来
	 * 目的:
	 * getDescription()时可以获取之前所有被装饰者的描述
	 * cost()时可以获取之前所有被装饰的价格
	 */
	Beverage beverage;
	
	public Whip(Beverage beverage) {
		this.beverage = beverage;
	}
	
	@Override
	public String getDescription() {
		//利用委托的办法获取之前的叙述,然后加上自己的描述
		//这样得到的描述就是 饮料+各种调料
		return beverage.getDescription() + ", 奶泡";
	}

	@Override
	public double cost() {
		// 同意利用委托获取的被装饰者的价格,然后加上自己的价格
		return beverage.cost() + 0.3;
	}
}

测试类:

//星巴兹咖啡系统测试
public class Test {

	public static void main(String[] args) {
		//压缩咖啡,什么调料也不加
		Beverage espresso = new Espresso();
		System.out.println(espresso.getDescription());
		System.out.println("价格:" + espresso.cost() + "元");
		System.out.println();
		
		//HouseBlend,将双倍摩卡和奶泡
		Beverage houseBlend = new HouseBlend();
		Beverage houseMocha1 = new Mocha(houseBlend);
		Beverage houseMocha2 = new Mocha(houseMocha1);
		Beverage houseWhip = new Whip(houseMocha2);
		System.out.println(houseWhip.getDescription());
		System.out.println("价格:" + houseWhip.cost() + "元");
		System.out.println();
		
		//DarkRoast,加摩卡、豆浆、奶泡
		Beverage darkRoast = new DarkRoast();
		Beverage darkMocha = new Mocha(darkRoast);
		Beverage darkSoy = new Soy(darkMocha);
		Beverage darkWhip = new Whip(darkSoy);
		System.out.println(darkWhip.getDescription());
		System.out.println("价格:" + darkWhip.cost() + "元");
		System.out.println();
	}
}

测试结果:

[浓缩咖啡]
价格:1.0元

[HouseBlend咖啡], 摩卡, 摩卡, 奶泡
价格:2.5元

[DarkRoast], 摩卡, 豆浆, 奶泡
价格:3.6元

6.真实世界的装饰者模式

6.1以装饰者模式的视角看待Java的I/O类库

以输入流为例:

举例:

被装饰的组件:FileInputStream

具体的装饰者1:BufferedInputStream

增加的功能:

1.利用缓冲区提高性能

2.readLine()一次读取一行数据

具体的装饰者2:LineNumberInputStream

增加了计算行数的能力

6.2FileInputStream使用案例

被读取的文件:

HELLO WORLD

测试程序:

//BufferedInputStream对FileInputStream进行了装饰
//使得我们现在不仅可以读取文件中的数据,而且还可以利用缓冲区,按行读取。
public class InputTest {

	public static void main(String[] args) throws IOException {
		
		int c;
		InputStream in = new BufferedInputStream(new FileInputStream("test.txt"));
		while((c = in.read()) >= 0) {
			System.out.print((char)c);
		}
		in.close();
	}
}

读取结果:

HELLO WORLD

6.3编写装饰BufferedInputStream的程序

现在编写程序对BufferedInputStream进行装饰,装饰后的效果是,新的类不仅可以利用缓冲区按行读取文件数据,而且还具有将文件中的字符从大写转换为小写的能力。

定义装饰类:

//继承FilterInputStream,FilterInputStream是所有InputStream的抽象装饰者
public class LowerCaseInputStream extends FilterInputStream{

	protected LowerCaseInputStream(InputStream in) {
		super(in);
	}

	//覆盖父类的read方法,读取字节
	@Override
	public int read() throws IOException {
		//从此输入流中读取下一个数据字节。 
		//valuebyte作为int返回,范围为0到255.如果没有可用字节,则由于已到达流的末尾,则返回值-1
		int c = super.read();
		//将字符数据转换为小写,直至文件末尾
		return c == -1 ? c : Character.toLowerCase((char)c);
	}
}

测试程序:

//对BufferedInputStream进行装饰,在原来的基础上增加将字母转换为小写的能力
public class InputTest {

	public static void main(String[] args) throws IOException {
		
		int c;
		InputStream in = 
				new LowerCaseInputStream(
						new BufferedInputStream(
								new FileInputStream("test.txt")));
		while((c = in.read()) >= 0) {
			System.out.print((char)c);
		}
		in.close();
	}
}

输出结果:

hello world

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

琴瘦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值