3.1装饰者模式
亦称: 装饰者模式、装饰器模式、Wrapper、Decorator
装饰模式是一种结构型设计模式, 允许你通过将对象放入包含行为的特殊封装对象中来为原对象绑定新的行为。
举个例子:天气很冷,我们一件一件穿衣服,从内到外,穿了很多件衣服。
问题
欢迎来到瑞心咖啡店,本店以迅速的扩张优势占领了附近的几条街,但是随着顾客的增多,我们需要一套更完善和流程的架构去应对咖啡的销售,所以我们需要更新我们的订单系统!
我们原先的类设计是这样的:
- 每种咖啡不同,价格也不同
- 消费者可以根据不同的咖啡选择加入不同的小料:摩卡、豆浆等等,或者覆盖奶泡
- 当然有的咖啡加入了某些小料之后会变得异常难喝
瑞心咖啡会个根据不同的小料收取不同的费用。所以订单系统必须考虑到这些事情。我们继续使用这样的类型,使我们的结构变得异常臃肿:
当每上市一种新的调料或者咖啡,我们就陷入了一种麻烦的循环中去。。。。
解决方案
我们可以使用继承和实例变量来追踪这些调料
我们先从Beverage
下手,加上一些实例变量来代表是否加调料,然后加上cost()
方法来计算所需要的价格。对不同的咖啡继承对应的Beverage
类,其中不包含任何小料,当需要小料时,只需要将小料加入进来即可。那么当然小料也需要一个类去实现。
那么对于我们的设计来说,需要遵循一个设计原则:开闭原则
开闭原则:对扩展开发,对修改关闭
我们可以对类进行扩展,但是就不用更改已经有的实现
所以如果我们有以下需求:
- 点一杯Espresso(意大利浓咖啡)
- 加一份摩卡
- 加一份牛奶
那么根据我们的装饰者模式来说,最后包装的样子是不是这样:
所以当我们是算钱的时候,是不是一层一层的委托给外面,就可以实现了呢?
好了我们已经了解了装饰者模式,让我们看实现的代码:
- Beverage类,抽象出来,作为咖啡的和调味品的基类,用于算钱和描述
public abstract class Beverage {
/**
* 描述
*/
public String description = "Unknown Beverage";
/**
* 返回咖啡描述
* @return 描述
*/
public String getDescription(){
return description;
}
/**
* 购买该描述需要花费的
*/
public abstract double cost();
}
- DarkRoast咖啡类
public class DarkRoast extends Beverage {
public DarkRoast(){
description = "DarkRoast";
}
@Override
public double cost() {
return 1.69;
}
}
- Espresso咖啡类
public class Espresso extends Beverage {
/**
* 咖啡类
*/
public Espresso(){
description = "Espresso";
}
@Override
public double cost() {
return 1.99;
}
}
-
HouseBlend类,和Espresso、DarkRoast一样,不再写
-
CondimentDecorator调味品类,用于描述咖啡所加的内容
public abstract class CondimentDecorator extends Beverage {
/**
* 获取描述
*/
public abstract String getDescription();
}
- 调味品的实现类:Mocha类
public class Mocha extends CondimentDecorator {
/**
* 不同的咖啡作为参数加入进来
*/
Beverage beverage;
public Mocha(Beverage beverage){
this.beverage = beverage;
}
/**
* 计算价格
* @return 该调味品需要的钱 + coffee需要的钱
*/
@Override
public double cost() {
return 0.20 + beverage.cost();
}
@Override
public String getDescription() {
return beverage.getDescription() + ",Mocha";
}
}
-
其他的调味品类(Soy、Whip)同上
-
设计好了之后,我们开始调试我们的包装者吧!
public class Main {
public static void main(String[] args) {
Beverage beverage = new Espresso();
System.out.println("Beverage:"+beverage.getDescription());
//DarkRost咖啡 添加了Mocha * 2 , whip * 1
Beverage beverage1 = new DarkRoast();
beverage1 = new Mocha(beverage1);
beverage1 = new Mocha(beverage1);
beverage1 = new Whip(beverage1);
System.out.println("DarkRoast:"+beverage1.getDescription() + " cost: $" + beverage1.cost());
//HouseBlen咖啡 添加了Soy、Mocha、Whip * 1
Beverage beverage2 = new HouseBlend();
beverage2 = new Soy(beverage2);
beverage2 = new Mocha(beverage2);
beverage2 = new Whip(beverage2);
System.out.println("HouseBlend:"+beverage2.getDescription() + " cost: $" + beverage2.cost());
}
}
运行结果:
Beverage:Espresso
DarkRoast:DarkRoast,Mocha,Mocha,whip cost: $2.5999999999999996
HouseBlend:House Blend Coffee,soy,Mocha,whip cost: $2.02
装饰模式结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gGFdizP6-1676982856417)(null)]
- 部件 (Component) 声明封装器和被封装对象的公用接口。
- 具体部件 (Concrete Component) 类是被封装对象所属的类。 它定义了基础行为, 但装饰类可以改变这些行为。
- 基础装饰 (Base Decorator) 类拥有一个指向被封装对象的引用成员变量。 该变量的类型应当被声明为通用部件接口, 这样它就可以引用具体的部件和装饰。 装饰基类会将所有操作委派给被封装的对象。
- 具体装饰类 (Concrete Decorators) 定义了可动态添加到部件的额外行为。 具体装饰类会重写装饰基类的方法, 并在调用父类方法之前或之后进行额外的行为。
- 客户端 (Client) 可以使用多层装饰来封装部件, 只要它能使用通用接口与所有对象互动即可。
装饰模式适合应用场景
如果你希望在无需修改代码的情况下即可使用对象, 且希望在运行时为对象新增额外的行为, 可以使用装饰模式。
装饰能将业务逻辑组织为层次结构, 你可为各层创建一个装饰, 在运行时将各种不同逻辑组合成对象。 由于这些对象都遵循通用接口, 客户端代码能以相同的方式使用这些对象。
如果用继承来扩展对象行为的方案难以实现或者根本不可行, 你可以使用该模式。
许多编程语言使用 final
最终关键字来限制对某个类的进一步扩展。 复用最终类已有行为的唯一方法是使用装饰模式: 用封装器对其进行封装。
实现方式
- 确保业务逻辑可用一个基本组件及多个额外可选层次表示。
- 找出基本组件和可选层次的通用方法。 创建一个组件接口并在其中声明这些方法。
- 创建一个具体组件类, 并定义其基础行为。
- 创建装饰基类, 使用一个成员变量存储指向被封装对象的引用。 该成员变量必须被声明为组件接口类型, 从而能在运行时连接具体组件和装饰。 装饰基类必须将所有工作委派给被封装的对象。
- 确保所有类实现组件接口。
- 将装饰基类扩展为具体装饰。 具体装饰必须在调用父类方法 (总是委派给被封装对象) 之前或之后执行自身的行为。
- 客户端代码负责创建装饰并将其组合成客户端所需的形式。
装饰模式优缺点
优点:
- 你无需创建新子类即可扩展对象的行为。
- 你可以在运行时添加或删除对象的功能。
- 你可以用多个装饰封装对象来组合几种行为。
- 单一职责原则。 你可以将实现了许多不同行为的一个大类拆分为多个较小的类。
缺点:
- 在封装器栈中删除特定封装器比较困难。
- 实现行为不受装饰栈顺序影响的装饰比较困难。
- 各层的初始化配置代码看上去可能会很糟糕。
与其他模式的关系
-
适配器模式可以对已有对象的接口进行修改, 装饰模式则能在不改变对象接口的前提下强化对象功能。 此外, 装饰还支持递归组合, 适配器则无法实现。
-
适配器能为被封装对象提供不同的接口, 代理模式能为对象提供相同的接口, 装饰则能为对象提供加强的接口。
-
责任链模式和装饰模式的类结构非常相似。 两者都依赖递归组合将需要执行的操作传递给一系列对象。 但是, 两者有几点重要的不同之处。
责任链的管理者可以相互独立地执行一切操作, 还可以随时停止传递请求。 另一方面, 各种装饰可以在遵循基本接口的情况下扩展对象的行为。 此外, 装饰无法中断请求的传递。
-
组合模式和装饰的结构图很相似, 因为两者都依赖递归组合来组织无限数量的对象。
装饰类似于组合, 但其只有一个子组件。 此外还有一个明显不同: 装饰为被封装对象添加了额外的职责, 组合仅对其子节点的结果进行了 “求和”。
但是, 模式也可以相互合作: 你可以使用装饰来扩展组合树中特定对象的行为。
-
大量使用组合和装饰的设计通常可从对于原型模式的使用中获益。 你可以通过该模式来复制复杂结构, 而非从零开始重新构造。
-
装饰可让你更改对象的外表, 策略模式则让你能够改变其本质。
-
装饰和代理有着相似的结构, 但是其意图却非常不同。 这两个模式的构建都基于组合原则, 也就是说一个对象应该将部分工作委派给另一个对象。 两者之间的不同之处在于代理通常自行管理其服务对象的生命周期, 而装饰的生成则总是由客户端进行控制。
参考:First Head设计模式、设计模式