例说装饰者模式(Decorator Pattern)

前言

   装饰者模式在餐饮行业有着比较广泛的应用,网上大部分关于该模式的例子都和饮食相关,以前看译制片电影,每当看到老外们在咖啡店一口流利的点咖啡要加糖要加奶昔要加这加那的时候,感觉好有派~好高大上啊~,为啥我在小卖部都是“来瓶汽水”就没话说了呢~,难道是我不会“装”?
 

官方定义

   动态的给一个对象添加一些职责,就增加功能来说,该模式比生成子类更为灵活——GOF
 
   Decorator模式是一种相对简单的对象结构性模式,动态和对象是个对应的关系,正如静态和类这样的对应关系,编译时能够决定的特质是静态特质,动态则表示在运行时进行操作,传统情况下使用的继承是静态的给类添加职责,动态的给对象添加职责,则是装饰者模式所要完成的事。
   给类和给对象添加职责有什么不同呢,前者的灵活性要差,如果需要添加的职责很多,前者需要为每种情况都定义一个固定类,这里的每种情况指的是职责的排列组合,假如我要为一个原始类添加5个职责,则会出现5的阶乘种情况,不仅仅是类爆炸带来的繁琐,运行时对职责的修改是任意的,这就使得编译时确定的类又要在运行时频繁改变,而直接往对象中添加职责则使的结构可以灵活多变,同样的情况,我只需要额外增加5个修饰类就可以完美解决类爆炸及运行时改变的情况,这就是上述装饰者模式定义所要表达的具体含义。
 

 场景

        产生类爆炸的原因很多,并不是每种情况都可以用装饰者模式来解决,该模式应用的典型场景需满足以下三点
 1 原始组件(被装饰者)和装饰者符合逻辑上的修饰关系时
        这个比较好理解,HeadFirst设计模式举了个为咖啡添加不同口味的配料,不同口味的配料对应一个装饰者,通常来讲,代码结构要反映出逻辑关系,无论是哪种口味的配料,他对咖啡对象确实产生了修饰关系,网上关于装饰者模式的举例大部分是关于饮食类,这是有道理的,不同口味或者配料对应不同的装饰者,这样是符合现实世界的逻辑关系的。
 2 装饰者之间是可以独立存在的
   还是继续咖啡的例子,每种口味之间都是独立关系,互相之间没有依赖关系,这个根据客户的需求,有人喜欢摩卡口味,有人喜欢奶油口味,这两种修饰者可以独立修饰咖啡的,当然有人喜欢摩卡奶油咖啡,这个仅仅是修饰顺序的问题,不影响两种口味的独立性。如果修饰者之间不满足这种独立性,使用装饰者模式是不合理得。
 3 当无法确定原始组件被装饰的方式和时机时
   一杯咖啡,根据客户需求的不同,添加不同口味的配料,客户会喜欢那种口味或者哪几种,事先是不确定的,咖啡店也不需要事先确定,他们会把不同口味的配料放在不同的机器中,随要随取,灵活应对需求,如果需要新口味,添加一个新机器就可以,扩展灵活,至少这些咖啡店的老板,都是懂装饰者模式的。
 

举例

           装饰者模式的经典例子很多,没有必要重复造轮子,就接着咖啡这个例子来说吧。装饰者模式最核心的部分,就是从下图开始

例说装饰者模式(Decorator Pattern)_第1张图片

 

     一杯咖啡代表被装饰的组件component,咖啡与被辅料之间有两种关系,组合与继承,IS-A和Has-A的关系,HAS-A比较好理解,通过组合一个被装饰对象,可以使用被装饰对象的操作并进行装饰,IS-A则和通常情况有区别,辅料(Decorator)本质上并不是咖啡,所以这里的继承关系并不是类继承,而是接口(协议)继承,其含义就是经过辅料修饰后的咖啡仍然具有咖啡本身所含有的接口特性,也就是描述和价格,不能把这个继承关系理解成调料也是一种咖啡,之所以使用继承的原因是可以方便灵活的在运行时动态添加职责,后面的测试例结果可以看出这一点。
   和上述UML图对于的源码如下
   Cafe.java
package com.klpchan.example.cafe;

public abstract class Cafe {
	
	public String getDescription() {
		return description;
	}
	
	public abstract float getPrice();
	
	String description = "This is Cafe";
}
    Decorator.java
package com.klpchan.example.cafe;

public abstract class Decorator extends Cafe{

	public Decorator(Cafe _cafe) {
		// TODO Auto-generated constructor stub
		this.cafe = _cafe;
	}
	
	@Override
	public String getDescription() {
		// TODO Auto-generated method stub
		return cafe.getDescription();
	}

