专栏首页EdisonTalk设计模式的征途—17.模板方法(Template Method)模式

设计模式的征途—17.模板方法(Template Method)模式

在现实生活中,很多事情都需要经过几个步骤才能完成,例如请客吃饭,无论吃什么,一般都包含:点单、吃东西、买单等几个步骤,通常情况下这几个步骤的次序是:点单=>吃东西=>买单。在这3个步骤中,点单和买单大同小异,最大的区别在于第2步-吃什么?吃面条和吃满汉全席可大不相同。

在软件开发中,有时候也会遇到类似的情况,某个方法的实现需要多个步骤(类似于“请客”),其中有些步骤是固定的,而有些步骤则存在可变性。为了提高代码复用性和系统灵活性,可以使用一种称之为模板方法模式的设计模式来对这类情况进行设计。

模板方法模式(Template Method)

学习难度:★★☆☆☆

使用频率:★★★☆☆

一、银行利息计算模块的设计

1.1 需求背景

Background:M公司欲为某银行的业务支撑系统开发一个利息计算模块,利息计算流程如下: (1)系统根据账号和密码验证用户信息,如果用户信息错误,系统显示错误提示。 (2)如果用户信息正确,则根据用户类型的不同使用不同的利息计算公式计算利息(例如活期账户和定期账户具有不同的利息计算公式) (3)系统显示利息。

1.2 初始设计

  M公司开发人员根据需求设计了一个Account类,在其中定义了3个方法实现上述3个步骤,其核心代码如下所示:

    public class Account
    {
        // 验证用户信息
        public bool Validate(string account, string password)
        {
            // 具体代码省略
        }

        // 计算利息
        public void CalculateInterest(string type)
        {
            if (type.Equals("Current", StringComparison.OrdinalIgnoreCase))
            {
                // 按活期利率计算利息,代码省略
            }
            else if (type.Equals("Saving", StringComparison.OrdinalIgnoreCase))
            {
                // 按定期利率计算利息,代码省略
            }
        }

        // 显示结果
        public void Display()
        {
            // 具体代码省略 
        }
    }

  客户端可以通过调用Account类实现完整的利息计算流程,核心代码片段如下:

    public class Client
    {
        public static void Main()
        {
            Account account = new Account();
            if (account.Validate("张无忌", "123456")) // 验证用户
            {
                account.CalculateInterest("Current");  // 计算利息
                account.Display();  // 显示利息
            }
        }
    }

  But,不难发现,该设计实现有以下两个问题:

  (1)系统可扩展性较差 => 如果需要增加一种新类型的用户,例如“小额贷款用户”,在系统中需要对应增加一种新的利息计算方法,不得不修改Account类的源代码,在CalculateInterest方法中增加新的判断逻辑,违背了开闭原则。

  (2)客户端需要逐个调用Account类中定义的方法,而且需要了解这些方法的执行与否,否则容易出错 => 例如Account类中的3个方法的次序为:Validate() => CalculateInterest() => Display(),如果不按次序调用,可能会导致结果出错。

  针对问题(1),可以使用Account类的子类来解决,在子类中覆盖父类的CalculateInterest()方法,实现扩展。但是针对问题(2),即使使用的是子类,也无法解决该问题。是否存在一种技术能够一次解决问题(1)和问题(2)?

二、模板方法模式概述

2.1 模板方法模式简介

  模板方法可以算是最简单的行为型设计模式,在其结构中只存在父类与子类之间的继承关系,其定义如下:

模板方法(Template Method)模式:定义一个操作中算法的框架,而将一些步骤延迟到子类中,模板方法使得子类可以不改变一个算法的结构即可重新定义该算法的特定步骤。模板方法是一种行为型模式。

