设计模式:装饰者模式

|▌冷眼眸甩不掉的悲伤 提交于 2020-02-10 21:06:13

运行时扩展,比编译时继承威力更大

装饰对象,给爱用继承的人一个全新的设计眼界

星巴兹(Starbuzz)咖啡订单系统1

在这里插入图片描述(实锤了 是我买不起的样子)
星巴兹咖啡的扩张速度太快了,他们准备更新订单系统
他们之前设计的类是这样的

// 饮料类,店内所有饮料继承此类
abstract class Beverage{
	// 咖啡店的宣传标语
	String description;
	public void getDesString() {
		System.out.println(this.description);
	}
	// 咖啡的花销,子类自己实现
	public abstract void cost();
}

在这里插入图片描述由于购买咖啡的时候会加入很多调料,比如牛奶、椰果等等,这样就组成了很多不同的咖啡种类,价格也就不一样
一旦调料多起来,类设计就会变成这样:
在这里插入图片描述类爆炸!这简直是维护噩梦,如果牛奶价格上涨,所有加了牛奶的咖啡种类的cost()都要重写,这可太刺激了

利用实例变量和继承

为了不出现类爆炸的情况,我们使用实例变量和继承,在基类Beverage种添加代表每种调料的boolean类型实例变量
在这里插入图片描述子类的cost()会在自己的花费上,判断是否有各个调料,有的话将调料的价格加上去

这会出现这样的问题:

  • 调料价钱的改变会使我们改变现有代码
  • 一旦出现新的调料,就要加上新的方法,并改变超类种的cost()方法
  • 比如,对于热咖啡来说,加”冰“这个调料是完全没必要的,但是热咖啡还是继承了hasIce()这个方法
  • ……

认识装饰者模式

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

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

我们可以这样做:以饮料为主体,之后在运行过程中用调料来“装饰“(decorate)它,比如顾客想要摩卡咖啡

  1. 拿一个深焙咖啡(DarkRoast)对象
  2. 加入摩卡(Mocha),我们称之为以摩卡对象装饰它
  3. 调用cost()方法,并依赖委托(delegrate)将调料的钱加上去

以装饰器构造饮料订单

  1. 首先准备一个深焙咖啡(DarkRoast)对象
    在这里插入图片描述
  2. 顾客想要摩卡(Mocha),我们建立一个Mocha对象,用Mocha对象把DarkRoast对象包装(wrap)起来
    • Mocha对象就是一个装饰者,它的父类型与被包装类型相同,所以Mocha对象也继承Beverage
    • Mocha对象由于继承Beverage,也有cost(),内部会有调用被包装类DarkRoastcost()的代码,也有加上摩卡价钱的代码
      在这里插入图片描述
  3. 如果顾客还想要牛奶(Milk),就在外面包一层牛奶
    • 同理,Milk类是装饰器,继承被装饰者的父类Beverage
    • Milk类内部会有调用被包装类Mochacost()的代码,也有加上牛奶价钱的代码
      在这里插入图片描述
  4. 这样我们计算总价格时,调用Milkcost(),会自动向下调用,调用之后返回加上本层价格的钱
    在这里插入图片描述
    目前我们知道了:
  • 装饰者和被装饰对象具有相同的超类,也正因如此,在任何需要原始对象的场合,可以使用被装饰过的对象来替换
  • 可以用一个或多个装饰着包装对象
  • 装饰者可以在所委托被装饰者的行为之前/之后加上自己的行为,达到特定目的(后面我们会讨论与代理模式的不同,可以直接通过目录跳转到)
  • 对象可以在任何时候背修饰,所以可以在运行时动态的、不限量的用你喜欢的装饰器修饰对象

定义装饰者模式

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

所有具体装饰者中,会有一个基类类型Component的引用,这个引用指向被装饰对象,装饰者通过这个引用来调用被装饰对象的成员
在这里插入图片描述

修改星巴兹

新的设计

新的星巴兹使用了装饰者模式之后就变成了下图的结构

在这里插入图片描述
加入了CondimentDecorator类其实是为了使继承链更加清晰,所有装饰器继承自CondimentDecorator,而不是直接继承Beverage类,这样装饰器就不与其他基本咖啡种类混在一起,继承链十分清晰

考虑到不同的调料有不同 的特点,自然就会有不同的标语,我们在CondimentDecorator使用抽象类覆盖了父类的getDescription(),准备在父类基础标语上加上修饰词,比如“浓郁的”、“香甜的”

星巴兹的代码

基类代码

首先是所有咖啡的基类,同时也是装饰器的基类Beverage

abstract class Beverage{
	// 咖啡店的宣传标语
	String description = "Unknown Beverage";
	public void getDesString() {
		System.out.println(this.description);
	}
	// 咖啡的花销,子类自己实现
	public abstract void cost();
}

之后是调料(Condiment)类,也就是所有具体装饰者(具体调料)的父类

public abstract class CondimentDecorator extends Beverage{
	public abstract String getDescription();
}

具体饮料类代码

在这里插入图片描述
这里就只写一个作为示例

public class Espresso extends Beverage {
	// 这种咖啡的描述,因为在装饰器调用链的最内层调用的,所以前面的装饰器可以在这个咖啡的描述之前,加上很多的形容词
	public Espresso() {
		description = "Espresso";
	}
	
	public double cost() {
		return 1.99;
	}
}

