一、基本概述
问题:有咖啡店,卖多种咖啡,以及调料(如豆浆、牛奶、奶油等)。现有的订单系统的类结构如下。
二、分析说明
上面的设计方式存在的一些问题?
- 调料价钱的改变会使我们更改现有代码。
- 一旦出现新的调料,我们就需要加上新的方法(属性),并改变超类中的Cost()方法。
- 以后可能会开发出新饮料,对这些饮料而言(例如,茶),某些调料可能并不适合,但是在这个设计方式中,Tea(茶)子类仍将继承那些不适合的方法。例如,HasWhip(加奶泡)。
- 若是需要双倍摩卡咖啡,怎么办。
- 违反了OO原则,如针对接口编程,而非针对实现编程。多用组合,少用继承。
小结:
1.尽管继承的威力强大,但是它并不总是能够实现最有效弹性和最好维护的设计。
2.利用继承设计子类的行为,是在编译时静态决定的,而且所有的子类都会继承到相同的行为。然而,如果能够利用组合的做法扩展对象的行为,就可以在运行时动态地进行扩展。
3.我们可以利用此技巧把多个新职责,甚至是设计超类时还没有想到的职责附加在对象上,而且可以不用修改原来的代码。
通过动态地组合对象,可以写新的代码添加新功能,而无需修改现有代码。既然没有改变现有代码,那么引进bug或产生意外副作用的机会将大幅度减少。
三、如何解决
1.OO原则:类应该对扩展开放,对修改关闭。(开放—关闭原则)
补充:
那么我们是否需要系统对设计的每个部分都遵循开放—关闭原则呢?
答案是否定的。通常你办不到,要让OO设计同时具备开放性与关闭性,又不修改现有的代码,需要花费许多时间和努力。一般来说,我们实在没有闲工夫把设计的每个部分都这么设计(而且,就算做得到,也可能是一种浪费)。遵循开放—关闭原则,通常会引入新的抽象层次,增加代码的复杂度,你需要把注意力集中在设计中最有可能改变的地方,然后应用开放—关闭原则。
恰恰装饰者模式符合这一原则。
2.装饰者模式动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案。
补充:
(1) 装饰者和被装饰者对象有相同的超类型。
(2) 你可以用一个或多个装饰者包装一个对象。
(3) 既然装饰者和被装饰者对象有相同的超类型,所以在任何需要原始对象(被包装对象)的场合,可以用装饰者对象代替它。
(4) 装饰者可以在所委托的被装饰者的行为之前与/或之后,加上自己的行为,以达到特定的目地。
(5) 对象可以在任何时候被装饰,所以可以在运行时动态地、不限量地用装饰者来装饰对象。
上面虽然说明了装饰者模式的“角色”,但是没有说明在具体的实际中怎么使用,下面的类图结构能够帮我们梳理思路,后面的饮料问题就是套用此结构图。
看了上面的类图,是否在继承和组合之间思维有些混淆。
(1) 如CondimentDecorator扩展自Beverage类,这用到了继承,不是吗?
答:的确是如此,但我认为这么做的重点在于,装饰者和被装饰者必须是一样的类型。也就是有共同的超类,这是相当关键的地方。在这里,我们利用继承到达“类型匹配”,而不是利用继承获得“行为”。
(2) 既然装饰者必须能代替被装饰者,那行为又从哪里来呢?
答:当我们将装饰者与组件组合时,就是在加入新的行为,所得到的新行为,并不是继承自超类,而是由组合对象得来的。
(3) 如果我们需要继承的是Component类型,为什么不把Beverage类设计成一个接口,而是设计成一个抽象类呢?
答;通常装饰者模式是采用抽象类,但是也可以使用接口。尽管如此,但我们都努力避免修改现有的代码。而这个程序的Beverage类本身一个抽象类。
下面列出饮料系统的详细的类图与代码。
/// <summary> /// 饮料 /// </summary> public abstract class Beverage { protected string description = "Unknown Beverage"; public string Description { get { return description; } } public abstract float Cost(); } /// <summary> /// 抽象的调料装饰者 /// </summary> public abstract class CondimentDecorator : Beverage { //TODO 可根据需要添加处理信息 } public class Espresso : Beverage { public Espresso() { description = "Espresso"; } public override float Cost() { return 1.99f; } } public class HouseBlend : Beverage { public HouseBlend() { description = "House Blend Coffee"; } public override float Cost() { return 0.89f; } } public class DarkRoast : Beverage { public DarkRoast() { description = "Dark Roast Coffee"; } public override float Cost() { return 0.99f; } } public class Decaf : Beverage { public Decaf() { description = "Decaf coffee"; } public override float Cost() { return 1.05f; } } public class Mocha : CondimentDecorator { private Beverage beverage; public Mocha(Beverage beverage) { this.beverage = beverage; this.description= beverage.Description + ",Mocha"; } public override float Cost() { return 0.20f + beverage.Cost(); } } public class Soy : CondimentDecorator { private Beverage beverage; public Soy(Beverage beverage) { this.beverage = beverage; this.description = beverage.Description + ",Soy"; } public override float Cost() { return 0.15f + beverage.Cost(); } } public class Whip : CondimentDecorator { private Beverage beverage; public Whip(Beverage beverage) { this.beverage = beverage; this.description= beverage.Description + ",Whip"; } public override float Cost() { return 0.10f + beverage.Cost(); } } [Test] public void StarbuzzCoffee() { Beverage beverage = new Espresso(); Console.WriteLine("{0} ${1}", beverage.Description, beverage.Cost()); Beverage beverage2 = new DarkRoast(); beverage2 = new Mocha(beverage2); beverage2 = new Mocha(beverage2); beverage2 = new Whip(beverage2); Console.WriteLine("{0} ${1}", beverage2.Description, beverage2.Cost()); Beverage beverage3 = new HouseBlend(); beverage3 = new Soy(beverage3); beverage3 = new Mocha(beverage3); beverage3 = new Whip(beverage3); Console.WriteLine("{0} ${1}", beverage3.Description, beverage3.Cost()); }
------------------------以上内容根据《Head First Design mode》进行整理