2.2 模板方法模式结构

  模板方法模式结构比较简单,其核心是抽象类和其中的模板方法的设计,其结构如下图所示:

  (1)AbstractClass(抽象类):在抽象类中定义了一系列基本操作(Primitive Operations),这些基本操作可以是具体的,也可以是抽象的,每一个基本操作对应算法的一个步骤,在其子类中可以重新定义或实现这些步骤。同时,在抽象类中实现了一个模板方法(Template Method),用于定义一个算法的框架。

  (2)ConcreteClass(具体子类):抽象类的子类,用于实现在父类中声明的抽象基本操作以完成子类特定算法的步骤,也可以覆盖在父类中已经实现的具体基本操作。

  多说无益,下面我们直接看代码实现,一眼就可以明白。

三、重构银行利息计算模块设计

3.1 重构后的设计结构

  其中,Account充当抽象类角色,CurrentAccount与SavingAccount充当具体子类角色。=> 是不是简单得不行?

3.2 具体代码实现

  (1)抽象类:Account

    /// <summary>
    /// 抽象类:Account
    /// </summary>
    public abstract class Account
    {
        // 基本方法 - 具体方法
        public bool Validate(string account, string password)
        {
            Console.WriteLine("账号 : {0}", account);
            Console.WriteLine("密码 : {0}", password);

            if (account.Equals("张无忌") && password.Equals("123456"))
            {
                return true;
            }
            else
            {
                return false;
            }
        }

        // 基本方法 - 抽象方法
        public abstract void CalculateInterest();

        // 基本方法 - 具体方法
        public void Display()
        {
            Console.WriteLine("显示利息");
        }

        // 基本方法 - 钩子方法
        public virtual bool IsAllowDisplay()
        {
            return true;
        }

        // 模板方法
        public void Handle(string account, string password)
        {
            if (!Validate(account, password))
            {
                Console.WriteLine("账户或密码错误,请重新输入!");
                return;
            }

            CalculateInterest();

            if (IsAllowDisplay())
            {
                Display();
            }
        }
    }

  (2)具体子类:CurrentAccount和SavingAccount

    /// <summary>
    /// 具体子类:CurrentAccount => 活期账户类
    /// </summary>
    public class CurrentAccount : Account
    {
        // 重写父类的抽象基本方法
        public override void CalculateInterest()
        {
            Console.WriteLine("按活期利率计算利息!");
        }

        // 重写父类的钩子方法
        public override bool IsAllowDisplay()
        {
            return base.IsAllowDisplay();
        }
    }

    /// <summary>
    /// 具体子类:SavingAccount => 定期账户类
    /// </summary>
    public class SavingAccount : Account
    {
        // 重写父类的抽象基本方法
        public override void CalculateInterest()
        {
            Console.WriteLine("按定期利率计算利息!");
        }

        // 重写父类的钩子方法
        public override bool IsAllowDisplay()
        {
            return false;
        }
    }

  (3)客户端测试

    public class Program
    {
        public static void Main(string[] args)
        {
            Account account = AppConfigHelper.GetAccountInstance() as Account;
            if (account != null)
            {
                account.Handle("张无忌", "123456");
            }

            Console.ReadKey();
        }
    }

  这里,我们将具体子类配置在了配置文件中:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="AccountType" value="Manulife.ChengDu.DesignPattern.TemplateMethod.SavingAccount, Manulife.ChengDu.DesignPattern.TemplateMethod" />
  </appSettings>
