3. 装饰者模式:装饰对象

来源:互联网 发布:怎么申请开淘宝店 编辑:程序博客网 时间:2024/04/27 18:41

3.1. 引言: Es什么so?Cappu什么no?

你会不会忽然地出现,在街角的咖啡店……

 

喂喂,喝咖啡本来是为了抖擞精神,不要搞得这么伤感。

 

Espresso,Cappuccino,Latte……是不是傻傻分不清?如果你只喝美式咖啡(和现在的我一样),或者只喝速溶咖啡(和过去的我一样),或者有选择恐惧症,那么从喝咖啡的角度来说,接下来的内容可能跟你关系不大。但咱们不是来学习设计模式的嘛。

3.2. 新的需求:一名咖啡师的自我修养

作为喝咖啡的人,可以完全不去探究喝到嘴里的究竟是什么,只要味道好就可以了。但如果你要成为一名咖啡师,那可就要好好修炼了。我们先来看看常见的几种咖啡是怎么做出来的。

 

 

好像也没那么复杂,不同的咖啡就是成分不同而已。那么我们开始动手吧。

3.3. 解决方案

做一杯Coffee就是实例化一个Coffee对象,那么就需要一个Coffee类,它包含至少两个成员:Cost用来计算费用,Description用来描述成分。

3.3.1. 非常蠢的做法:Kaboom!

3.2.中列举了9种Coffee,那就建立9个ConcreteCoffee类吧。每新增加一种Coffee,就得新增加一个ConcreteCoffee类。甚至原有的Coffee需要一些成分比例的调整时,也得新增加一个ConcreteCoffee类。这样下去,类会多得爆炸的,Kaboom!

 

这违背了“针对接口编程,不针对实现编程”的OO原则。

 

再者,不同种类Coffee的组成也不是完全不同的,比如Espresso就出现在每一种Coffee中,它的Description和Cost属性完全应该被其他种类的Coffee复用。

 

这违背了“封装变化”的OO原则。

 

这样不仅很没有原则,而且真的是蠢哭了。

3.3.2. 一般蠢的做法:扩充超类的成员

为了增加复用性,一个方法就是扩充超类成员,把3.2.中每种咖啡成分的描述及费用都包含进来,子类(ConcreteCoffee类)继承超类之后根据实际情况设置属性即可。以Latte类为例(这里省略了对成分的描述)。

 

Coffee基类