对于这种咖啡的描述,因为在装饰器调用链的最内层调用的,所以前面的装饰器可以在这个咖啡的描述之前,加上很多的形容词

具体调料代码

在这里插入图片描述

public class Mocha extends CondimentDecorator{
	// 这个引用指向被装饰对象,装饰者通过这个引用来调用被装饰对象的成员
	Beverage beverage;
	
	public Mocha(Beverage beverage) {
		this.beverage = beverage;
	}
	
	public String getDescription() {
		// beverage.getDescription()会一层一层的向下调用,最底下一层为具体咖啡的描述
		return  beverage.getDescription() + "加了摩卡";
	}
	
	public double cost() {
		return 0.2 + beverage.cost();
	}
}

使用装饰者:供应咖啡

public static void main(String[] args) throws IOException{
	Beverage darkRoast = new DarkRoast(); // 拿到一杯简单的深焙咖啡
	System.out.println(darkRoast.getDescription); // 打印结果为深焙咖啡的原本描述,假设为“深焙咖啡”
	darkRoast = new Mocha(darkRoast); // 摩卡装饰器装饰深焙咖啡,向深焙咖啡中加入摩卡
	System.out.println(darkRoast.getDescription); // 打印结果:"深焙咖啡加摩卡"
}

JAVA中的装饰器

在这里插入图片描述
JAVA中的IO很多使用了装饰器,比如LineNumberInputStream就是一个修饰器,它添加了计算行数的能力
这里我们就不详细讲解了

装饰器模式的黑暗面

  • 装饰器模式由于不断创建新的装饰器对象,会导致产生很多的小对象,增加代码的复杂度,之后的工厂模式和生成器模式会对这个问题有所解决
  • 如果某些代码依赖于具体类型类型1,需要那些只有类型1而它的父类类型0没有的属性和方法,但是修饰器却继承于类型0,这时如果我们使用装饰器替代原类型就会出现问题

回顾

在这里插入图片描述

OO原则

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

装饰器模式

装饰器模式:动态的将责任附加到对象上,想要扩展功能,装饰者提供有利于继承的另一种选择

要点

  • 继承属于扩展形式之一,但不见得是达到弹性设计的最佳方式
  • 我们的设计中,应允许行为可以被扩展,无需修改现有的代码
  • 组合和委托可用于运行时动态添加新行为
  • 除了继承,装饰者模式可以让我们扩展行为
  • 装饰者模式意味着一群装饰者类。这些类用来包装具体组件
  • 装饰者类与被装饰者组件有相同的类型(通过继承或接口)
  • 装饰者可以在被装饰者前后加上自己行为,甚至可以取代
  • 可以用无数个装饰者包装一个组件
  • 装饰者一般对组件的客户透明,除非客户程序依赖于具体的组件类型
  • 装饰者会导致设计中出现许多小对象,如果过度使用会使程序变复杂

补充知识:装饰者模式与代理模式的区别2

  • 对装饰器模式来说,装饰者(Decorator)和被装饰者(Decoratee)都实现一个接口。对代理模式来说,代理类(Proxy Class)和真实处理的类(Real Class)都实现同一个接口。此外,不论我们使用哪一个模式,都可以很容易地在真实对象的方法前面或者后面加上自定义的方法。

  • 在上面的例子中,装饰器模式是使用的调用者从外部传入的被装饰对象(coffee),调用者只想要你把他给你的对象装饰(加强)一下。而代理模式使用的是代理对象在自己的构造方法里面new的一个被代理的对象,不是调用者传入的。调用者不知道你找了其他人,他也不关心这些事,只要你把事情做对了即可。

  • 装饰器模式关注于在一个对象上动态地添加方法,而代理模式关注于控制对对象的访问。换句话说,用代理模式,代理类可以对它的客户隐藏一个对象的具体信息。因此当使用代理模式的时候,我们常常在一个代理类中创建一个对象的实例;当使用装饰器模式的时候,我们通常的做法是将原始对象作为一个参数传给装饰器的构造器。

  • 装饰器模式和代理模式的使用场景不一样,比如IO流使用的是装饰者模式,可以层层增加功能。而代理模式则一般是用于增加特殊的功能,有些动态代理不支持多层嵌套。

  • 代理和装饰其实从另一个角度更容易去理解两个模式的区别:代理更多的是强调对对象的访问控制,比如说,访问A对象的查询功能时,访问B对象的更新功能时,访问C对象的删除功能时,都需要判断对象是否登陆,那么我需要将判断用户是否登陆的功能抽提出来,并对A对象、B对象和C对象进行代理,使访问它们时都需要去判断用户是否登陆,简单地说就是将某个控制访问权限应用到多个对象上;而装饰器更多的强调给对象加强功能,比如说要给只会唱歌的A对象添加跳舞功能,添加说唱功能等,简单地说就是将多个功能附加在一个对象上。

  • 所以,代理模式注重的是对对象的某一功能的流程把控和辅助,它可以控制对象做某些事,重心是为了借用对象的功能完成某一流程,而非对象功能如何。而装饰模式注重的是对对象功能的扩展,不关心外界如何调用,只注重对对象功能加强,装饰后还是对象本身。


  1. 参考:《Head First 设计模式》 ↩︎

  2. 本段来源:https://www.cnblogs.com/yanggb/p/10952843.html ↩︎

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!