Java设计模式-装饰模式

装饰(Decorator)模式,有时也被称为包装(Wrapper)模式,在23种设计模式中属于对象结构型模式。

装饰模式以对客户端透明的方式扩展对象的功能,换句话说,客户端并不会觉得对象在装饰前和装饰后有什么区别。装饰者提供了比继承更有弹性的替代方案。

模式介绍

下图可以更方便理解装饰模式。

在图片中第二列展示的是不同风格的相框,如果使用继承机制,那么最后一列的相框风格就不是第二列的后两种风格的组合而成的了,而是将第二列最后一种风格再继承自中间风格的相框,这就解释了为什么说,装饰者提供了比继承更有弹性的替代方案。

在平常编程时,经常听说,要学会抽象。抽象的目的是什么,抽象其实是为了找出代码中最可能需要变化的地方,把它们独立出来,不要和那些不需要变化的代码混在一起。再进一步,如果抽象时使用了太多的继承怎么办?已经抽象过了,可是后来发现,系统设计的依然很臃肿,代码结构仍然很复杂,只能说抽象的方向可能没有找对。

假设一个类A,有子类A01、A02,然后A01又有子类A001,以此类推,A0001,A00001,A00002。。。不能否定,很多代码就是这样被设计的。但是,经过分析细化后,发现A类还可以从另一个维度抽象出B01,B02,这样分离,后代的组合是不是明显增多了,比如A01B01,A01B02。这就是要说的抽象的另一个层面,要学会从不同的维度抽象。

一般,继承是有两个目的的,其一,是为了继承父类的行为,其二,是为了达到和父类具有相同类型的目的。在各种设计模式的编程书籍中,总是滔滔不绝不厌其烦的提醒开发者,要多用组合少用继承。装饰模式就是一个很好的示例,不仅会让我们认识到继承的好处,同样也会认识到组合的优势

适用性

  • 在不影响其它对象的情况下,以透明、动态地给某单个对象添加职责。
  • 需要动态地给一个对象添加功能,这些功能也可以动态地撤销。
  • 当不能采用继承方式对系统进行扩展或者采用继承不利于系统的扩展和维护时。一种情况是,可能有大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈现爆炸式的增长;另外情况可能是因为类的定义被隐藏,如@hide注释的类,或者定义的类不能生成子类,如final修饰的类。

示例介绍

假设为某个咖啡店设计一个订单系统,由于咖啡种类不同,可以先为所有的咖啡设计一个共同的父类,这样所有的咖啡都继承自该父类。

public abstract class CoffeeComponent {

	// 对于咖啡的简要描述
	protected String desc = "Unknown Coffee";

	public String getDesc() {
		return desc;
	}

	// 因为每种咖啡的价格不一样,所以const设计为抽象方法
	public abstract double cost();
}

经过初步分析设计,该订单系统的抽象方式基本可以实现不同种类咖啡的需要。但是要知道,一般点咖啡时,还多少会添加一些调料,在上文也分析过如何抽象,可以将调料也抽象出一个维度,这样咖啡和调料两个维度的组合,可以让订单系统更容易扩展。

再进一步分析,如何将几十种调料放到咖啡的业务场景中,将每一种调料作为CoffeeComponent成员变量,一下在CoffeeComponent中多了几十种成员变量,然后根据用户的选择,使用条件语句组合计算出所选调料的总价,最后与咖啡的价格累加,这种实现方式是我们所希望见到的吗?面向对象的设计模式,十分忌讳的就是庞大的多分支条件语句。另外,后续如果需要添加或者剔除某种调料,都需要更改CoffeeComponent源码,这一点也违反了软件设计的开闭原则。

其实这里要强调一点,并不是说在设计程序时每一个地方都需要遵循开闭原则,因为这样可能不仅浪费时间和经历,还可能造成代码复杂且难以理解。

分析到这里,装饰模式终于可以登场了。上文分析过,一般继承有两个目的,在装饰模式中两个目的刚好都使用到了。将每一种调料都作为咖啡装饰一下,并将咖啡对象包起来(将咖啡对象作为调料的成员变量),当调料使用cost()计算所需价格时,可以叠加上咖啡对象的价格。

public class Espresso extends CoffeeComponent{
	public Espresso(){
		desc = "Espresso";
	}
	
	@Override
	public double cost() {
		return 10.99;
	}
}
public abstract class CondimentDecorator extends CoffeeComponent {
	protected CoffeeComponent coffee;
	
	public CondimentDecorator(CoffeeComponent coffee){
		this.coffee=coffee;
	}
	
	public abstract String getDesc();

}
public class Whip extends CondimentDecorator {
	public Whip(CoffeeComponent coffee) {
		super(coffee);
	}

