从汉堡加料说起——浅谈C#中的Decorator模式

相信大家都在都在汉堡店吃过汉堡,有些汉堡店很有特色,推出了汉堡订制服务,即,可以在汉堡中加料,加肉饼,加生菜之类(有点类似我们本地的肥肠粉里面加冒结子)。更是让不少吃货大快朵颐,大呼过瘾,加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 cost
            Beefs.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主要用于如下场景:

  • 想要方便的添加一些行为,而这些行为又不属于类的核心行为。
  • 添加行为的时候,不希望出现类数量爆炸的时候。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章