在实际编程中,会经常遇到多个类中的某些方法实现逻辑类似的情况,这时我们可以将这些类中的相同部分抽象到父类中,对于有差异的地方,子类根据自身的实际需求来各自实现。
以羽毛球运动为例,打球必有发接发环节,发球分正手和反手两种(这里不谈论羽球技术细节),一般男单反手发球,女单正手发球,但发接发这个环节的流程是一致的。
abstract class Badminton { public abstract void Serve(); public abstract void Catch(); public abstract void Play(); } class MenSingle : Badminton { public override void Serve() { Console.WriteLine("反手发球......"); } public override void Catch() { Console.WriteLine("正手推底线"); } public override void Play() { Serve(); Catch(); } } class WomenSingle : Badminton { public override void Serve() { Console.WriteLine("正手发球......."); } public override void Catch() { Console.WriteLine("软压一拍"); } public override void Play() { Serve(); Catch(); } }
程序开发中有个重要的原则:Don't repeat yourself
。而上面一段代码中,子类MenSingle
和WomenSingle
中的Play
方法是重复的,羽毛球运动除男单、女单外还有男双,女双,混双,如此则代码中至少五处重复,这显然不利于日后维护。
接下来对代码进行改进:
abstract class Badminton { protected abstract void Serve(); protected abstract void Catch(); public void Play() { Serve(); Catch(); } } class MenSingle : Badminton { protected override void Serve() { Console.WriteLine("反手发球......"); } protected override void Catch() { Console.WriteLine("正手推底线"); } } class WomenSingle : Badminton { protected override void Serve() { Console.WriteLine("正手发球......."); } protected override void Catch() { Console.WriteLine("软压一拍"); } }
这段代码将Play
方法放到父类中实现,对于有差异的Serve
和Catch
则交有子类实现,这边是模板方法模式,封装不变部分,扩展可变部分。其中Play
方法称之为模板方法,Serve
和Catch
称为基本方法。
通常模板方法(可以有多个)在父类中实现并调用基本方法以完成固定的逻辑,且不允许子类重写。
基本方法一般为抽象方法,由子类来完成具体的实现。基本方法的访问修饰符通常是protected
,不需要对外界暴露(迪米特法则),客户端只需要调用模板方法即可。
那么,问题来了,世界羽联没有规定男单必须用反手发球,女单必须正手发球。如果男单想用正手发球怎么办?为适应这种有着多种可能的场景,我们对代码稍作调整:
abstract class Badminton { private void ForehandServe() { Console.WriteLine("正手发球......."); } private void BackhandServe() { Console.WriteLine("反手发球......"); } protected abstract void Catch(); protected abstract bool IsForeHandServe { get; } public void Play() { if (IsForeHandServe) { ForehandServe(); } else { BackhandServe(); } Catch(); } } class MenSingle : Badminton { protected override bool IsForeHandServe => false; protected override void Catch() { Console.WriteLine("正手推底线"); } } class WomenSingle : Badminton { protected override bool IsForeHandServe => true; protected override void Catch() { Console.WriteLine("软压一拍"); } }
这里,我们通过在子类中实现属性IsForehandServe
来控制父类中具体调用ForehandServe
方法还是调用BackhandServe
方法。属性IsForehandServe
称为钩子函数,根据钩子函数的不同实现,模板方法可以有不同的执行结果,即子类对父类产生了影响。
以上,是一个模板方法的杜撰使用场景。模板方法模式有个很重要的特征:父类控制流程,子类负责具体细节的实现。这里有没有联想到IoC(控制反转)?IoC的实现方式有多种,DI只是其中之一,模板方法模式也可以。
许多框架(如:ASP.NET MVC)也是这个套路,框架定义一套流程,然后由不同的类负责不同功能的实现,并预留扩展点让开发人员可根据实际需求进行扩展开发,但整个框架的处理流程开发人员是控制不了的。
小结
模板方法模式有以下优点:
1、封装不变部分,扩展可变部分;
写程序就因该是这样,不仅仅是在模板方法模式中
2、提取公共部分便于日后维护;
Ctrl + C,Ctrl + V 大法好,但滥用也是要命的
3、父类控制流程,子类负责实现;
如此,子类便可通过扩展的方式来增加功能;
同时,对于一些复杂的算法,我们可以现在父类的模板方法中定义好流程,然后再在子类中去实现,思路上也会清晰不少;
结语
最后,附一段使用模板方法模式写的分页查询代码:
public class DbBase { public virtual string TableName { get { throw new NotImplementedException($"属性:{nameof(TableName)}不得为空!"); } } protected virtual string ConnectionString { get { throw new NotImplementedException("属性:" + nameof(ConnectionString) + "不得为空!"); } } protected SqlConnection CreateSqlConnection() { return CreateSqlConnection(ConnectionString); } protected SqlConnection CreateSqlConnection(string connnectionString) { SqlConnection dbConnection = new SqlConnection(connnectionString); if (dbConnection.State == ConnectionState.Closed) { dbConnection.Open(); } return dbConnection; }
public interface IPagingQuery<T> where T : class { /// <summary> /// 数据总量 /// </summary> int DataCount { get; } /// <summary> /// 分页查询 /// </summary> /// <param name="pageNumber">页码</param> /// <param name="pageSize">每页数据量</param> /// <returns></returns> IEnumerable<T> PagingQuery(int pageNumber, int pageSize); }
public abstract class PagingQueryDalBase<T> : DbBase, IPagingQuery<T> where T : class { public int DataCount => GetDataCount(); /// <summary> /// 查询数据总数SQL /// </summary> protected abstract string QueryDataCountSql(); private int GetDataCount() { int dataCount; using (SqlConnection sqlConnection = base.CreateSqlConnection()) { string sql = QueryDataCountSql(); dataCount = sqlConnection.QueryFirstOrDefault<int>(sql); } return dataCount; } /// <summary> /// 分页查询SQL /// </summary> protected abstract string PagingQuerySql(int pageNumber, int pageSize); public IEnumerable<T> PagingQuery(int pageNumber, int pageSize) { if (pageNumber - 1 < 0) { throw new ArgumentException("参数:pageNumber不得小于1"); } if (pageSize <= 0) { throw new ArgumentException("参数:pageNumber必须大于0"); } IEnumerable<T> result; using (SqlConnection sqlConnection = CreateSqlConnection()) { string sql = PagingQuerySql(pageNumber, pageSize); result = sqlConnection.Query<T>(sql); } return result; } }