public class Coffee    {        // 每种成分的费用        private const float espressoCost = 5;        private const float milkFoamCost = 1;        private const float steamedMilkCost = 3;         // 是否需要某种成分        public bool HasEspresso { get; set; }        public bool HasMilkFoam { get; set; }        public bool HasSteamedMilk { get; set; }         // Constructor,初始状态时不包含任何成分        public Coffee()        {            HasEspresso = false;            HasMilkFoam = false;            HasSteamedMilk = false;        }         // 根据成分计算总费用        public float Cost()        {            float totalCost = 0;             if (HasEspresso)            {                totalCost += espressoCost;            }             if (HasMilkFoam)            {                totalCost += milkFoamCost;            }             if (HasSteamedMilk)            {                totalCost += steamedMilkCost;            }             return totalCost;        }    }

ConcreteCoffee类

public class Latte : Coffee{    public Latte()    {        this.HasEspresso = true;        this.HasMilkFoam = true;        this.HasSteamedMilk = true;    }}


来做一杯Latte吧:

Coffee coffee = new Latte();Console.WriteLine(coffee.Cost());

3.4. 新的需求:更多的成分,更多的组合

3.3.2.中解决了3.3.1.中的问题,但这并不意味着就没有问题了,一名有理想的咖啡师总是要考虑得更多一点。

 

1. 大部分Coffee并不需要那么多成分,所以ConcreteCoffee类并不需要从Coffee基类中继承所有的成员。你还记得1.3.1.中那个鸭子满天飞的世界吗?这里也违背了“多用组合,少用继承”的OO原则。

2. Coffee成分的种类、费用、比例都有可能发生变化(新的Coffee调配方法,原料价格变动,不同顾客的口味偏好),按照现有的结构,Coffee基类需要一直被修改。变化的部分存在于超类中,这违背了“封装变化”的OO原则。

3.5. 解决方案:多用组合,少用继承

如何改进呢?别忘了,还有一个“多用组合,少用继承”的OO原则。

3.5.1. 成分

不同ConcreteCoffee对象的各种成分有着一些共同的特征,比如费用,比如对成分的描述。我们可以建立一个AbstractComponent类,每种具体成分都作为一个ConcreteComponent类去继承它。

 

AbstractComponent类

public abstract class CoffeeComponent{    public float Cost { get; set; }     public string Description { get; set; }     public float Quantity { get; set; }     public void Add(CoffeeComponent coffeeComponent)    {        this.Cost += coffeeComponent.Cost;        this.Description = string.Format("{0} + {1}", this.Description, coffeeComponent.Description);    }     public void GetDescription()    {        Console.WriteLine("Total cost: " + this.Cost);        Console.WriteLine("This coffee consists of: " + this.Description);        Console.WriteLine();    }}

 

ConcreteComponent类

public class Espresso : CoffeeComponent{    private const float cost = 5;     public Espresso(float quantity)    {        this.Quantity = quantity;        this.Cost = cost * this.Quantity;        this.Description = this.Quantity + " Espresso";    }}

3.5.2. 由成分组成的咖啡

对于标准化的Coffee,可以建立由ConcreteComponent对象组成的ConcreteCoffee类。每个ConcreteCoffee对象都是由若干ConcreteComponent对象组成的,每个ConcreteComponent对象就像是一个装饰者(Decorator)一样去装饰其他的ConcreteComponent对象,它本身也可以被其他的ConcreteComponent对象所装饰。

 

建立一个继承自CoffeeComponent类的AbstractCoffee类,该类具有一个CoffeeComponent对象成员,该成员由若干CoffeeComponent对象组成。所有的ConcreteCoffee类都继承自AbstractCoffee类。

 

AbstractCoffee类:

public abstract class AbstractCoffee : CoffeeComponent{    public CoffeeComponent Coffee { get; set; }}


ConcreteCoffee类:

public class Cappuccino : AbstractCoffee{    public Cappuccino()    {        this.Description = "Cappuccino";        this.Coffee = new Espresso((float)1);        this.Coffee.Add(new SteamedMilk((float)0.5));        this.Coffee.Add(new MilkFoam((float)1));    }}

 

对于定制化的Coffee,既可以单独建立一个 ConcreteCoffee类,也可以在运行时决定添加哪些ConcreteComponent对象。

 

与3.3.中的方法对比:

以前的做法

现在的做法

先获取所有的成分,再看需要什么

需要哪种成分就单独获取这种成分

利用继承,子类的行为是在编译时静态决定的

利用组合,子类的行为可以在运行时动态地扩展

3.6. 最终方案

3.6.1. UML

 

其中,Espresso,MilkFoam,SteamedMilk为ConcreteComponent类,继承自CoffeeComponent类。Cappuccino,Latte为ConcreteCoffee类,继承自AbstractCoffee类。

3.6.2. 测试代码

class Program{    static void Main(string[] args)    {        // 标准化Coffee        Console.WriteLine("标准化Coffee:");        // Cappuccino        Coffee standardCoffee = new Cappuccino();        Console.WriteLine(string.Format("Make a cup of {0}.", standardCoffee.Description));        standardCoffee = ((Cappuccino)standardCoffee).Coffee;        standardCoffee.GetDescription();         // 运行时添加成分        Console.WriteLine("运行时添加成分:");        Console.WriteLine("Add more steamed milk.");        standardCoffee.Add(new SteamedMilk((float)5));        standardCoffee.GetDescription();         // 定制化Coffee        Console.WriteLine("定制化Coffee:");        Console.WriteLine("Make a cup of cutomized coffee.");        Coffee customizedCoffee = new Espresso((float)1);        customizedCoffee.Add(new MilkFoam((float)0.5));        customizedCoffee.Add(new SteamedMilk((float)0.2));        customizedCoffee.GetDescription();    }}

3.6.3. 运行结果

标准化Coffee:

Make a cup of Cappuccino.

Total cost: 8.5

This coffee consists of: 1 Espresso + 0.5 Steamed Milk + 1 Milk Foam

 

运行时添加成分:

Add more steamed milk.

Total cost: 23.5

This coffee consists of: 1 Espresso + 0.5 Steamed Milk + 1 Milk Foam + 5 Steamed Milk

 

定制化Coffee:

Make a cup of cutomized coffee.

Total cost: 6.6

This coffee consists of: 1 Espresso + 0.5 Milk Foam + 0.2 Steamed Milk

3.7. 总结

【装饰者模式】动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案。

 

包含的OO原则:

类应该对扩展开放,对修改关闭。对修改关闭指的是不修改现有的代码,尤其是不修改超类的代码。


(注:本文为笔者学习《Head First Design Patterns》的笔记,若涉及版权等问题,请告知。)

0 0
原创粉丝点击