[设计模式-6] 样板方法模式

话三国~样板方法模式


诸葛亮智算华容

话说,周瑜赤壁一把大火,烧掉曹操统一天下的梦想之后,诸葛亮也未闲着,正在积极的准备埋伏,要在陆战彻底歼灭曹操.画面转到刘备军营帐,孔明与刘备等坐定,便对赵云说:子龙,你带三千军马,在乌林小路埋伏,只可追击,不可追死,避免敌军拼死一战,减少损伤.赵云领命便退下了;孔明接着传唤张飞:翼德可领三千兵在葫芦口埋伏,曹操到的时候已是傍晚、必会埋锅造饭,一看火起,便可杀出,就算不能抓到曹操,功劳也不小了.张飞听完大喜,便下去准备;最后孔明又分别吩咐糜芳,刘封交代完任务,便准备散会了.这时候关羽终于按耐不住,上前说道:关某陪大哥征战多年,未曾落后,今日如此大战,岂可无我!孔明听罢曰:我有一个重大任务,想要交给将军,岂耐因将军曾受曹操大恩,故此犹豫不决.关羽怒道,吾斩颜良、诛文丑,已报此恩,今日相见,公是公,私是私,岂可混淆!孔明道:口说无凭.关羽答:愿立军令状!孔明大喜,便道:曹操今夜会经华容道,将军可埋伏于该地,曹操到时人困马乏,将军坐下赤兔马,曹操必定无法逃脱,就看今夜关将军立此大功.关羽领命而去.

以上故事,我们先来看基本版的程序应该如何达成,首先赵云得到命令,他必须要先做埋伏准备动作,1.先集合军队,2.准备武器粮草,3.全军出发埋伏,最后等曹操到来才执行孔明的攻击命令;接下来,张飞得到命令,也必须做埋伏准备动作1.先集合军队,2.准备武器粮草,3.全军出发埋伏,最后等曹操到来才执行孔明的攻击命令;接下来糜芳,刘封,关羽依此类推,就不赘述.因此程序大概长得像以下的样子.

    /// 
    /// 赵云
    /// 
    class Commander1
    {
        private bool armed = false;
        public void Convene()
        {
            Console.WriteLine("部队听令 全军集合");
        }

        public void Prepare()
        {
            armed = true;
            Console.WriteLine("准备武器、粮草");
        }

        public void Depart()
        {
            Console.WriteLine("全军出发");
        }

        public void Operate(string command)
        {
            if (armed)
            {
                Console.WriteLine("赵云挥舞梨花枪:" + command);
            }
            else
            {
                Console.WriteLine("赵云粮草不足 军队溃败");
            }
        }
    }

首先,先来看Commander1(赵云)的程序,它具备了Convene(集合),Prepare(准备粮草),Depart(出发),Operate(执行孔明命令)的方法.

    /// 
    /// 张飞
    /// 
    class Commander2
    {
        private bool armed = false;
        public void Convene()
        {
            Console.WriteLine("部队听令 全军集合");
        }

        public void Prepare()
        {
            armed = true;
            Console.WriteLine("准备武器、粮草");
        }

        public void Depart()
        {
            Console.WriteLine("全军出发");
        }

        public void Operate(string command)
        {
            if(armed)
            {
               Console.WriteLine("张飞摆动丈八蛇矛:" + command);
            }
            else
            {
                Console.WriteLine("张飞粮草不足 军队溃败");
            }
        }
    }

接下来看Commander2(张飞)的程序,也是具备了Convene(集合),Prepare(准备粮草),Depart(出发),Operate(执行孔明命令)的方法.

    /// 
    /// 关羽
    /// 
    class Commander3
    {
        private bool armed = false;
        public void Convene()
        {
            Console.WriteLine("部队听令 全军集合");
        }

        public void Prepare()
        {
            armed = true;
            Console.WriteLine("准备武器、粮草");
        }

        public void Depart()
        {
            Console.WriteLine("全军出发");
        }

        public void Operate(string command)
        {
            if (armed)
            {
                Console.WriteLine("关羽舞动青龙偃月刀:" + command);
            }
            else
            {
                Console.WriteLine("关羽粮草不足 军队溃败");
            }
        }
    }

最后看Commander3(关羽)的程序,会发现也是具备了Convene(集合),Prepare(准备粮草),Depart(出发),Operate(执行孔明命令)的方法.看到这里应该有发现,除了Operate(执行孔明命令)的方法之外,剩下的三个方法实践都是一模一样的.

  class Program
    {
        static void Main(string[] args)
        {
            var 赵云 =new  Commander1();
            赵云.Convene();
            赵云.Prepare();
            赵云.Depart();
            赵云.Operate("突击曹操,只击不追");
            Console.WriteLine();

            var 张飞 = new Commander2();
            张飞.Convene();
            张飞.Depart();
            张飞.Operate("突击曹操,只击不追");
            Console.WriteLine();

            var 关羽 = new Commander3();
            关羽.Convene();
            关羽.Prepare();
            关羽.Depart();
            关羽.Operate("拦截曹操,务必生擒或当场击杀");

            Console.Read();
        }
    }

接下来看看真正执行的状况,Commander1(赵云)和Commander3(关羽)要埋伏之前执行了Convene(集合),Prepare(准备粮草),Depart(出发),之后才执行Operate(执行孔明命令)的方法;但Commander2(张飞)天生是个粗线条,他只有执行了Convene(集合),Depart(出发),忘记执行Prepare(准备粮草),就马上执行Operate(执行孔明命令)的方法.

