在實(shí)際編程中宠漩,會(huì)經(jīng)常遇到多個(gè)類中的某些方法實(shí)現(xiàn)邏輯類似的情況,這時(shí)我們可以將這些類中的相同部分抽象到父類中懊直,對于有差異的地方扒吁,子類根據(jù)自身的實(shí)際需求來各自實(shí)現(xiàn)。
以羽毛球運(yùn)動(dòng)為例室囊,打球必有發(fā)接發(fā)環(huán)節(jié)雕崩,發(fā)球分正手和反手兩種(這里不談?wù)撚鹎蚣夹g(shù)細(xì)節(jié)),一般男單反手發(fā)球波俄,女單正手發(fā)球晨逝,但發(fā)接發(fā)這個(gè)環(huán)節(jié)的流程是一致的。
abstract class Badminton
{
public abstract void Serve();
public abstract void Catch();
public abstract void Play();
}
class MenSingle : Badminton
{
public override void Serve()
{
Console.WriteLine("反手發(fā)球......");
}
public override void Catch()
{
Console.WriteLine("正手推底線");
}
public override void Play()
{
Serve();
Catch();
}
}
class WomenSingle : Badminton
{
public override void Serve()
{
Console.WriteLine("正手發(fā)球.......");
}
public override void Catch()
{
Console.WriteLine("軟壓一拍");
}
public override void Play()
{
Serve();
Catch();
}
}
程序開發(fā)中有個(gè)重要的原則:Don't repeat yourself
懦铺。而上面一段代碼中捉貌,子類MenSingle
和WomenSingle
中的Play
方法是重復(fù)的,羽毛球運(yùn)動(dòng)除男單、女單外還有男雙趁窃,女雙牧挣,混雙,如此則代碼中至少五處重復(fù)醒陆,這顯然不利于日后維護(hù)瀑构。
接下來對代碼進(jìn)行改進(jìn):
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("反手發(fā)球......");
}
protected override void Catch()
{
Console.WriteLine("正手推底線");
}
}
class WomenSingle : Badminton
{
protected override void Serve()
{
Console.WriteLine("正手發(fā)球.......");
}
protected override void Catch()
{
Console.WriteLine("軟壓一拍");
}
}
這段代碼將Play
方法放到父類中實(shí)現(xiàn),對于有差異的Serve
和Catch
則交有子類實(shí)現(xiàn)刨摩,這邊是模板方法模式寺晌,封裝不變部分,擴(kuò)展可變部分澡刹。其中Play
方法稱之為模板方法呻征,Serve
和Catch
稱為基本方法。
通常模板方法(可以有多個(gè))在父類中實(shí)現(xiàn)并調(diào)用基本方法以完成固定的邏輯罢浇,且不允許子類重寫陆赋。
基本方法一般為抽象方法,由子類來完成具體的實(shí)現(xiàn)嚷闭≡艿海基本方法的訪問修飾符通常是protected
,不需要對外界暴露(迪米特法則)胞锰,客戶端只需要調(diào)用模板方法即可灾锯。
那么,問題來了胜蛉,世界羽聯(lián)沒有規(guī)定男單必須用反手發(fā)球挠进,女單必須正手發(fā)球。如果男單想用正手發(fā)球怎么辦誊册?為適應(yīng)這種有著多種可能的場景领突,我們對代碼稍作調(diào)整:
abstract class Badminton
{
private void ForehandServe()
{
Console.WriteLine("正手發(fā)球.......");
}
private void BackhandServe()
{
Console.WriteLine("反手發(fā)球......");
}
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("軟壓一拍");
}
}
這里,我們通過在子類中實(shí)現(xiàn)屬性IsForehandServe
來控制父類中具體調(diào)用ForehandServe
方法還是調(diào)用BackhandServe
方法案怯。屬性IsForehandServe
稱為鉤子函數(shù)君旦,根據(jù)鉤子函數(shù)的不同實(shí)現(xiàn),模板方法可以有不同的執(zhí)行結(jié)果嘲碱,即子類對父類產(chǎn)生了影響金砍。
以上,是一個(gè)模板方法的杜撰使用場景麦锯。模板方法模式有個(gè)很重要的特征:父類控制流程恕稠,子類負(fù)責(zé)具體細(xì)節(jié)的實(shí)現(xiàn)。這里有沒有聯(lián)想到IoC(控制反轉(zhuǎn))扶欣?IoC的實(shí)現(xiàn)方式有多種鹅巍,DI只是其中之一千扶,模板方法模式也可以。
許多框架(如:ASP.NET MVC)也是這個(gè)套路骆捧,框架定義一套流程澎羞,然后由不同的類負(fù)責(zé)不同功能的實(shí)現(xiàn),并預(yù)留擴(kuò)展點(diǎn)讓開發(fā)人員可根據(jù)實(shí)際需求進(jìn)行擴(kuò)展開發(fā)敛苇,但整個(gè)框架的處理流程開發(fā)人員是控制不了的妆绞。
小結(jié)
模板方法模式有以下優(yōu)點(diǎn):
1、封裝不變部分枫攀,擴(kuò)展可變部分括饶;
寫程序就因該是這樣,不僅僅是在模板方法模式中
2脓豪、提取公共部分便于日后維護(hù)巷帝;
Ctrl + C,Ctrl + V 大法好,但濫用也是要命的
3扫夜、父類控制流程,子類負(fù)責(zé)實(shí)現(xiàn)驰徊;
如此笤闯,子類便可通過擴(kuò)展的方式來增加功能;
同時(shí)棍厂,對于一些復(fù)雜的算法颗味,我們可以現(xiàn)在父類的模板方法中定義好流程,然后再在子類中去實(shí)現(xiàn)牺弹,思路上也會(huì)清晰不少浦马;
結(jié)語
最后,附一段使用模板方法模式寫的分頁查詢代碼:
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>
/// 數(shù)據(jù)總量
/// </summary>
int DataCount { get; }
/// <summary>
/// 分頁查詢
/// </summary>
/// <param name="pageNumber">頁碼</param>
/// <param name="pageSize">每頁數(shù)據(jù)量</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>
/// 查詢數(shù)據(jù)總數(shù)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("參數(shù):pageNumber不得小于1");
}
if (pageSize <= 0)
{
throw new ArgumentException("參數(shù):pageNumber必須大于0");
}
IEnumerable<T> result;
using (SqlConnection sqlConnection = CreateSqlConnection())
{
string sql = PagingQuerySql(pageNumber, pageSize);
result = sqlConnection.Query<T>(sql);
}
return result;
}
}