	@Override
	public String getDesc() {
		return coffee.getDesc() + ", Whip";
	}

	@Override
	public double cost() {
		return coffee.cost()+2.99;
	}
}
public static void main(String[] args) {
	CoffeeComponent coffee01 = new Espresso();
	System.out.println(coffee01.getDesc() + " ¥" + coffee01.cost());

	coffee01 = new Milk(coffee01);
	coffee01 = new Mocha(coffee01);
	System.out.println(coffee01.getDesc() + " ¥" + coffee01.cost());

	CoffeeComponent coffee02 = new HouseBlend();
	coffee02 = new Lemon(coffee02);
	coffee02 = new Mocha(coffee02);
	coffee02 = new Whip(coffee02);
	System.out.println(coffee02.getDesc() + " ¥" + coffee02.cost());
}
// Espresso ¥10.99
// Espresso, Milk, Mocha ¥16.97
// House Blend Coffee, Lemon, Mocha, Whip ¥17.96

模式分析

装饰模式的一般类图如下:

  • Component:定义一个对象接口,可以给这些对象动态地添加职责。
  • ConcreteComponent:定义一个对象,可以给这个对象添加一些职责。
  • Decorator:维持一个指向Component对象的引用。一般也会直接实现Component接口,以达到相同父类型的目的。
  • ConcreteDecorator:向组件添加职责。

在使用装饰模式是一定要注意,装饰对象的接口(Decorator)必须要和它所要装饰的组件接口(Component)一致。示例中所有的调料最终都是继承自咖啡组件CoffeeComponent。一般开发中,会将装饰对象重新抽取一个公用的父类,即使仅仅是继承组件类什么都不做,这样做就是为了明确职责,代码结构也更加清晰。比如后续需要新增调料,新调料类直接继承自Decorator即可,后续有新的咖啡,直接继承Component。

保持Component类的简单性,为了保证接口的一致性,组件和装饰必须有一个公共的Component父类。因此保持这个类的简单性是很重要的,否则Component类会变得过于复杂和庞大,因而难以大量使用。

模式优缺点

模式优点

装饰模式和继承都可以扩展对象的功能,但是装饰模式提供了更加灵活的方式向对象添加职责。

在创建装饰类时,可以先从不同维度进行抽象,这样可以创建更多不同行为的组合。可以使用多个不同的装饰类装饰同一个对象,得到功能更加强大的对象。

避免了在高层次的类有太多的特征,它并不试图在一个复杂的可定制的类中支持所有可预见的特征,相反,而是定义一个简单的类,并且用Decorator类给它逐渐地添加功能。可以从简单的部件组合出复杂的功能。这样,应用程序不必为不需要的特征付出代价。同时也更易于不依赖于Decorator所扩展(甚至是不可预知的扩展)的类而独立地定义新类型的Decorator。扩展一个复杂类的时候,很可能会暴露与添加的职责无关的细节。

模式缺点

使用装饰模式会产生许多小的对象,如果过度使用,会让程序变得复杂。

虽然装饰模式比继承更具有灵活性,但是也同样意味着装饰模式比继承更加容易出错。

与其它模式对比

适配器(Adapter)模式:装饰模式不同于适配器模式,适配器模式会给对象一个全新的接口,而装饰模式是改变对象的职责,并不会改变对象接口。

组合(Composite)模式:可以将装饰装饰模式视为一个退化的、仅有一个组件的组合(示例中调料类保存了一个咖啡的引用)。然而,装饰仅给对象添加一些额外的职责—它的目的不在于对象聚集。

策略(Strategy)模式:装饰模式主要改变的对象的外表;而策略模式重点在于改变对象的内核。它们是改变对象的两种途径。

小结

装饰(Decorator)模式,有时也被称为包装(Wrapper)模式,其实包装模式可能还更容易理解,图形如下:

本文内容不多,就介绍了一个结构型设计模式。

装饰模式的核心在于不改变对象原功能基础上,动态新增功能。这里所说的动态新增功能,并不是将所需新增的功能作为原对象的一个属性添加进去,也不是采用继承原对象后再在子类中新增行为,而是将需要添加的功能对象装饰成与原对象相同的类型。

在上文示例中,装饰者与组件使用的相同的行为(计算价格)就可以满足需要。在实际应用中,有可能相同的行为并不能满足装饰者和组件的共同需求,那么装饰者还可以在组件行为的前面或者后面再附件上自己的其它行为(类似代理模式),甚至将被组件的行为整个取代掉,而达到特定的目的。

作为开发人员,不能为了学习设计模式而学习设计模式,其实这样并不会让代码写得更清晰,反而可能陷入某种过度设计的争论中。

示例源代码下载,提取码:anp5

评论

您确定要删除吗?删除之后不可恢复