	@Override
	public float getPrice() {
		// TODO Auto-generated method stub
		return cafe.getPrice();
	}

	Cafe cafe;
}
   咖啡作为被修饰者,含有很多具体的子类,顾客选择时首先要选择具体的某种咖啡(具体component),然后在为其选择口味(具体Decorator),本例选择脱脂(DECAF)和意式(Espresso)两种咖啡,第二图UML图诞生了

例说装饰者模式(Decorator Pattern)_第2张图片

 
 
   两种具体的咖啡是被修饰的具体类,各自定义了咖啡的价格和说明,源码如下
 DeCaf.java
package com.klpchan.example.cafe;

public class DeCaf extends Cafe{

	public DeCaf() {
		// TODO Auto-generated constructor stub
		description = "This is DECAF cofe";
	}
	
	@Override
	public float getPrice() {
		// TODO Auto-generated method stub
		return Constants.CAFE_DECAF_PRICE;
	}
}
Espresso.java
package com.klpchan.example.cafe;

public class Espresso extends Cafe{

	public Espresso() {
		// TODO Auto-generated constructor stub
		description = "This is Espresso " ;
	}
	
	@Override
	public float getPrice() {
		// TODO Auto-generated method stub
		return Constants.CAFE_ESPRESSO_PRICE;
	}
}
 
         选择好咖啡后,用户开始选择口味配料(Decorator),这就是装饰模式的核心,本例假设有三种配料
   摩卡(Mocha)、奶油(Milk)和巧克力(Chocolate),每种配料都有各自的价格,价格表具体如下
package com.klpchan.example.cafe;

public class Constants {
	//脱脂和意式两种咖啡的基本价格
	public static final float CAFE_DECAF_PRICE = 8;
	public static final float CAFE_ESPRESSO_PRICE = 9;
	
	//摩卡、牛奶、巧克力三种口味调料的价格
	public static final float DECORATOR_MOCHA_PRICE = 0.5f;
	public static final float DECORATOR_MILK_PRICE = 0.4f;
	public static final float DECORATOR_CHOCOLATE_PRICE = 0.8f;
}
        通过添加具体的配料类来修饰咖啡,UML图如下
 

例说装饰者模式(Decorator Pattern)_第3张图片

 

   添加了三个具体的配料类摩卡、奶油和巧克力,源码如下

MochaDecorator.java
package com.klpchan.example.cafe;

public class MochaDecorator extends Decorator{

	public MochaDecorator(Cafe _cafe) {
		super(_cafe);
		// TODO Auto-generated constructor stub
	}

	@Override
	public String getDescription() {
		// TODO Auto-generated method stub
		return super.getDescription() + " add mocha ";
	}

	@Override
	public float getPrice() {
		// TODO Auto-generated method stub
		return super.getPrice() + Constants.DECORATOR_MOCHA_PRICE;
	}	
}
 
MilkDecorator.java
package com.klpchan.example.cafe;

public class MilkDecorator extends Decorator{

	public MilkDecorator(Cafe _cafe) {
		super(_cafe);
		// TODO Auto-generated constructor stub
	}

	@Override
	public String getDescription() {
		// TODO Auto-generated method stub
		return super.getDescription() + " add milk ";
	}

	@Override
	public float getPrice() {
		// TODO Auto-generated method stub
		return super.getPrice() + Constants.DECORATOR_MILK_PRICE;
	}	
}
ChocolateDecorator.java
package com.klpchan.example.cafe;

public class ChocolateDecorator extends Decorator{

	public ChocolateDecorator(Cafe _cafe) {
		super(_cafe);
		// TODO Auto-generated constructor stub
	}
	
	@Override
	public String getDescription() {
		// TODO Auto-generated method stub
		return super.getDescription() + " add chocolate ";
	}

	@Override
	public float getPrice() {
		// TODO Auto-generated method stub
		return super.getPrice() + Constants.DECORATOR_CHOCOLATE_PRICE;
	}	
}
         在每一个具体的修饰类中,新方法使用了被修饰对象本身的方法并加上了新的特性,本例中是加上了说明和配料价格,新方法可以用在被修饰对象方法的前或者后,可以动态改变被修饰对象的状态和操作,被修饰对象(咖啡)并不需要知道修饰者(调料)到底做了那些操作,这些都是透明的。
 
   在客户文件写了个测试例如下
  
		Cafe cafe = new DeCaf();
		Cafe mochaChocCafe = new MochaDecorator(new ChocolateDecorator(cafe));
		System.out.println(mochaChocCafe.getDescription() + " Price is " + mochaChocCafe.getPrice());
		
		Cafe cafe2 = new Espresso();
		Cafe milkChocMochaCafe = new MochaDecorator(new ChocolateDecorator(new MilkDecorator(cafe2)));
		System.out.println(milkChocMochaCafe.getDescription() + " Price is " + milkChocMochaCafe.getPrice());
 
        可以看出对于一个选定的咖啡类,用户可以自由组合其装饰者,这就是较继承灵活的体现,运行结果如下
