10-蒸馒头:装饰者模式
10.1 蒸馒头事件
本例使用蒸馒头的事例来讲述装饰者模式。众所周知,馒头有很多种口味的,例如,普通的白馒头、加了糖的馒头、添加了玉米面的玉米馒头、同时加了糖和玉米面的甜玉米馒头……
10.2 模式定义
装饰者模式(Decorator Pattern),是在不改变原类文件、不使用继承的情况下,动态地扩展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。
使用装饰者模式的时候需要注意以下几点内容:
1)装饰对象和真实对象有相同的接口。这样客户端对象就可以以和真实对象相同的方式和装饰对象交互。
2)装饰对象包含一个真实对象的引用。
3)装饰对象接受所有来自客户端的请求,并把这些请求转发给真实的对象。
4)装饰对象可以在转发这些请求以前或以后增加一些附加功能。这样就确保了在运行时,不用修改给定对象的结构就可以在外部增加附加功能。在面向对象的设计中,通常是通过继承来实现对给定类的功能扩展。然而,装饰者模式不需要子类,可以在应用程序运行时动态扩展功能,更加方便、灵活。
10.3 一般化分析
下面,我们来分析一下玉米馒头的制作过程:
1)需要生产普通的馒头逻辑。
2)加入玉米面到普通面粉中。
3)通过和面机搅拌,最后生产出玉米馒头。
一般的逻辑思维如何实现上述过程呢?我们来这样设计:定义一个生产馒头的接口,普通馒头实现该接口,而玉米馒头继承普通馒头类,不同的是玉米馒头在具体实现的时候,增加了个性的行为(添加玉米面)。通过分析,建立逻辑结构如下图:
10.4 一般化实现
10.4.1 创建馒头接口
package com.demo.abs; /** * Created by lsq on 2018/3/19. * 馒头加工接口 */ public interface IBread { //准备材料 public void prepair(); //和面 public void kneadFlour(); //蒸馒头 public void steamed(); //加工馒头方法 public void process(); }
10.4.2 生产普通馒头
1. 创建普通馒头的实现类
package com.demo.abs; /** * Created by lsq on 2018/3/19. * 普通馒头的实现 */ public class NormalBread implements IBread{ //准备材料 @Override public void prepair() { System.out.println("准备面粉、水以及发酵粉……"); } //和面 @Override public void kneadFlour() { System.out.println("和面……"); } //蒸馒头 @Override public void steamed() { System.out.println("蒸馒头……香喷喷的馒头出炉了!"); } /** * 加工馒头方法 */ @Override public void process() { //准备材料 prepair(); //和面 kneadFlour(); //蒸馒头 steamed(); } }
2. 生产普通馒头
package com.demo; import com.demo.abs.IBread; import com.demo.abs.NormalBread; /** * Created by lsq on 2018/3/19. * 客户端应用程序 */ public class Client { public static void main(String[] args) { //正常生产的馒头 System.out.println("====开始生产正常馒头……"); //创建普通的馒头实例 IBread normalBread = new NormalBread(); //加工馒头 normalBread.process(); System.out.println("====正常馒头生产结束……"); } }
运行程序,结果如下:
10.4.3 玉米馒头
1. 玉米馒头实现——CornBread
package com.demo.common; import com.demo.abs.NormalBread; /** * Created by lsq on 2018/3/19. * 添加玉米面的玉米馒头 */ public class CornBread extends NormalBread{ //开始添加玉米面 public void paint(){ System.out.println("添加玉米面……"); } //重载父类的和面方法 @Override public void kneadFlour() { //在面粉中添加玉米面之后才开始和面 this.paint(); //和面 super.kneadFlour(); } }
2. 生产玉米馒头
package com.demo; import com.demo.abs.IBread; import com.demo.abs.NormalBread; import com.demo.common.CornBread; /** * Created by lsq on 2018/3/19. * 客户端应用程序 */ public class Client { public static void main(String[] args) { //正常生产的馒头 System.out.println("====开始生产正常馒头……"); //创建普通的馒头实例 IBread normalBread = new NormalBread(); //加工馒头 normalBread.process(); System.out.println("====正常馒头生产结束……"); System.out.println("\n====开始生产添加玉米面的玉米馒头……"); //创建添加玉米面的玉米馒头实例 IBread normalBread2 = new CornBread(); //加工馒头 normalBread2.process(); System.out.println("====添加玉米面的玉米馒头生产结束……"); } }
运行程序,结果如下:
我们看到,玉米面馒头生产出来了。在玉米馒头的和面方法中,执行了添加玉米面的方法,从而达到了生产玉米馒头的目的。
10.4.4 甜馒头
同样的,我们可以新增一个类继承普通馒头类,然后重载和面的方法即可。
1. 甜馒头实现——SweetBread
package com.demo.common; import com.demo.abs.NormalBread; /** * Created by lsq on 2018/3/19. * 甜馒头实现类 */ public class SweetBread extends NormalBread{ //开始添加糖 public void paint(){ System.out.println("添加糖……"); } //重载父类的和面方法 @Override public void kneadFlour() { //在面粉中加入糖之后才开始和面 this.paint(); //和面 super.kneadFlour(); } }
2. 生产甜馒头
package com.demo; import com.demo.abs.IBread; import com.demo.abs.NormalBread; import com.demo.common.CornBread; import com.demo.common.SweetBread; /** * Created by lsq on 2018/3/19. * 客户端应用程序 */ public class Client { public static void main(String[] args) { //正常生产的馒头 System.out.println("====开始生产正常馒头……"); //创建普通的馒头实例 IBread normalBread = new NormalBread(); //加工馒头 normalBread.process(); System.out.println("====正常馒头生产结束……"); System.out.println("\n====开始生产添加玉米面的玉米馒头……"); //创建添加玉米面的玉米馒头实例 IBread normalBread2 = new CornBread(); //加工馒头 normalBread2.process(); System.out.println("====添加玉米面的玉米馒头生产结束……"); System.out.println("\n开始生产甜馒头……"); //创建甜馒头实例 IBread normalBread3 = new SweetBread(); //加工馒头 normalBread3.process(); System.out.println("====甜馒头生产结束……"); } }
执行程序,运行结果如下:
10.4.5 如何生产甜玉米馒头?
现在我们有了玉米馒头,有了甜馒头,但是,如果我想要甜的玉米馒头,该怎么办呢?按照现在的思路,再创建一种馒头,继承普通馒头类,然后再重载父类的和面方法。不可否认,这样是可以达到我们的要求。然而,这样将会导致普通馒头类会有庞大的继承关系图。继承方式存在这样两点不利因素:
1)父类的依赖程度过高,父类修改会影响子类的行为;
2)不能复用已有的类,造成子类过多。
为了解决这个问题,我们尝试使用装饰者模式来重新实现蒸馒头的实例。
10.5 装饰者模式实现
10.5.1 创建抽象装饰者
为了装饰普通馒头NormalBread,我们需要一个和普通馒头一样的抽象装饰者:AbstractBread,该类和普通馒头NormalBread一样实现IBread馒头接口。不同的是,该抽象类含有一个IBread接口类型的私有属性bread,然后通过构造方法,将外部IBread接口类型对象传入。在AbstractBread抽象类中,调用bread对象实现IBread接口的各个方法,内容如下所示:
package com.demo.decorator; import com.demo.abs.IBread; /** * Created by lsq on 2018/3/19. * 抽象装饰者 */ public abstract class AbstractBread implements IBread{ //存储传入的IBread对象 private final IBread bread; //构造方法 public AbstractBread(IBread bread){ this.bread = bread; } //准备材料 public void prepair(){ this.bread.prepair(); } //和面 public void kneadFlour(){ this.bread.kneadFlour(); } //蒸馒头 public void steamed(){ this.bread.steamed(); } //加工馒头方法 public void process(){ prepair(); kneadFlour(); steamed(); } }
10.5.2 创建装饰者
1. 创建添加玉米面装饰者——CornDecorator
继承AbstractBread类,含有个性行为:添加玉米面。
package com.demo.decorator; import com.demo.abs.IBread; /** * Created by lsq on 2018/3/19. * 添加玉米面的玉米馒头 */ public class CornDecorator extends AbstractBread{ public CornDecorator(IBread bread) { super(bread); } //开始添加玉米面 public void paint() { System.out.println("添加玉米面……"); } //重载父类的和面方法 @Override public void kneadFlour() { //在面粉中加入玉米面之后才开始和面 this.paint(); //和面 super.kneadFlour(); } }
2. 创建甜馒头装饰者——SweetDecorator
package com.demo.decorator; import com.demo.abs.IBread; /** * Created by lsq on 2018/3/19. * 甜馒头 */ public class SweetDecorator extends AbstractBread{ public SweetDecorator(IBread bread) { super(bread); } //开始添加糖 public void paint() { System.out.println("添加糖……"); } //重载父类的和面方法 @Override public void kneadFlour() { //在面粉中加入糖之后才开始和面 this.paint(); //和面 super.kneadFlour(); } }
10.5.3 生产甜玉米馒头
package com.demo; import com.demo.abs.IBread; import com.demo.abs.NormalBread; import com.demo.decorator.CornDecorator; import com.demo.decorator.SweetDecorator; /** * Created by lsq on 2018/3/19. * 应用程序 */ public class DecoratorClient { public static void main(String[] args) { //生产装饰馒头 System.out.println("\n====开始装饰馒头……"); //创建普通的正常馒头实例 //这是我们需要包装(装饰)的对象实例 IBread normalBread = new NormalBread(); //对正常馒头进行装饰 //使用糖装饰馒头 normalBread = new SweetDecorator(normalBread); //使用玉米面装饰馒头 normalBread = new CornDecorator(normalBread); //生产馒头信息 normalBread.process(); System.out.println("装饰馒头结束……"); } }
运行结果:
可以看到,动态地实现了对普通馒头的装饰,而不是使用继承方式。但是,对打印结果你可能感到奇怪,我们先添加的是糖,然后添加的是玉米面,那么结果应该是先打印添加糖,然后打印添加玉米面才是,现在怎么反过来了?
因为装饰者对原对象的包装,就像礼品盒的多层包装。从最内层开始,但是最先展现给我们的却是最外层的。在上面的应用程序中,装饰者的调用关系如下图所示:
10.6 使用场合
1)当我们需要为某个现有的对象动态地增加一个新的功能或职责时,可以考虑使用装饰者模式;
2)当某个对象的职责经常发生变化或经常需要动态地增加职责,避免为了适应这样的变化而增加继承子类扩展的方式,因为这种方式会造成子类膨胀的速度过快,难以控制,此时可以使用装饰者模式。
下面,我们来看一下装饰者模式的静态类图,使我们对装饰者模式有一个更加清晰的认识。
如图所示,装饰者模式以对客户端透明的方式动态地给一个对象附加更多的责任。换言之,客户端并不会觉得对象在装饰前和装饰后有什么不同。装饰者模式可以在不创建更多子类的情况下,将对象的功能加以扩展,装饰者模式使用原来被装饰的类的一个子类实例,把客户端的调用委派到被装饰类。
装饰者模式中的角色:
1)被装饰者抽象Component:是一个接口或抽象类,就是定义最核心的对象,也是最原始的对象。在装饰模式中,必然有一个被提取出来最核心、最原始、最基本的接口或抽象类,这个类就是我们需要装饰类的基类。例如本例中的IBread接口。
2)被装饰者具体实现ConcreteComponent:这是Component类的一个实现类,我们要装饰的就是这个具体的实现类。例如本例中的普通馒头类NormalBread。
3)装饰者Decorator:一般是一个抽象类,做什么呢?实现接口或者抽象方法,它里面必然有一个指向Component变量的引用。例如本例中的抽象装饰者AbstractBread。
4)装饰者实现ConcreteDecorator1和ConcreteDecorator2:是具体的装饰者类,用来装饰最基本的类。例如,本例中的添加玉米面装饰者CornDecorator和添加糖装饰者SweetDecorator。