• 使用C# (.NET Core) 实现模板方法模式 (Template Method Pattern)


    本文的概念内容来自深入浅出设计模式一书.

    项目需求

    有一家咖啡店, 供应咖啡和茶, 它们的工序如下:

    咖啡:

    茶:

    可以看到咖啡和茶的制作工序是差不多的, 都是有4步, 其中有两步它们两个是一样的, 另外两步虽然具体内容不一样, 但是都做做的同一类工作.

    现在问题也有了, 当前的设计两个类里面有很多重复的代码, 那么应该怎样设计以减少冗余呢?

    初次尝试

    把共有的方法放到父类里面, 把不同的方法放到子类里面.

    父类里面有一个抽象的prepareRecipe()方法[翻译为准备烹饪方法/制作方法], 然后在不同的子类里面有不同的实现. 也就是说每个子类都有自己制作饮料的方法.

    再仔细想想应该怎样设计

    可以发现两个饮料的制作方法遵循了同样的算法:

    1. 把水烧开
    2. 用开水冲咖啡或茶
    3. 把冲开的饮料放到杯里
    4. 添加适当的调料

    现在我们来抽像prepareRecipe()方法:

    1.先看看两个饮料的差异:

    两种饮料都有四道工序, 两个是完全一样的, 另外两个在具体的实现上是略有不同的, 但是还是同样性质的工序.

    这两道不同的工序的本质就是冲饮料和添加调料, 所以prepareRecipe()可以这样写:

    2. 把上面的方法放到超类里:

    这个父类是抽象的, prepareRecipe()将会用来制作咖啡或者茶, 而且我不想让子类去重写这个方法, 因为制作工序(算法)是一定的.

    只不过里面的第2部和第4部是需要子类自己来实现的. 所以brew()和addCondiments()是两个抽象的方法, 而另外两个方法则直接在父类里面实现了.

    3. 最后茶和咖啡就是这个样子的:

    我们做了什么?

    我们意识到两种饮料的工序大体是一致的, 尽管某些工序需要不同的实现方法. 所以我们把这些饮料的制作方法归纳到了一个基类CaffeineBeverage里面.

    CaffeineBeverage控制着整个工序, 第1, 3部由它自己完成, 第2, 4步则是由具体的饮料子类来完成.

    初识模板方法模式

    上面的需求种, prepareRecipe() 就是模板方法. 因为, 它首先是一个方法, 然后它还充当了算法模板的角色, 这个需求里, 算法就是制作饮料的整个工序.

    所以说: 模板方法定义了一个算法的步骤, 并允许子类提供其中若干个步骤的具体实现.

    捋一遍整个流程

    1. 我需要做一个茶:

    2. 然后调用茶的模板方法:

    3. 在模板方法里面执行下列工序:

    boildWater();

    brew();

    pourInCup();

    addCondiments();

    模板方法有什么好处?

    不使用模板方法时:

    • 咖啡和茶各自控制自己的算法.
    • 饮料间的代码重复.
    • 改变算法需要修改多个地方
    • 添加新饮料需要做很多工作.
    • 算法分布在了不同的类里面

    使用模板方法后:

    • CaffeineBeverage这个父类控制并保护算法
    • 父类最大化的代码的复用
    • 算法只在一个地方, 改变算法也只需改变这个地方
    • 新的饮料只需实现部分工序即可
    • 父类掌握着算法, 但是依靠子类去做具体的实现.

    模板方法定义

    模板方法在一个方法里定义了一套算法的骨架, 算法的某些步骤可以让子类来实现. 模板方法让子类重新定义算法的某些步骤而无需改变算法的结构.

    类图:

    这个抽象类:

    针对这个抽象类, 我们可以有一些扩展:

    看这个hook方法, 它是一个具体的方法, 但是啥也没做, 这种就叫做钩子方法. 子类可以重写该方法, 也可以不重写.

    模板方法里面的钩子

    所谓的钩子, 它是一个在抽象类里面声明的方法, 但是方法里面默认的实现是空的. 这也就给了子类"钩进"算法某个点的能力, 当然子类也可以不这么做, 就看子类是否需要了.

    看这个带钩子的饮料父类:

    customerWantsCondiments()就是钩子, 子类可以重写它.

    在prepareRecipe()方法里面, 通过这个钩子方法的结果来决定是否添加调料.

    下面是使用这个钩子的咖啡:

    C#代码实现

    不带钩子的父类:

    using System;
    
    namespace TemplateMethodPattern.Abstractions
    {
        public abstract class CaffeineBeverage
        {
            public void PrepareRecipe()
            {
                BoilWater();
                Brew();
                PourInCup();
                AddCondiments();
            }
    
            protected void BoilWater()
            {
                Console.WriteLine("Boiling water");
            }
    
            protected abstract void Brew();
    
            protected void PourInCup()
            {
                Console.WriteLine("Pouring into cup");
            }
    
            protected abstract void AddCondiments();
        }
    }

    咖啡和茶:

    using System;
    using TemplateMethodPattern.Abstractions;
    
    namespace TemplateMethodPattern.Beverages
    {
        public class Coffee: CaffeineBeverage
        {
            protected override void Brew()
            {
                Console.WriteLine("Dripping Coffee through filter");
            }
    
            protected override void AddCondiments()
            {
                Console.WriteLine("Adding Sugar and Milk");
            }
        }
    }
    
    using System;
    using TemplateMethodPattern.Abstractions;
    
    namespace TemplateMethodPattern.Beverages
    {
        public class Tea: CaffeineBeverage
        {
            protected override void Brew()
            {
                Console.WriteLine("Steeping the tea");
            }
    
            protected override void AddCondiments()
            {
                Console.WriteLine("Adding Lemon");
            }
        }
    }

    测试:

    var tea = new Tea();
    tea.PrepareRecipe();

    带钩子的父类:

    using System;
    
    namespace TemplateMethodPattern.Abstractions
    {
        public abstract class CaffeineBeverageWithHook
        {
            public void PrepareRecipe()
            {
                BoilWater();
                Brew();
                PourInCup();
                if (CustomerWantsCondiments())
                {
                    AddCondiments();
                }
            }
    
            protected abstract void Brew();
            protected abstract void AddCondiments();
    
            protected void BoilWater()
            {
                Console.WriteLine("Boiling water");
            }
    
            protected void PourInCup()
            {
                Console.WriteLine("Pouring into cup");
            }
    
            public virtual bool CustomerWantsCondiments()
            {
                return true;
            }
        }
    }

    咖啡:

    using System;
    using TemplateMethodPattern.Abstractions;
    
    namespace TemplateMethodPattern.Beverages
    {
        public class CoffeeWithHook: CaffeineBeverageWithHook
        {
            protected override void Brew()
            {
                Console.WriteLine("Dripping Coffee through filter");
            }
    
            protected override void AddCondiments()
            {
                Console.WriteLine("Adding Sugar and Milk");
            }
    
            public override bool CustomerWantsCondiments()
            {
                var answer = GetUserInput();
                if (answer == "yes")
                {
                    return true;
                }
                return false;
            }
    
            private string GetUserInput()
            {
                Console.WriteLine("Would you like milk and sugar with you coffee (y/n) ?");
                var keyInfo = Console.ReadKey();
                return keyInfo.KeyChar == 'y' ? "yes" : "no";
            }
        }
    }

    测试:

            static void MakeCoffeeWithHook()
            {
                var coffeeWithHook = new CoffeeWithHook();
                Console.WriteLine("Making coffee...");
                coffeeWithHook.PrepareRecipe();
            }

    钩子和抽象方法的区别?

    抽象方法是算法里面必须要实现的一个方法或步骤, 而钩子是可选实现的.

    好莱坞设计原则

    好莱坞设计原则就是: 别给我们打电话, 我们会给你打电话.

    好莱坞原则可以防止依赖关系腐烂. 依赖关系腐烂是指高级别的组件依赖于低级别的组件, 它又依赖于高级别组件, 它又依赖于横向组件, 又依赖于低级别组件....以此类推. 当腐烂发生的时候, 没人会看懂你的系统是怎么设计的.

    而使用好莱坞原则, 我们可以让低级别组件钩进一个系统, 但是高级别组件决定何时并且以哪种方式它们才会被需要. 换句话说就是, 高级别组件对低级别组件说: "别给我们打电话, 我们给你们打电话".

    好莱坞原则和模板方法模式

    模板方法里, 父类控制算法, 并在需要的时候调用子类的方法.

    而子类从来不会直接主动调用父类的方法.

    其他问题

    好莱坞原则和依赖反转原则DIP的的区别?

    DIP告诉我们不要使用具体的类, 尽量使用抽象类. 而好莱坞原则则是让低级别组件可以被钩进算法中去, 也没有建立低级别组件和高级别组件间的依赖关系.

    三种模式比较:

    模板方法模式: 子类决定如何实现算法中特定的步骤

    策略模式: 封装变化的行为并使用委托来决定哪个行为被使用.

    工厂方法模式: 子类决定实例化哪个具体的类.

    使用模板方法做排序

    看看java里面数组的排序方法:

    mergeSort就可以看做事模板方法, compareTo()就是需要具体实现的方法.

    但是这个并没有使用子类, 但是根据实际情况, 还是可以灵活使用的, 你需要做的就是实现Comparable接口即可., 这个接口里面只有一个CompareTo()方法.

    具体使用C#就是这样:

    鸭子:

    using System;
    
    namespace TemplateMethodPattern.ForArraySort
    {
        public class Duck : IComparable
        {
            private readonly string _name;
            private readonly int _weight;
    
            public Duck(string name, int weight)
            {
                _name = name;
                _weight = weight;
            }
    
            public override string ToString()
            {
                return $"{_name} weights {_weight}";
            }
    
            public int CompareTo(object obj)
            {
                if (obj is Duck otherDuck)
                {
                    if (_weight < otherDuck._weight)
                    {
                        return -1;
                    }
                    if (_weight == otherDuck._weight)
                    {
                        return 0;
                    }
                }
                return 1;
            }
        }
    }

    比较鸭子:

            static void SortDuck()
            {
                var ducks = new Duck[]
                {
                    new Duck("Duffy", 8),
                    new Duck("Dewey",  2),
                    new Duck("Howard", 7),
                    new Duck("Louie", 2),
                    new Duck("Donal", 10),
                    new Duck("Huey", 3)
                };
                Console.WriteLine("Before sorting:");
                DisplayDucks(ducks);
    
                Array.Sort(ducks);
    
                Console.WriteLine();
                Console.WriteLine("After sorting:");
                DisplayDucks(ducks);
            }
    
            private static void DisplayDucks(Duck[] ducks)
            {
                foreach (Duck t in ducks)
                {
                    Console.WriteLine(t);
                }
            }

    效果:

    其他钩子例子

    java的JFrame:

    JFrame父类里面有一个update()方法, 它控制着算法, 我们可以使用paint()方法来钩进到该算法的那部分.

    父类里面JFrame的paint()啥也没做, 就是个钩子, 我们可以在子类里面重写paint(), 上面例子的效果就是:

    另一个例子Applet小程序:

     

    这5个方法全是重写的钩子...

    我没看过winform或者wpf/sl的源码, 我估计也应该有一些钩子吧.

    总结

    好莱坞原则: "别给我们打电话, 我们给你打电话"

    模板方法模式: 模板方法在一个方法里定义了一套算法的骨架, 算法的某些步骤可以让子类来实现. 模板方法让子类重新定义算法的某些步骤而无需改变算法的结构

    该系列的源码: https://github.com/solenovex/Head-First-Design-Patterns-in-CSharp

  • 相关阅读:
    MySQL的max()函数使用时遇到的小问题
    scp命令需要指定端口时要紧跟在scp后
    linux系统之间基于密钥对免输入密码登陆
    c++的引用用法
    预测模型
    mysql出现ERROR 1366 (HY000):的解决办法
    R语言可视化--颜色
    R语言可视化--ggplot函数
    R语言可视化--qplot函数
    R语言可视化二
  • 原文地址:https://www.cnblogs.com/cgzl/p/8865861.html
Copyright © 2020-2023  润新知