程序输出:
部队听令 全军集合
准备武器粮草
全军出发
赵云挥舞梨花枪:突击曹操,只击不追


部队听令 全军集合
全军出发
张飞粮草不足,全军溃败


部队听令 全军集合
准备武器粮草
全军出发
关羽舞动青龙偃月刀:拦截曹操,务必生擒或当场击杀

这样的输出结果,很明显不是我们预期的,因为张飞居然溃败了.根据程序设计原则DRP,重复的程序是令人反感的;而且在实际使用该类的时候,甚至有可能忘了执行某个方法,或者调用顺序错误,而造成输出结果有问题.这些都是我们事先就可以避免的事情.

由上面的程序实践方式可以看到,Convene(集合),Prepare(准备粮草),Depart(出发)三个方法功能都是一样的,关羽张飞和赵云的差别只有在Operate(执行孔明命令)的方法,而在设计模式中就有一种模式是透过将重复的程序上移到父类,而子类就能去除这些程序而专注于自己类差异的部分.再深入一点来说,这些重复的程序就是"不会改变的行为",以本故事的埋伏工作来说,Convene(集合),Prepare(准备粮草),Depart(出发)三个方法是不会改变的,不管是哪位将军来执行,都必须做这些事情,而且这三个方法是有顺序性的,总不能先Depart(出发)才Prepare(准备粮草)吧?俗话说:三军未动,粮草先行就是这个道理;而真正在作战的行为则是可变的,比如说,赵云使用梨花枪,关羽用青龙偃月刀,张飞使用丈八蛇矛等等.而当一个类中,同时具备了可变的行为和不变的行为,只要把不变的行为抽离至父类,便可让子类摆脱这些重复程序的纠缠.以下要介绍的便是样板方法模式

定义

样板方法模式=>定义算法结构,将实践延迟到子类进行.使子类可以不改变算法结构,透过实践特定步骤达到目的。

   /// 
    /// 部队指挥官
    /// 
     abstract class ICommander
    {
        protected string _command = "";
        public ICommander(string command) {
            _command = command;
        }

        public void ExcuteCommand()
        {
            Convene();
            Prepare();
            Depart();
            Operate(_command);
        }

        private void Convene()
        {
            Console.WriteLine("部队听令 全军集合");
        }

        private void Prepare()
        {
            Console.WriteLine("准备武器、粮草");
        }

        private void Depart()
        {
            Console.WriteLine("全军出发 前往指定地点");
        }

        protected abstract void Operate(string command);

    }

要达成样板方法模式,首先须定义一个ICommander(部队指挥官)的父类,将原本各将军不变的方法,分别是Convene(集合),Prepare(准备粮草),Depart(出发)方法集合到这个类来,并明确实践;除此之外,还需要将可变的行为也定义出来,本例便是Operate(执行孔明命令)的方法,并将它抽象化,因为这个方法,会被所有子类override,所以不需要实做;最后,定义一个样板方法,这个样板方法会照定义的顺序,依序来调用指定的方法,也就是样板方法模式的核心,定义一个算法的结构,透过定义抽象的方法来强制继承的子类实践,以达到扩充的目的.

   /// 
    /// 赵云
    /// 
    class CommanderA : ICommander
    {
        public CommanderA(string command) : base(command)
        { }

        protected override void Operate(string command)
        {
            Console.WriteLine("赵云挥舞梨花枪" + command);
        }
    }

  /// 
    /// 张飞
    /// 
    class CommanderB : ICommander
    {
        public CommanderB(string command) : base(command)
        { }
        protected override void Operate(string command)
        {
            Console.WriteLine("张飞摆动丈八蛇矛:" + command);
        }
    }

  /// 
    /// 关羽
    /// 
    class CommanderC : ICommander
    {
        public CommanderC(string command) : base(command)
        { }
        protected override void Operate(string command)
        {
            Console.WriteLine("关羽舞动青龙偃月刀:" + command);
        }
    }

接下来各将军的类只需要继承ICommander,并且override Operate(执行孔明命令)的方法,其余的功能,便可借由继承获得,程序相对的变了简洁许多.

 class Program
    {
        static void Main(string[] args)
        {
            var 赵云 =new  CommanderA("突击曹操,只击不追");
            var 张飞 = new CommanderB("突击曹操,只击不追");
            var 关羽 = new CommanderC("拦截曹操,务必生擒或当场击杀");

            赵云.ExcuteCommand();
            Console.WriteLine();
            张飞.ExcuteCommand();
            Console.WriteLine();
            关羽.ExcuteCommand();
            Console.Read();
        }
    }

最后,再来看主程序,透过样板方法模式,算法的顺序不但被固定下来,而且也不怕漏掉哪个必要的进程,而造成输出结果有问题,同时整个程序也简洁了不少.

程序输出:
部队听令 全军集合
准备武器粮草
全军出发
赵云挥舞梨花枪:突击曹操,只击不追


部队听令 全军集合
准备武器粮草
全军出发
张飞摆动丈八蛇矛:突击曹操,只击不追


部队听令 全军集合
准备武器粮草
全军出发
关羽舞动青龙偃月刀:拦截曹操,务必生擒或当场击杀

以上便是样板方法模式的心得分享...