Head First 设计模式之模板方法模式

Head First 设计模式之模板方法模式

前言:本章要深入封装算法,让子类可以在任何时候都可以将自己挂接进运算里,并会引入新的设计原则。

1.   现实场景应用

现实场景:冲咖啡和冲茶的方式非常相似。快速的搞定冲咖啡和茶的几个类。

1.1 冲咖啡类

public class Coffee
    {
        publicvoid PrepareRecipe()
        {
           BoilWater();//烧水
           BrewCoffeeGrinds();//冲泡咖啡
           PourInCup();//把咖啡倒入杯子
           AddSugarAndMilk(); //加糖和奶
        }
 
        publicvoid BoilWater()
        {
           Console.WriteLine(" Boiling Water !");
        }
 
        publicvoid BrewCoffeeGrinds()
        {
           Console.WriteLine(" Dripping Coffee through filter !");
        }
 
        publicvoid PourInCup()
        {
           Console.WriteLine(" Pouring into cup !");
        }
 
        publicvoid AddSugarAndMilk()
        {
           Console.WriteLine(" Adding Sugar and Milk !");
        }
    }

1.2 冲茶类

public class Tea
    {
        publicvoid Tea()
        {
           BoilWater();//烧水
           SteepTeaBag();//冲泡茶
           PourInCup();//把茶倒入杯中
           AddLemon(); //加柠檬
        }
 
        publicvoid BoilWater()
        {
           Console.WriteLine(" Boiling Water !");
        }
 
        publicvoid SteepTeaBag()
        {
            Console.WriteLine(" Steeping thetea !");
        }
 
        publicvoid PourInCup()
        {
           Console.WriteLine(" Pouring into cup !");
        }
 
        publicvoid AddLemon()
        {
           Console.WriteLine(" Adding Lemon !");
        }
    } publicclass Tea
    {
        publicvoid Tea()
        {
           BoilWater();
           SteepTeaBag();
           PourInCup();
           AddLemon();
        }
 
        publicvoid BoilWater()
        {
           Console.WriteLine(" Boiling Water !");
        }
 
        publicvoid SteepTeaBag()
        {
           Console.WriteLine(" Steeping the tea !");
        }
 
        publicvoid PourInCup()
        {
            Console.WriteLine(" Pouringinto cup !");
        }
 
        publicvoid AddLemon()
        {
           Console.WriteLine(" Adding Lemon !");
        }
    }

1.3 抽取咖啡和茶

从1.1和1.2中我们可以看出两个类中都有共同的操作即“BoilWater”和“PourInCup”我们可以抽取一下,抽取后的类图如下:


除了以上的设计之外,我们还能发现咖啡和茶还有一些共同点,如下两份冲泡都采用了相同的算法:

l  1.把水煮沸

l  2.用热水泡水或者咖啡

l  3.把饮料倒进杯子里

l  4.在饮料内加入适当的调料

其中1,3两步已经被抽出放到基类中了,2,4虽然没被抽出来,但是他们是一样的,只是应用在不同的饮料上。

从此,我们也可以将PrepareRecipe()抽象出来。

1.3.1 重新组织PrepareRecipe()方法

    从上边的分析中可知咖啡和茶的区别主要在于冲泡和添加调料,因此我们将这两个步骤分部分部抽象出来为Brew()和AddCondiments(),这样新的PrepareRecipe()方法就成了这样:

public voidPrepareRecipe()
        {
            BoilWater();
            Brew();//泡
            PourInCup();
            AddCondiments(); //添加调料
        }
1.3.2 完善CaffeineBeverage超类

有了新的PrepareRecipe()方法,我们也要完善一下CaffeineBeverage类,如下:

public abstractclass CaffeineBeverage//该类为抽象类
    {
        public sealed void PrepareRecipe()//该方法为密封方法,防止子类覆盖或者重写
        {
            BoilWater();
            Brew();
            PourInCup();
            AddCondiments();
        }
        public abstract void Brew();//因为茶和咖啡的处理方法不同所以这两个方法声明为抽象,由各自的子类去处理
        public abstract void AddCondiments();
        public void BoilWater()//这两个方法咖啡和茶都一样,所以抽象到超类中
        {
            Console.WriteLine(" BoilingWater!");
        }
        public void PourInCup()
        {
            Console.WriteLine("Pouringinto Cup");
        }
    }
1.3.3 处理咖啡和茶类

现在我们需要再处理一下咖啡和茶,这两个类都需要依赖超类,需要自己处理一下冲泡和添加调料的部分。

咖啡:

public classCoffee:CaffeineBeverage
    {     
        public void Brew()
        {
            Console.WriteLine(" DrippingCoffee through filter !");
        }
 
        public void AddCondiments()
        {
            Console.WriteLine(" AddingSugar and Milk !");
        }
    }

茶:

public classTea:CaffeineBeverage
    {      
        public void Brew()
        {
            Console.WriteLine(" Steepingthe tea !");
        }
 
        public void AddCondiments()
        {
            Console.WriteLine(" AddingLemon !");
        }
    }

2.   定义模板方法模式

从1.3中对咖啡和茶类的提取的CaffeineBeverage类,就包含了“模板方法”,如下图所示:


模板方法定义了一个算法的步骤,并允许子类为一个或多个步骤提供实现。

由此引出了模板方法模式:

模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。

这个模式是用来创建一个算法的模板,即这个方法将算法定义成一组步骤,其中的任何步骤都可以是抽象的,由子类负责实现。这可以确保算法结构保持不变,同时由子类提供部分实现。类图如下:


定义抽象类:

    

Public abstract class AbstractClass{
        Public seal void TemplateMethod(){//模板方法,声明为seal,避免子类修改
PrimitiveOperation1();//定义了一连串步骤,每一个步骤由一个方法代表
PrimitiveOperation2();
ConcreteOperation();
}
Public abstract void PrimitiveOperation1();//需要子类实现
Public abstract void PrimitiveOperation2();
Public void ConcreteOperation()
{
    //实现
}
}

3.   对模板方法进行挂钩

    我们在2中定义了抽象类,其中该抽象类中还有一个具体的操作。现在我们在抽象类中再加入一个新的方法调用,改变TemplateMethod()

Public abstractclass AbstractClass{
        Public void TemplateMethod(){//模板方法
PrimitiveOperation1();//定义了一连串步骤,每一个步骤由一个方法代表
PrimitiveOperation2();
ConcreteOperation();
}
Public abstract void PrimitiveOperation1();//需要子类实现
Public abstract void PrimitiveOperation2();
Public void ConcreteOperation()
{
    //实现
}
}

现在我们写一个带钩子的CoffeeBeverageWithHook类,如下:

public abstractclass CaffeineBeverageWithHook
    {
        public void PrepareRecipe()
        {
            BoilWater();
            Brew();
            PourInCup();
            if(CustomerWantsCondiments())
               AddCondiments();
        }
        public virtual boolCustomerWantsCondiments()//可以被子类重写
        {
            return true;
        }
        public abstract void Brew();
        public abstract void AddCondiments();
        public void BoilWater()
        {
            Console.WriteLine(" BoilingWater!");
        }
 
        public void PourInCup()
        {
            Console.WriteLine("Pouringinto Cup");
        }
    }

新的CoffeeWithHook类:

public classCoffeeWithHook : CaffeineBeverageWithHook
    {
        public override void Brew()
        {
            Console.WriteLine(" DrippingCoffee through filter !");
        }
        public override void AddCondiments()
        {
            Console.WriteLine(" AddingSugar and Milk !");
        }
        public override boolCustomerWantsCondiments()//重写该方法,根据用户的输入,来选择是否加入调料,实现钩子的作用
        {
            string answer = GetUserInput();
            if(answer.ToLower().StartsWith("y"))
            {
                return true;
            }
            else
                return false;
        }
        public string GetUserInput()//获取用户的输入
        {
            string answer = null;
            System.Console.WriteLine("Would you like milk and sugar with your coffee (y/n)? ");
            answer = Console.ReadLine();
            if (string.IsNullOrEmpty(answer))
                return "no";
            return answer;
        }
    }

相关问题:

l  创建模板方法时什么时候用抽象方法?什么时候用钩子?

答:当子类必须提供算法中的某个方法或步骤的实现时,就是用抽象方法。如果算法的这个部分是可选的,则使用钩子。

l  使用钩子的目的?

1.      让子类实现算法中可选的部分

2.      让子类可以对模板方法中的某些即将发生的步骤做出反应。

4.   好莱坞原则

好莱坞原则:别调用我们,我们会调用你。

好莱坞原则可以给我们一种防止腐败的方法。当高层组件依赖底层组件,而底层组件又依赖高层组件,而高层组件又依赖边侧组件,而边侧组件又依赖底层组件,就发生了依赖腐败。但是好莱坞原则允许底层组件将自己挂钩到系统上,高层组件会决定什么时候使用这些底层组件。

好莱坞原则和模板方法

好莱坞原则和模板方法之间的连接比较明显即当我们设计模板方法模式时,我们告诉子类,不要调用我们,我们会调用你。看如下的咖啡因饮料的设计类图:


好莱坞原则和依赖倒置原则的关系?

依赖倒置原则是尽量避免使用具体类,多使用抽象。

好莱坞原则是用在框架或组件上的一种技巧,好让底层组件能够被挂钩进计算中,而又不会让高层组件依赖底层组件。两者的目标都是解耦,但是依赖倒置原则更加注重如何在设计中避免依赖,好莱坞原则则是教我们一个技巧,创建一个有弹性的设计,允许底层结构能够互相操作,又防止其他类太过依赖它们。

5.   总结

模板方法模式——在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法的结构情况下,重新定义算法中的某些步骤。

l  模板方法定义了算法的步骤,把这些步骤实现延迟到子类。

l  模板方法为我们提供了一种代码复用的重要技巧。

l  模板方法的抽象类可以定义具体的方法、抽象方法和钩子方法。

l  抽象方法由子类实现。

l  钩子是一种方法,它在抽象类中不做事,或者只做默认的事,子类可以选择要不要覆盖它。

l  好莱坞原则告诉我们将决策权放在高层模板中,以便决定如何及何时调用底层模块。

l  策略模式和模板方法模式都封装算法,一个是用组合,一个是用继承。

l  工厂方法是模板方法的一种特殊版本

发布了137 篇原创文章 · 获赞 62 · 访问量 120万+

猜你喜欢

转载自blog.csdn.net/xuemoyao/article/details/53998928