目录
1 定义概述
1.1 定义
装饰着模式:简单的一句话理解就是,动态的给一个对象添加一些额外的功能,装饰者模式相对于生成子类更加的灵活。
我们来看下装饰者模式的UML图:
装饰者模式又称为包装模式,它主要是为了扩展对象的功能,通过持有对象的引用,把对象包装起来,可以在调用对象的方法之前或者之后增加新的功能,以达到给对象添加一些额外的职责,就像为对象添加了一些装饰。
另一个要点是,包装者可以有很多个,对象被包装了一层之后,依然可以继续再包装来添加新的职责。添加职责的目的其实就是为了扩展对象的功能,通过使用装饰者模式可以使得系统具有非常好的弹性。遵循了面向对象原则:对扩展开放,对修改关闭。如果有新的需求变更,对象功能扩展,只需要新增一个装饰者类,将对象包装起来即可扩展对象的功能,而不需要对旧有对象代码进行修改。
通过继承,子类进行方法重写同样也可以扩展对象的功能。但它的弹性比较差,一旦业务变更或新增业务功能时,就需要打开实现类的代码进行修改,这样不仅需要检查旧有代码,同时还要保证新增代码的正确性。类的继承关系是在编译期就已经确定好了,运行期间不能动态的更改,使用装饰者模式可以在运行期间动态的、不限量的给对象添加装饰者,扩展功能。装饰者模式是替代继承来扩展对象功能更好的方案。
- 抽象组件(Component) :定义装饰方法的规范
- 被装饰者(ConcreteComponent) :Component的具体实现,也就是我们要装饰的具体对象。
- 装饰者组件(Decorator) :持有组件(Component)对象的实例引用,该类的职责就是为了装饰具体组件对象,定义的规范。
- 具体装饰(ConcreteDecorator) :负责给构件对象装饰附加的功能。
1.装饰者和被装饰者有相同的超类(Component)。
2.你可以用一个或多个具体装饰(ConcreteDecorator)包装一个对象。
3.装饰者可以在所委托被装饰者的行为之前与/或之后,加上自己的行为,以达到特定的目的。(类似代理模式)
4.对象可以在任何时候被装饰,所以可以在运行时动态地、不限量地用你喜欢的装饰者来装饰对象。
1.2 优缺点
优点
- 继承的有力补充,比继承灵活,不改变原有对象的情况下给一个对象扩展功能。(继承在扩展功能是静态的,必须在编译时就确定好,而使用装饰者可以在运行时决定,装饰者也建立在继承的基础之上的)
- 通过使用不同装饰类以及这些类的排列组合,可以实现不同的效果。
- 符合开闭原则
缺点
- 会出现更多的代码,更多的类,增加程序的复杂性。
- 动态装饰时,多层装饰时会更复杂。(使用继承来拓展功能会增加类的数量,使用装饰者模式不会像继承那样增加那么多类的数量但是会增加对象的数量,当对象的数量增加到一定的级别时,无疑会大大增加我们代码调试的难度)
装饰者相关的设计模式
- 装饰者和代理模式
装饰者模式关注的是对象的动态添加功能。代理模式关注的是对对象的控制访问,对它的用户隐藏对象的具体信息。
- 装饰者模式和适配器模式
装饰者模式和被装饰的类要实现同一个接口,或者装饰类是被装饰的类的子类。 适配器模式和被适配的类具有不同的接口。
2 实例解析
2.1 通用模型
装饰模式中的角色:
- 抽象构件(Component)角色:Component是一个抽象类或接口,是要包装的原始对象。
- 具体构件(ConcreteComponent)角色:是Component的实现类,最终要装饰的实际对象。
- 装饰(Decorator)角色:是一个抽象类,继承或实现了Component的接口,同时它持有一个对Component实例对象的引用,也可以有自己的方法。
- 具体装饰(ConcreteDecorator)角色:是Decorator的实现类,是具体的装饰者对象,负责给ConcreteComponent附加责任。
示例代码:
public abstract class Component {
public abstract void operate();
}
public class ConcreteComponent extends Component {
@Override
public void operate() {
System.out.println("ConcreteComponent 原始对象操作");
}
}
public abstract class Decorator extends Component {
private Component component;
/**
* 构造函数传递要装饰的对象
* @param component 被装饰的对象
*/
public Decorator(Component component) {
this.component = component;
}
@Override
public void operate() {
//调用被装饰者的方法
this.component.operate();
}
}
public class ConcreteDecoratorA extends Decorator {
public ConcreteDecoratorA(Component component) {
super(component);
}
@Override
public void operate() {
super.operate();
//调用自己的方法
this.operateAMethod();
}
private void operateAMethod() {
System.out.println("ConcreteDecoratorA添加的修饰方法");
}
}
public class ConcreteDecoratorB extends Decorator {
public ConcreteDecoratorB(Component component) {
super(component);
}
@Override
public void operate() {
//调用自己的方法
this.operateBMethod();
super.operate();
}
private void operateBMethod() {
System.out.println("ConcreteDecoratorB添加的修饰方法");
}
}
public class Client {
public static void main(String[] args) {
//创建原始对象
Component component = new ConcreteComponent();
//第一次装饰
component = new ConcreteDecoratorA(component);
//第二次装饰
component = new ConcreteDecoratorB(component);
//两次装饰后的操作
component.operate();
}
}
输出结果:
装饰者对象和被装饰的对象都实现了相同的操作接口,装饰者将被装饰者包装起来,在同名的接口方法中,在调用被装饰者的方法之前或者之后做一些自己的操作,这样在外部调用者来看,就相当于被“装饰”了一样。
2.2 实例
咖啡的例子
首先咖啡是一种饮料,然后咖啡还可以加调料如加糖加牛奶等等。饮料是抽象的构件基类,咖啡作为具体的构件实现类,调料就是我们抽象的装饰者基类,然后会有具体的饮料装饰者实现类。在基类中我们给饮料添加两个行为,一个是获取饮料的描述,一个是获取饮料的价格。类图设计如下:
其中Beverage是抽象的饮料类,Coffee是具体的咖啡饮料类,BeverageDecorator是抽象的饮料装饰者类,SugarCoffee、 LemonCoffee和MilkCoffee是具体的饮料装饰类,分别表示给咖啡加糖、加柠檬和加牛奶。
相关实现代码:
/**
* 饮料抽象类
*/
public abstract class Beverage {
protected String description = "Unknown Beverage";
public String getDescription() {
return description;
}
public abstract double getPrice();
}
/**
* 咖啡饮料
*/
public class Coffee extends Beverage {
public Coffee() {
description = "咖啡饮料";
}
@Override
public double getPrice() {
return 10.00;
}
}
/**
* 饮料装饰者抽象类
*/
public abstract class BeverageDecorator extends Beverage {
private Beverage beverage;
public BeverageDecorator(Beverage beverage) {
this.beverage = beverage;
}
@Override
public String getDescription() {
return beverage.getDescription();
}
@Override
public double getPrice() {
return beverage.getPrice();
}
}
/**
* 装饰者类,负责给咖啡加糖
*/
public class SugarCoffee extends BeverageDecorator {
public SugarCoffee(Beverage beverage) {
super(beverage);
}
@Override
public String getDescription() {
return super.getDescription() + ",加糖";
}
@Override
public double getPrice() {
return super.getPrice() + 2.00;
}
}
/**
* 装饰者类,负责给咖啡加牛奶
*/
public class MilkCoffee extends BeverageDecorator {
public MilkCoffee(Beverage beverage) {
super(beverage);
}
@Override
public String getDescription() {
return super.getDescription() + ",加牛奶";
}
@Override
public double getPrice() {
return super.getPrice() + 3.00;
}
}
/**
* 装饰者类,负责给咖啡加柠檬
*/
public class LemonCoffee extends BeverageDecorator {
public LemonCoffee(Beverage beverage) {
super(beverage);
}
@Override
public String getDescription() {
return super.getDescription() + ",加柠檬";
}
@Override
public double getPrice() {
return super.getPrice() + 4.00;
}
}
public class Client {
public static void main(String[] args) {
//创建一种叫咖啡的饮料
Beverage coffee = new Coffee();
//给咖啡加糖
coffee = new SugarCoffee(coffee);
//给咖啡加牛奶
coffee = new MilkCoffee(coffee);
//给咖啡加柠檬
coffee = new LemonCoffee(coffee);
System.out.println("你点的饮料是:"+coffee.getDescription()+"\n"+"价格是:"+coffee.getPrice()+"元");
}
}
咖啡类经过加糖加牛奶加咖啡三次装饰之后的输出结果:
可以看到,我们要生成各种调料的咖啡,代码就很简单,只要调用构造函数,一层一层的往上套就可以了,每加一层饮料修饰,咖啡就具有了新的饮料特性以及售价。并且这里顺序其实也是可以调整的,不过对这个例子而言顺序调整没有太大意义,但在实际当中或许是有用的。
仔细思考,经过多次装饰的过程中我们的咖啡究竟发生了哪些变化?
以获取售价为例,当经过SugarCoffee之后getPrice()会先获取原始的咖啡售价,然后再加上Sugar自己的售价,最后返回。在经过MilkCoffee装饰之后,getPrice()则会先获取经过SugarCoffee装饰之后的价格,然后加上Milk自己的价格,最后返回。在经过LemonCoffee装饰之后,getPrice()则会先获取经过MilkCoffee装饰之后的价格,然后加上Lemon自己的价格,最后返回。整个过程就像剥洋葱一样,层层嵌套,当然貌似有点递归的意思在里面。
通过下面的图可以帮助你很好的理解装饰者与原始对象之间的关系:
3 java中的装饰者
3.1 装饰模式的简化
大多数情况下,装饰模式的实现都要比上面给出的示意性例子要简单。
如果只有一个ConcreteComponent类,那么可以考虑去掉抽象的Component抽象类(接口),把Decorator作为一个ConcreteComponent子类。如下图所示:
如果只有一个ConcreteDecorator类,那么就没有必要建立一个单独的Decorator抽象类,而可以把Decorator和ConcreteDecorator的责任合并成一个类。甚至在只有两个ConcreteDecorator类的情况下,都可以这样做。如下图所示:
装饰者模式在使用的时候尽量按照依赖倒置的原则,持有变量的引用类型尽量使用抽象类型,而不是具体类型。即Component component = new ConcreteComponent()
, 只有当你需要使用具体类中自己的方法时可以定义成ConcreteComponent component = new ConcreteComponent()
,装饰者类型持有的构件的引用也是抽象构建类型。
透明的装饰者和半透明的装饰者
- 透明的装饰者, 要求具体构件角色、装饰角色的接口与抽象构件角色的接口完全一致。这种模式在使用的时候两种类型没有区别,因为方法接口是完全相同的,完全可以使用抽象类型的变量引用去操作,对外是完全透明的。这种是理想中的模式。
- 半透明的装饰者,如果装饰角色的接口与抽象构件角色接口不一致,也就是说装饰角色的接口比抽象构件角色的接口宽的话,装饰角色实际上已经成了一个适配器角色,这种装饰模式称为“半透明”的装饰模式,这种模式下子类可以拥有自己个性化的方法。
在实际当中应用最多的还是半透明的装饰者模式,因为子类一般都会拥有自己的特性。
3.2 Java中的装饰者
Java中的java.io包里面的InputStream类和OutputStream类就是装饰者模式。
刚接触java的时候,相信你一定见过类似下面的代码:
InputStream inputStream = null;
OutputStream outputStream = null;
try {
outputStream = new BufferedOutputStream(new FileOutputStream(new File("test2.txt")));
inputStream = new DataInputStream(new BufferedInputStream(new FileInputStream(new File("text.txt"))););
byte[] buffer = new byte[1024];
int length = 0;
while ((length = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, length);
}
} catch (Exception e) {
e.printStackTrace();
}
实际上在涉及输入流输出流的操作时,都会进行一些嵌套使用,如果对装饰者模式不熟悉,你可能会困惑或者不理解为什么java的输入流要嵌套这么多层。其实每一层都有自己实现的一些特性:
这些都是装饰者模式的实现,实际上在java的io包中存在着数量众多的装饰者类,单看InputStream就有好多:
优缺点
装饰者模式可以带来比继承更加灵活性的扩展功能,使用更加方便,可以通过组合不同的装饰者对象来获取具有不同行为状态的多样化的结果。装饰者模式比继承更具良好的扩展性,完美的遵循开闭原则,继承是静态的附加责任,装饰者则是动态的附加责任。
同时也有缺点,那就是装饰者实现类可能会很多,容易出现类膨胀,需要维护的代码就要多一些, 并且它们之间往往长得都很相似,比如java中的InputStream类,如果对这些装饰者类不熟悉的话,可能一时间会陷入不知道该使用哪个装饰者的尴尬境地。
何时应用装饰者模式:
当你需要扩展一个类的功能,或者给一个类动态的附加功能,又可以动态的撤销,或者想在一个方法的执行前/后添加自己的行为,都可以选择装饰者模式。