1 引子
大家都喝过奶茶,我们知道奶茶可以根据自己的喜好来加一些配料,如珍珠、糖、红豆等使得奶茶更好喝。
在这可以简单方式实现上述过程:
奶茶类MilkyTea:可以加各种配料组合成一杯可口的奶茶
public class MilkyTea {
public MilkyTea(){
System.out.println("只有奶茶,什么也没加...");
}
public void addPearl(){
System.out.println("加一份珍珠....");
}
public void addRedBean(){
System.out.println("加一份红豆...");
}
public void addSugar(){
System.out.println("加半糖...");
}
}
门店类Client:根据客户的需要定制一杯奶茶
public class Drink {
public static void main(String[] args) {
System.out.println("================第一份奶茶==============");
MilkyTea milkyTea = new MilkyTea();
milkyTea.addPearl();
milkyTea.addRedBean();
System.out.println("================第二份奶茶==============");
MilkyTea milkyTea2 = new MilkyTea();
milkyTea2.addPearl();
milkyTea2.addSugar();
}
}
运行结果:
================第一份奶茶==============
只有奶茶,什么也没加...
加一份珍珠....
加一份红豆...
================第二份奶茶==============
只有奶茶,什么也没加...
加一份珍珠....
加半糖...
对于上面的过程,有以下几个缺点:
①如果新进一种配料,则需要在MilkyTea类中新添加一个方法;
②并非所有的MilkyTea类都可以加糖,即可以调配的MilkyTea类需要多种;
可以看出,MilkyTea类的可扩展性差,且新增会违背“开闭原则”;究其原因,奶茶与配料的耦合度较高,要想满足类的设计原则,提高可扩展性,则需要把奶茶类与各种配料进行解耦,使得客户想加什么配料都能加,且新加的配料不会影响原有的奶茶。
为了解决以上的缺点,构造新的生产奶茶的过程。
为了奶茶好喝,需要加各种配料:
// 接口
public interface Enjoy {
void addBatching();
}
原味奶茶MilkyTea类:
// 为了好喝 奶茶还需要加点配料
public class MilkyTea implements Enjoy {
@Override
public void addBatching() {
System.out.println("只有奶茶,没加任何配料....");
}
}
抽象配料Batching类:
// 抽象配料类
public abstract class Batching implements Enjoy {
Enjoy enjoy;
public Batching(Enjoy enjoy){
this.enjoy = enjoy;
}
@Override
public void addBatching(){
enjoy.addBatching();
}
}
下面是各种配料
珍珠配料Pearl类:
// 珍珠配料
public class Pearl extends Batching {
public Pearl(Enjoy enjoy) {
super(enjoy);
}
@Override
public void addBatching(){
super.addBatching();
System.out.println("加一份珍珠....");
}
}
红豆配料RedBean类:
// 红豆配料
public class RedBean extends Batching {
public RedBean(Enjoy enjoy) {
super(enjoy);
}
@Override
public void addBatching(){
super.addBatching();
System.out.println("加一份红豆....");
}
}
糖配料Sugar类:
// 糖配料
public class Sugar extends Batching {
public Sugar(Enjoy enjoy) {
super(enjoy);
}
@Override
public void addBatching(){
super.addBatching();
System.out.println("加半糖...");
}
}
暂且就三种配料,下面是客户类的需求:
public class Client {
public static void main(String[] args) {
System.out.println("==============第一份奶茶==============");
MilkyTea milkyTea = new MilkyTea();
Enjoy sugar = new Sugar(milkyTea);
Enjoy pearl = new Pearl(sugar);
Enjoy redBean = new RedBean(pearl);
redBean.addBatching();
System.out.println("===============第二份奶茶=============");
MilkyTea milkyTea2 = new MilkyTea();
Enjoy pearl2 = new Pearl(milkyTea2);
pearl2.addBatching();
}
}
运行结果:
==============第一份奶茶==============
只有奶茶,没加任何配料....
加半糖...
加一份珍珠....
加一份红豆....
===============第二份奶茶=============
只有奶茶,没加任何配料....
加一份珍珠....
达到的效果跟第一种方式一样,但是过程却不一样。很明显,第二种的灵活性要比第一种高,且能满足各种不同客户的要求。类似于这种结构的模式称为装饰模式。上面过程的uml如下:
2 装饰模式的原理
《大话设计模式》中这样定义装饰模式:动态的给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成子类更灵活。
这里是给生成的对象,按需添加职责,在上面的例子就是给milkyTea对象添加各种配料;给对象添加职责比通过父子类添加职责更灵活。子类继承父类的职责,且子类可以添加额外的职责,这种新添加的职责使得子类与父类耦合在一起,无法灵活扩展,只有不停地新增子类才能满足实际需求。装饰模式的UML类图:
装饰模式UML类图中的角色有:
“抽象构建角色Component”:抽象接口,是用来规范准备接收附加责任的对象。
“具体构建角色ConcreteComponent”:定义一个将要接收附加责任的类。
“装饰角色Decorator”:持有一个构建对象的实例,并定义一个与抽象构建接口一致的接口或者抽象类。
“具体装饰角色ConcreteDecoratorA/ConcreteDecoratorB”:负责给构建对象增加额外的责任。
注意:装饰角色和具体构建角色实现同一个接口,是为了继承类型,而不是行为。意思就是说,装饰角色和具体构建角色在客户端具有共同的类型,使得各种配料对客户端完全透明。如果装饰角色没有实现Component接口,则客户端需要构造具体的装饰角色对象,且装饰角色繁杂。
3 装饰模式的特点
装饰模式有哪些优点呢?
装饰模式具有较强的灵活性,遵守类的设计原则,即“开闭原则”。另外,可动态的给构建实例增加新的“装饰”,且不影响构建实例本身;相比于继承关系,装饰模式扩展性更好,可随意把 “装饰实例”贴给构建实例,而继承在运行前就确定了类的功能。
装饰模式有哪些缺点?
装饰模式设计上较难理解,特别是接口类。
注意:如果只有一个ConcreteConponent类而没有抽象的Component类,那么Decorator类可以是ConcreteComponent的一个子类。同样的道理,如果只有一个ConcreteDecorator类,那么就没有必要建立一个单独的Decorator类,而可以把Decorator和ConcreteDecorator的责任合并成一个类。
4 装饰模式的适用场景
装饰模式在java的I/O库中有运行,具体可看下面参考资料第一个链接。
5 参考资料
《大话设计模式》
http://www.cnblogs.com/java-my-life/archive/2012/04/20/2455726.html
https://www.jianshu.com/p/d7f20ae63186
https://blog.csdn.net/l_bestcoder/article/details/66967740