运行时扩展,比编译时继承威力更大
装饰对象,给爱用继承的人一个全新的设计眼界
星巴兹(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
)它,比如顾客想要摩卡咖啡
- 拿一个深焙咖啡(
DarkRoast
)对象 - 加入摩卡(
Mocha
),我们称之为以摩卡对象装饰它 - 调用
cost()
方法,并依赖委托(delegrate)将调料的钱加上去
以装饰器构造饮料订单
- 首先准备一个深焙咖啡(
DarkRoast
)对象 - 顾客想要摩卡(
Mocha
),我们建立一个Mocha
对象,用Mocha
对象把DarkRoast
对象包装(wrap)起来Mocha
对象就是一个装饰者,它的父类型与被包装类型相同,所以Mocha
对象也继承Beverage
类Mocha
对象由于继承Beverage
,也有cost()
,内部会有调用被包装类DarkRoast
的cost()
的代码,也有加上摩卡价钱的代码
- 如果顾客还想要牛奶(
Milk
),就在外面包一层牛奶- 同理,
Milk
类是装饰器,继承被装饰者的父类Beverage
Milk
类内部会有调用被包装类Mocha
的cost()
的代码,也有加上牛奶价钱的代码
- 同理,
- 这样我们计算总价格时,调用
Milk
的cost()
,会自动向下调用,调用之后返回加上本层价格的钱
目前我们知道了:
- 装饰者和被装饰对象具有相同的超类,也正因如此,在任何需要原始对象的场合,可以使用被装饰过的对象来替换
- 可以用一个或多个装饰着包装对象
- 装饰者可以在所委托被装饰者的行为之前/之后加上自己的行为,达到特定目的(后面我们会讨论与代理模式的不同,可以直接通过目录跳转到)
- 对象可以在任何时候背修饰,所以可以在运行时动态的、不限量的用你喜欢的装饰器修饰对象
定义装饰者模式
装饰者模式:动态的将责任附加到对象上,若要扩展功能,装饰着提供了比继承更有弹性的替代方案
所有具体装饰者中,会有一个基类类型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对象添加跳舞功能,添加说唱功能等,简单地说就是将多个功能附加在一个对象上。
-
所以,代理模式注重的是对对象的某一功能的流程把控和辅助,它可以控制对象做某些事,重心是为了借用对象的功能完成某一流程,而非对象功能如何。而装饰模式注重的是对对象功能的扩展,不关心外界如何调用,只注重对对象功能加强,装饰后还是对象本身。
来源:CSDN
作者:Tonited
链接:https://blog.csdn.net/weixin_43553694/article/details/104226092