相信大家都在都在汉堡店吃过汉堡,有些汉堡店很有特色,推出了汉堡订制服务,即,可以在汉堡中加料,加肉饼,加生菜之类(有点类似我们本地的肥肠粉里面加冒结子)。更是让不少吃货大快朵颐,大呼过瘾,加6,7层肉饼的感觉简直不要太好。 那么大饱口福之后,让我们来思考一个问题,汉堡是要钱的,加的料,比如肉饼,生菜,也都是收费的,如果让我们来设计出一套类,计算客户买汉堡的消费,我们应该怎么做比较合适?这里为了简单起见,我们就假定加的肉饼是beef,生菜是tomatto。
第一种设计
建立3个类,一个表示汉堡,另外两个表示肉饼和生菜。汉堡类中有办法添加肉饼和生菜。结算费用的时候,直接调用汉堡类的方法。
在代码中则以这样的形式呈现。
class Beef{public double GetCost(){return 10;}}class Tomatto{public double GetCost(){return 5;}}class Hamburg{public List<Beef> Beefs { get; private set; } = new List<Beef>();public List<Tomatto> Tomattos { get; private set; } = new List<Tomatto>();public void AddBeef(Beef beef){Beefs.Add(beef);}public void AddTomatto (Tomatto tomatto){Tomattos.Add(tomatto);}public double GetCost(){var result = 20d; //hamburg\'s costBeefs.ForEach(b => result += b.GetCost());Tomattos.ForEach(t => result += t.GetCost());return result;}}
这就是最简单的一种实现方法,然而它有以下几个弊端。
- 类数量过多,应该通过抽象减少类数量,如果以后还有鸡肉饼,小龙虾饼,岂不是又要加新的类?而其实这些类彼此都是相似的。
- 不满足开闭原则。如果以后有了其他可以添加的料,我们会不可避免的修改Hamburg类。
- Hamburg类与具体的料耦合。
所以,这种最简单的做法,如果对于一个小项目或者很简单的案例,我们还可以容忍,如果对于一个大项目,或者预计到未来会出现需求改变的时候,我们就需要改进我们的设计方案。
第二种设计,抽象出料接口
第一种设计中很大的一个缺陷来自于,不管是牛肉饼也罢,生菜也罢,它们都汉堡的一种添加物,对于计费系统,关心的也仅仅是添加物的名字和价格而已,所以,我们应该抽象出接口来进行汉堡类和具体添加物类的解耦。
代码实现如下:
interface Addin{double GetCost();}class Beef:Addin{public double GetCost(){return 10;}}class Tomatto:Addin{public double GetCost(){return 5;}}class Hamburg{public List<Addin> Addins { get; private set; } = new List<Addin>();public void AddAddin(Addin addin){Addins.Add(addin);}public double GetCost(){var result = 20d;Addins.ForEach(a => result += a.GetCost());return result;}}
在第二版设计中,我们提炼出了接口addin,使得hamburg类依赖于addin而不直接依赖于具体某个添加物类。同时也保证了开闭原则的实现,就算新的添加物上线,我们也不用修改hamburg类了,我们似乎达到了设计的理想境界。但是真的这样就无懈可击了吗?
第三种设计,Decorator模式
虽然我们第二种设计解决了依赖于具体类的问题并实现了开闭原则,但是还是会有人觉得不爽,因为大家觉得,虽然第二种设计没有什么大问题了,但是在语义上面,我们希望能保证hamburg类的纯洁性。什么意思呢,就是说,hamburg自己代表自己的价格就行了,添加物毕竟是外来物,没有必要深入到hamburg类的内部。所以,我们就再次更新我们的设计,这次我们祭出Decorator模式。
以下是Decorotor模式中需要注意的点:
- 装饰类基类和被装饰对象都继承自同一个接口,装饰基类内部还聚合了一个此接口对象。
- 装饰具体类在计算中,先计算自己那部分,再调用基类方法,基类方法一般是计算内部聚合的那个对象, 这样确保了装饰模式可以一层嵌套一层。
我们看看具体代码。
abstract class Food{public abstract double GetCost();}class Hamburger : Food{public override double GetCost(){return 20;}}class FoodDecorate : Food{private Food _food = null;public FoodDecorate(Food food){_food = food;}public override double GetCost(){return _food.GetCost();}}class TomatoDecorator : FoodDecorate{public TomatoDecorator(Food food) : base(food) { }public override double GetCost(){return 5 + base.GetCost();}}class BeefDecorator : FoodDecorate{public BeefDecorator(Food food) : base(food) { }public override double GetCost(){return 10 + base.GetCost();}}
因为不管是Hamburg还是Decorator,大家都实现了Food接口,同时Decorator聚合的也是Food对象,所以在客户端我们可以很方便的写
BeefDecorator beefAddHamburg = new BeefDecorator(new BeefDecorator(new Hamburger()));Console.WriteLine(beefAddHamburg.GetCost());
以此来表示加了两层牛肉的hamburg。怎么样,这是不是比第二种设计又方便了一点呢? 总结一下,Decorator主要用于如下场景:
- 想要方便的添加一些行为,而这些行为又不属于类的核心行为。
- 添加行为的时候,不希望出现类数量爆炸的时候。