什么是装饰器模式?
装饰器模式又称为包装模式(Wrapper Pattern),属于结构型设计模式。顾名思义,这个模式是在不必改变原类和使用继承的情况下,动态的扩展一个对象的功能。它是通过创建一个包装对象,来实现功能的扩展。
为什么使用装饰器模式?
通过该模式的定义我们可以推测出该模式的优点,即:在不改变原类和使用继承的情况下,可以动态的扩展一个对象的功能。
在我们进行编码实践的过程中,通常是先完成某个类的基本功能,在后续的开发过程中在慢慢加入新的功能,所以扩展对象的功能这件事是不可避免的。那么通常来说,我们有三种方式来扩展对象的功能:
-
直接在原类中修改
这种方法是最不可取的,且不说增加的代码导致的新行为会不会导致bug的产生,单单是类的不可复用性就已经是一个软件工程师不能接受的了。
举个例子,一家咖啡馆里,一开始只有一种咖啡产品,即普通咖啡。后来我们想增加一个新品种,比如加奶咖啡,难不成我们把普通咖啡的类改成加奶咖啡的类?那如果有顾客就喜欢喝普通咖啡怎么办?
有一个很重要的原则是对扩展开发,对修改关闭
所以这种在原类上修改的方法十分不可取。
-
使用继承,在子类中扩展
这种方法比直接在原类中修改更像是一名合格的软件工程师写出来的代码,即不在原类上扩展,而是通过使用继承来扩展功能。
比如上面咖啡馆的例子,我们可以把普通咖啡类当做基类,然后将加奶咖啡类作为该类的子类。这样既保留了普通咖啡,也增加了新的咖啡种类。
但是这种实现方式也有问题,这种使用继承的方式特别不灵活,并且会引入很多额外的类。
一个很简单的例子,如果我们在有个加糖咖啡类,这个类也是继承自普通咖啡类。如果我们还想要一杯既加奶也加糖的类呢?如果我们想要一杯加了两份奶的咖啡呢?这么一想就会新建出好多的子类……
下面是代码说明:
// 普通咖啡类 public class OriginalCoffee { public void make() { System.out.println("original coffee "); } } // 加奶咖啡类 public class MilkCoffee extends OriginalCoffee { @Override public void make() { super.make(); System.out.println("add milk "); } } // 加糖咖啡类 public class SugarCoffee extends OriginalCoffee { @Override public void make() { super.make(); System.out.println("add sugar "); } } // 加糖加奶咖啡类 public class MilkSugarCoffee extends OriginalCoffee { @Override public void make() { super.make(); System.out.println("add Milk "); System.out.println("add sugar "); } } // 两份奶咖啡类 public class DoubleMilkCoffee extends OriginalCoffee{ } //……
(3) 使用装饰器模式
通过上面的例子,我们可以得出一个结论:继承并不是一个好的扩展对象功能的方法。而聚合相比于继承,是一个更好的扩展对象的方法。而装饰器模式,就是通过聚合来实现扩展对象的功能的。
如何使用装饰器模式?
在使用装饰器模式的过程中,最需要理解的一点就是:
装饰者与被装饰者拥有共同的超类,继承的目的是继承类型,而不是行为
即,装饰器与被装饰对象实现了同样的接口。因此在客户端看来,装饰器与被装饰对象是完全一样的,这样就可以对客户端透明地更改和添加对象的功能。同理,不同功能的装饰器应该也共享同一个接口,这样就能透明的叠加不同的装饰器了。
所以一般来说,装饰器模式大概有如下四种角色:
- Component Interface : 装饰器和被装饰类的共同接口。这个接口为了统一装饰器对象和被装饰对象在客户端的行为表现。
- Concrete Component: 具体的被装饰对象。
- Decorator Interface: 装饰器之间共享的接口或抽象类,可以让各个不同功能的装饰器可以无差别的被使用。
- Concrete Decorator: 具体的装饰器对象,对应不同的功能。
装饰器模式的类图如下
那么上面咖啡馆的案例我们就能用下面的代码实现:
// Component interface: 统一装饰器类和被装饰类的行为表现
public interface ICoffee {
void make();
}
// Component: 被装饰类
public class OriginalCoffee implements ICoffee{
@Override
public void make() {
System.out.println("original coffee ");
}
}
// Decorator Interface
public abstract class CoffeeDecorator implements ICoffee{
protected ICoffee coffee;
public CoffeeDecorator(ICoffee coffee) {
this.coffee = coffee;
}
@Override
public abstract void make();
}
// Concreate Decorator, 加奶装饰器
public class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(ICoffee coffee) {
super(coffee);
}
@Override
public void make() {
this.coffee.make();
System.out.println("add milk");
}
}
// 加糖装饰器
public class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(ICoffee coffee) {
super(coffee);
}
@Override
public void make() {
this.coffee.make();
System.out.println("add sugar");
}
}
// 测试类
public class CoffeeShop {
public static void main(String[] args) {
ICoffee originalCoffee = new OriginalCoffee(); // 被装饰对象
// 加奶咖啡
ICoffee milkCoffee =
new MilkDecorator(
originalCoffee
);
// 加糖咖啡
ICoffee sugarCoffee =
new SugarDecorator(
originalCoffee
);
// 加糖加奶咖啡
ICoffee sugarMilkCoffee =
new SugarDecorator(
new MilkDecorator(
originalCoffee
)
);
// 加双份奶咖啡
ICoffee doubleMilkCoffee =
new MilkDecorator(
new MilkDecorator(
originalCoffee
)
);
}
}
从上面的例子我们可以看出,直接把功能封装成了一个个独立的装饰器,然后根据需求往被装饰对象上堆就行了,简单粗暴,并且特别灵活。
总结
装饰器模式的优点
- 相比于继承,通过装饰器扩展对象的功能更加灵活,通过对不同的修饰器类进行排列组合,可以创造出很多新的行为模式。
- 避免了继承方法生成的各种子类,节约了系统资源。
实现步骤
- 确保业务逻辑可以用一个基本组件以及多个额外可选层次构成。
- 找出基本组件和可选层次的通用方法。创建一个接口然后声明这些方法
- 创建一个具体组件类,并在其中定义这些方法的基础行为。
- 创建修饰器基类,然后用一个成员变量存储被装饰对象的引用。该成员变量必须被声明为组件接口类型。装饰基类将所有工作委派给被装饰的对象
- 确保所有类实现组件接口。
- 客户端代码负责创建被装饰对象,各类装饰器并将其组合成客户端所需的形式。