</configuration>

  其中,AppConfigHelper类用于获取配置文件中的具体子类的实例:

    public class AppConfigHelper
    {
        public static string GetAccountTypeName()
        {
            string factoryName = null;
            try
            {
                factoryName = System.Configuration.ConfigurationManager.AppSettings["AccountType"];
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
            return factoryName;
        }

        public static object GetAccountInstance()
        {
            string assemblyName = AppConfigHelper.GetAccountTypeName();
            Type type = Type.GetType(assemblyName);

            var instance = Activator.CreateInstance(type);
            return instance;
        }
    }

  编译运行后的结果如下图所示:

  如果这时我们需要更换具体子类,那么无须更改源代码,只需修改配置文件:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="AccountType" value="Manulife.ChengDu.DesignPattern.TemplateMethod.CurrentAccount, Manulife.ChengDu.DesignPattern.TemplateMethod" />
  </appSettings>
</configuration>

  重新运行客户端后的结果如下图所示:

四、模板方法模式总结

4.1 主要优点

  模板方法中可以通过子类来覆盖父类的基本方法,不同的子类可以提供基本方法的不同实现,更换和增加新的子类很方便,符合单一职责和开闭原则。

4.2 主要缺点

  需要为每一个基本方法的不同实现一个子类,如果父类中可变的基本方法太多,将会导致类的个数增加,系统更加庞大,设计也会更加抽象。

4.3 应用场景

  (1)对一些复杂的算法进行分割,将其算法中固定不变的部分设计为模板方法和父类具体方法,而一些可以改变的细节由其子类来实现。

  (2)需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制。

参考资料

  刘伟,《设计模式的艺术—软件开发人员内功修炼之道》

作者:周旭龙

出处:http://edisonchou.cnblogs.com

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 设计模式的征途—23.解释器(Interpreter)模式

    虽然目前计算机编程语言有好几百种,但有时人们还是希望用一些简单的语言来实现特定的操作,只需要向计算机输入一个句子或文件,就能按照预定的文法规则来对句子或文件进行...

    Edison Zhou
  • 数据结构基础温故-2.栈

    现实生活中的事情往往都能总结归纳成一定的数据结构,例如餐馆中餐盘的堆叠和使用,羽毛球筒里装的羽毛球等都是典型的栈结构。而在.NET中,值类型在线程栈上进行分配,...

    Edison Zhou
  • .NET基础拾遗(2)面向对象的实现和异常的处理基础

      在C#中申明一个类型时,只支持单继承(即继承一个父类),但支持实现多个接口(Java也是如此)。像C++可能会支持同时继承自多个父类,但.NET的设计小组认...

    Edison Zhou
  • 结合案例深入解析模板方法设计模式

    模板方法模式是类的行为模式。准备一个抽象类,将部分逻辑以具体方法以及具体构造函数的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的...

    李红
  • Hibernate【映射】续篇

    组件映射 Java主要的类主要有两种方式 组合关系,组合关系对应的就是组件映射 继承关系,继承关系对应的就是继承映射 组件映射实际上就是将组合关系的数据映射成一...

    Java3y
  • CNCF网络研讨会:理解云原生应用程序包(视频+PDF)

    云原生应用程序包cnab.io是一个开源包格式规范,用于使用一个可安装文件管理分布式应用程序。使用bundle,你可以在不同的环境中可靠地提供应用程序资源,并管...

    CNCF
  • CNCF网络研讨会:理解云原生应用程序包(PDF)

    云原生应用程序包cnab.io是一个开源包格式规范,用于使用一个可安装文件管理分布式应用程序。使用bundle,你可以在不同的环境中可靠地提供应用程序资源,并管...

    CNCF
  • Prometheus监控神器-Kubernetes篇(二)

    本篇使用StorageClass来持久化数据,搭建Statefulset的Grafana,并且在Dashboard导入前配置前面已经创建好的Prometheus...

    没有故事的陈师傅
  • Prometheus监控神器-Kubernetes篇(二)

    本篇使用StorageClass来持久化数据,搭建Statefulset的Grafana,并且在Dashboard导入前配置前面已经创建好的Prometheus...

    Kubernetes技术栈
  • 【游戏开发】浅谈游戏开发中常见的设计原则

      俗话说得好:“设计模式,常读常新~”。的确,每读一遍设计模式都会有些新的体会和收获。马三不才,才读了两遍设计模式(还有一遍是在学校学的),属于菜鸟级别的。这...

    马三小伙儿

扫码关注云+社区

领取腾讯云代金券