前言
前面学习了结构型模式的适配器模式、桥接模式
适配器模式是将类转换成客户需要的类
桥接模式是将抽象部分和实现部分分离,使两个维度的变化分割、组合在一起
而接下来学习结构型模式 - 装饰模式
目录
现实问题
现实中,常常需要对某种产品增加新功能或者美化,如在相片外加一个相框、房屋装修,软件开发中,在不改变该组件核心功能的基础上,对其进行功能扩展
例如:
咖啡有很多种类,咖啡可以添加很多东西:牛奶、砂糖等等
对于客户,可以随意搭配,每一种搭配价钱也不一样
装饰模式
我们知道对一个对象进行扩展,有两种方法:
- 继承:通过继承一个现有类可以使得子类在拥有自身方法的同时还拥有父类的方法,但是继承缺乏灵活性且耦合度高
- 关联:将一个类的对象嵌入另一个对象中,由另一个对象来决定是否调用嵌入对象的行为以便扩展自己的行为,这样灵活性高且耦合度低
装饰模式(Decorator Pattern) :动态地给一个对象增加一些额外的职责(Responsibility),就增加对象功能来说,装饰模式比生成子类实现更为灵活,在不需要创造更多子类的情况下,将对象的功能加以扩展。
其别名也可以称为包装器(Wrapper),与适配器模式的别名相同,但它们适用于不同的场合。根据翻译的不同,装饰模式也有人称之为“油漆工模式”,它是一种对象结构型模式。
装饰模式需要遵守:
- 一个装饰类的接口必须与被装饰类的接口保持相同,对于客户端来说无论是装饰之前的对象还是装饰之后的对象都可以一致对待(不能因为装饰了之后,就无法像以前一样使用对象了)
- 尽量保持具体构件类Component作为一个“轻”类,也就是说不要把太多的逻辑和状态放在具体构件类中,可以通过装饰类对其进行扩展
装饰模式结构
- Component: 抽象构件
- ConcreteComponent: 具体构件
- Decorator: 抽象装饰类
- ConcreteDecorator: 具体装饰类
有没有发现与桥接模式特别像
两者的区别在与:
- 桥接模式两种维度的变化是平等的,而装饰模式有修饰者和被修饰者的区分(Decorator抽象装饰类继承Component构件)
- 解决的问题不同:桥接模式是为了现有机制的多个维度的变化,而装饰模式是对现有机制的扩展
一个装饰模式案例
实现前面的咖啡案例
对于咖啡这个被装饰对象,和调味品这个装饰对象
- Component抽象构建:Coffee
package com.company.Structural.Decorator;
public abstract class Coffee {
//订单描述
//后面要重写getDescription,为了获得description就设置为public
private String description;
//价钱
private float price = 0.0f;
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public float getPrice() {
return price;
}
public void setPrice(float price) {
this.price = price;
}
//计算花费
public abstract float cost();
}
- 具体构件
Americano:
package com.company.Structural.Decorator;
public class Americano extends Coffee{
public Americano() {
setDescription("Americano咖啡");
setPrice(8.0f);
}
@Override
public float cost() {
return getPrice();
}
}
Espresso:
package com.company.Structural.Decorator;
public class Espresso extends Coffee{
public Espresso() {
setDescription("Espresso咖啡");
setPrice(6.0f);
}
@Override
public float cost() {
return getPrice();
}
}
FlatWhite:
package com.company.Structural.Decorator;
public class FlatWhite extends Coffee{
public FlatWhite() {
setDescription("FlatWhite咖啡");
setPrice(7.0f);
}
@Override
public float cost() {
return getPrice();
}
}
- 抽象装饰类Decorator
package com.company.Structural.Decorator;
public class Decorator extends Coffee {
private Coffee coffee;
public Decorator(Coffee coffee) {
this.coffee= coffee;
}
@Override
public float cost() {
//getPrice得到调味品的价格
//coffee.cost得到咖啡的价格
return getPrice() + coffee.cost();
}
@Override
public String getDescription() {
//得到咖啡的信息+调味品的信息
//这里就别写getDescription,会死循环
return description+ " : "+getPrice()+" && " +coffee.getDescription();
}
}
- 具体装饰类
Milk:
package com.company.Structural.Decorator;
public class Milk extends Decorator {
public Milk(Coffee coffee) {
super(coffee);
setDescription("调味品:Milk");
setPrice(3.0f);
}
}
Sugar:
package com.company.Structural.Decorator;
public class Sugar extends Decorator{
public Sugar(Coffee coffee) {
super(coffee);
setDescription("调味品:Sugar");
setPrice(1.0f);
}
}
- 客户类
package com.company.Structural.Decorator;
public class Client {
public static void main(String[] args) {
//客户需要Americano+milk+sugar
//先点coffee
Coffee americano = new Americano();
System.out.println("咖啡的价格:"+americano.cost());
//添加调味品
americano = new Milk(americano);
System.out.println("现在的价钱:"+americano.cost());
//再加一份sugar
americano = new Sugar(americano);
System.out.println("最终订单价钱:"+americano.cost());
System.out.println("描述:"+americano.getDescription());
}
}
装饰模式的优缺点
优点:
-
装饰模式与继承关系的目的都是要扩展对象的功能,但是装饰模式可以提供比继承更多的灵活性
-
可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的装饰器,从而实现不同的行为
-
通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合。可以使用多个具体装饰类来装饰同一对象,得到功能更为强大的对象
-
具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,在使用时再对其进行组合,原有代码无须改变,符合“开闭原则”
缺点:
- 使用装饰模式进行系统设计时将产生很多小对象,这些对象的区别在于它们之间相互连接的方式有所不同,而不是它们的类或者属性值有所不同,同时还将产生很多具体装饰类。这些装饰类和小对象的产生将增加系统的复杂度,加大学习与理解的难度
- 这种比继承更加灵活机动的特性,也同时意味着装饰模式比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐
- 各种对象都是嵌套的(套娃),方法也是各种递归,较为复杂
使用场景
在一下场景可以使用装饰模式:
-
在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
-
需要动态地给一个对象增加功能,这些功能也可以动态地被撤销。
-
当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。不能采用继承的情况主要有两类:第一类是系统中存在大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长;第二类是因为类定义不能继承(如final类)。
常用的装饰模式应用
最经典的Java IO
例如InputStream:类似与抽象构件,FileInputStream就是具体构件
而FilterInputStream类似与抽象装饰类;BufferedInputStream就类似与具体装饰类
我们可以看看源码:
FilterInputStream中:继承InputStream,聚合InputStream
而BufferedInputStream中:继承FilterInputStream,但是构造方法需要依赖InputStream
写一句使用方法:
BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("D:/file.txt"));
使用FileInputStream读取文件,为了提升速度我们可以添加一个功能:缓冲区,添加完功能后的加强版InputStream也就是BufferedInputStream
模式扩展
具体构件扩展
应该尽量保持具体构件类Component作为一个“轻”类,也就是说不要把太多的逻辑和状态放在具体构件类中
我们在定义具体构件时:Americano、FlatWhite、Espresso时,还是要继承Coffee类的cost方法:
可以进一步简化:定义一个类CoffeeType,继承Coffee抽象类,实现抽象方法cost,再有具体的咖啡类继承CoffeeType
模式简化
如果只有一个具体构件类而没有抽象构件类,那么抽象装饰类可以作为具体构件类的直接子类
当然这是极端情况
透明装饰模式
在透明装饰模式中,要求客户端完全针对抽象编程,装饰模式的透明性要求客户端程序不应该声明具体构件类型和具体装饰类型,而应该全部声明为抽象构件类型
我们前面编写的是半透明装饰模式:允许用户在客户端声明具体装饰者类型的对象,调用在具体装饰者中新增的方法(大部分都是半透明装饰模式)
总结
- 装饰模式的作用在与动态的给一个对象扩展功能
- 装饰模式有四个角色:抽象构件、具体构件、抽象装饰类、具体装饰类
- 装饰模式与桥接模式很像,区别在装饰类和被装饰类的不平等,装饰类继承被装饰类,且装饰模式是为了动态的扩展功能,而桥接模式是对象多个维度的变化的组合
- 装饰模式的优点:灵活性,动态扩展功能,且装饰类和被装饰类相对独立
- 装饰模式的缺点:会产生很多小对象,装饰对象和被装饰对象嵌套,方法递归,较为复杂
- 装饰模式适用于:需要动态、透明的给对象添加职责、功能,或者动态撤销;不能用继承方法扩充或者不利于采用继承方法
- 装饰模式可分为透明装饰模式和半透明装饰模式:透明装饰模式是客户端完全针对抽象编程,客户端程序不能声明具体构件类型和具体装饰类型,全部声明为抽象构件类型;半透明装饰模式允许客户端声明具体装饰者类型的对象,调用方法