This is DECAF cofe add chocolate  add mocha  Price is 9.3
This is Espresso  add milk  add chocolate  add mocha  Price is 10.7
         对于上述源码中的价格表及测试源码,可以看出两个订单的说明和价格均被正确修改完成。
 

适用性

  1 以动态透明的方式添加职责,动态前文已提及,透明是指被修饰者不依赖与修饰者,具体咖啡不依赖与调料,以脱脂咖啡为例,该类不需要关系是摩卡还是巧克力来修饰自己,更不关心这些修饰类如何来修饰自己,代码结构中可以表现为咖啡类并不存在修饰类的引用。
      2 处理可以撤销的职责,相当于上述过程的逆向工程,将职责模块化,可以在运行时动态增加删除。
  3 当无法使用继承来添加职责时。除了前文所述的由于职责过多引起的类爆炸,也可能是由于类定义被隐藏使得无法生成子类。
   

结构

   例说装饰者模式(Decorator Pattern)_第4张图片
   这个和上述UML图的结构基本吻合。
   Component 被修饰的组件抽象,本例中表示咖啡类,正如前文所述,装饰者和被装饰者需要同时继承这个抽象类,这样可以动态的为对象添加职责。
   ConcreteComponent 具体的被修饰对象,本例中有脱脂咖啡和意式咖啡,具体组件不关心装饰类如何装饰他们。
   Decorator 装饰者抽象,本例中的调料基类,和组件是继承与组合的关系。装饰基类原封不动的调用被装饰者的操作,供具体修饰者重写。
   ConcreteDecoratorA/B 具体装饰者,本例中的摩卡、牛奶、巧克力装饰类。可以修改被修饰者状态和行为。
 

效果

       1 比静态继承更加灵活,前文已述。
  2 避免在较高层次有较多特征,查看UML结构,组件和装饰者都有着较为简单的特征和操作,在使用该模式时,GOF建议为简单的类逐步添加功能而不是直接修饰一个复杂的类,因为逐步添加的功能可以组合出新的复杂功能,而直接扩展复杂的类容易暴露很多与职责无关的细节,应该尽量保持组件的简洁性,组件应该做的是定义接口一类的工作,本例中的组件Cafe,仅仅定义了基本的说明操作,因为装饰者也需要继承组件,在组件中定义过多且与职责无用的功能,会增加装饰者类的复杂程度,这段内容我理解的也不透彻,欢迎分享讨论~
  3 Decorator与Component不一样,前文已述,被装饰的组件与装饰者的继承关系为接口继承,并非类继承,这样做的目的是有利于多次修饰。我们可以使用一个或多个装饰者来装饰一个对象,装饰后的对象仍然是component对象。
  4 有许多小对象,这个可以参阅本文上述的测试程序,不同的装饰组合会产生的不同的对象,这个是装饰者模式的缺点。
  5 为了解决扩展职责所带来的类爆炸和灵活性问题,该模式使用组合而非继承的格式,继承所产生的大量子类难以维护,更无法应对职责改变所引起的扩展性问题。
 
 

和其它模式的关系

      适配器模式是改变一个对象的接口类型,使他成为能够满足与其它接口相匹配的要求,而装饰者模式是为对象动态透明的添加职责。
   装饰者模式改变外壳,策略模式改变内核。其实对象的外壳和内核是个相对的观点,类似于本例中的咖啡,咖啡的外壳改变就是前文所说口味的变化,口味变化并不影响咖啡的本质,意式咖啡如何加糖也成为不了脱脂咖啡,而如果把意式咖啡在制作过程中的一些工艺替换了,则改变了内核,制作工艺的不同是意式咖啡和其它类型咖啡本质的区别,此时使用策略模式比较合理,这就是两者的区别。
 
 

应用场景

        .NET框架中的装饰者模式应用,结构清晰不赘述了。
   

例说装饰者模式(Decorator Pattern)_第5张图片

 收尾

          装饰者模式是较易理解的常用结构性模式,通过组合而非继承来解决原始类动态添加职责所引起的问题,本文偷个懒,直接改写了HEAD First设计模式的例子,此类例子解释的比较形象,对于一些较为晦涩的理论给出了自己的理解,模式这种东西每个人都会有不同的理解,细节上会有差异,欢迎分享交流,共同进步~
 
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
注:欢迎分享,转载请声明~~
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

你可能感兴趣的:(模式设计)