第24章 行为型模式—模板方法模式
1. 模板方法模式(Template Method Pattern)的定义
(1)定义:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
(2)模板方法模式的结构和说明

①AbstractClass:抽象类。用于定义算法骨架和抽象的原语操作,具体的子类通过重定义这些原语操作来实现一个算法的各个步骤。在这个类里面,还可以提供算法中通用的实现。此外,该类还实现了一个模板方法,它定义了算法的骨架。该模板方法不仅调用原语操作,也调用AbstractClass或其他对象中的操作。
②ConcreteClass:具体实现类。用来实现算法骨架中的某些步骤,完成与特定子类相关的功能。
【编程实验】在线购物流程

//行为型模式——模板方式模式
//场景:在线购物
/*
网购,天猫或者京东,经历一下四个步骤:
①访问网站 //访问协议和网址不同
②浏览下单 //假设两个网站操作一样
③支付 //天猫:支付宝,东京;货到付款
④收货 //确认操作一样
注意:两个网站第2步和第4步都是一样的,但访问和支付的访问不一样
*/
#include <iostream>
#include <string>
using namespace std;
//抽象模板方法
class OnLineShop
{
protected:
virtual void accessURL() = 0; //由子类去实现
virtual void pay() = 0; //由子类去实现
private:
void createOrder()
{
cout << "创建订单成功" << endl;
}
void receiptGoods()
{
cout << "收到了物品" << endl;
}
public:
//购物
void shop()
{
accessURL(); //第1步:访问网站
createOrder(); //第2步:浏览下单
pay(); //第3步:支付
receiptGoods(); //第4步:收货
}
};
//天猫
class Tmall :public OnLineShop
{
public:
void accessURL()
{
cout << "https://www.tmall.com/" << endl;
}
void pay()
{
cout <<"支付宝"<< endl;
}
};
//京东
class JD :public OnLineShop
{
public:
void accessURL()
{
cout << "http://www.jd.com/" << endl;
}
void pay()
{
cout <<"货到付款"<< endl;
}
};
int main()
{
OnLineShop* tmall = new Tmall();
tmall->shop();
cout << endl;
OnLineShop* jd = new JD();
jd->shop();
return 0;
}
/*输出结果:
https://www.tmall.com/
创建订单成功
支付宝
收到了物品
http://www.jd.com/
创建订单成功
货到付款
收到了物品
*/
2. 思考模板方法模式
(1)模板方法模式的本质:固定算法骨架
(2)模板方法模式的核心:处理某个流程的代码己经都具备,但是其中某些节点的代码不能确定。因此,将这些节点的代码实现转移给子类完成。即处理步骤父类中己经定义好,具体实现延迟到子类中定义。
(3)模板方法中的变与不变
①需要变化的地方都通过纯虚函数,把具体实现延迟到子类中。
②不变的部分,进行公共实现。
(4)好莱坞法则:Don’t call me, I’ll call you
①正常的控制结构:子类调用父类方法,这很正常因为子类继承了父类,所以是知道父类的。
②反向的控制结构:父类调用子类方法,因为父类是不可能知道子类的,所以这也是一种反向的控制结构。在C++中是通过虚函数实现的。
3. 模板的写法
(1)模板方法:就是定义算法骨架的方法。如上例中的shop()
(2)具体的操作:在模板中直接实现的某些步骤方法,这些步骤的实现通常是固定的,不怎么会变化的,因此可以将其当作公共功能实现在模板中。如果不需子类访问这些函数,可以定义为private。子类需要访问定义为protected
(3)具体的AbstractClass操作:在模板中实现某些公共功能,可以提供给子类使用,一般不是具体的算法步骤的实现,而是一些辅助的公共功能。
(4)原语操作:就是在模板中定义的纯虚函数,通常是模板方法需要调用的操作,是必须的操作,而且在父类中还没办法确定下来如何实现,需要子类来真正实现的方法。
(5)钩子操作:在模板中定义,并提供默认实现的操作,这些方法通常被视为可扩展的点,但不是必须的,子类可以有选择地覆盖这些方法,以提供新的实现来扩展功能,这些函数一般被声明为virtual。
【编程实验】两种登录控制(普通用户和管理员)

//行为型模式——模板方式模式
//场景:两种登录控制(普通用户和管理员)
/*
说明:
1.普通用户和管理员是在数据库中是存储在不同的表里。此外管理员的
密码是加密的
2. 这两种角色登录的是不同模块,有不同的页面、不同的处理逻辑和不
同的数据存储。
登录控制的算法框架
1. 根据登录人员的编号去获取相应的数据
2. 获取对登录人员填写的密码进行加密后的数据,如果不需要加密,那就
直接返回登录人员填写的密码数据。(可选)
3. 判断登录人员填写的数据和从数据库中获取的数据是否匹配
*/
#include <iostream>
#include <string>
using namespace std;
//*********************************辅助类*************************
//封装进行登录控制所需要的数据(即前台页面填写的用户数据信息)
//本例为了简单,登录时的信息模型和数据表中用户信息的数据模型共用下面的数据结构
class LoginModel
{
private:
string loginId; //登录人员编号,可能是管理员或普通用户编号
string pwd; //登录密码
//string question; //密码验证问题,这里为了简单就不需要了。
//string answer; //密码验证答案
public:
string& getLoginId(){return loginId;}
void setLoginId(string value)
{
this->loginId = value;
}
string& getPassword(){return pwd;}
void setPassword(string value)
{
this->pwd = value;
}
};
//登录控制的模板,也是登录控制算法的骨架
class LoginTemplate
{
protected:
//根据登录编号从数据库中找到相应的用户信号
virtual LoginModel* findLoginUser(string loginId) = 0;
//对密码进行加密,提供默认的实现,子类可以去覆盖
virtual string encrptPwd(string pwd)
{
return pwd;
}
//判断用户填写的登录数据和数据库的数据是否匹配
//@param lm:用户填写的登录数据
//@param dblm:数据库中相应用户的信息
bool match(LoginModel* lm, LoginModel* dbLm)
{
bool bRet;
bRet = (lm->getLoginId() == dbLm->getLoginId());
bRet &= (lm->getPassword() == dbLm->getPassword());
return bRet;
}
public:
bool login(LoginModel* lm)
{
bool bRet = false;
//1.根据登录人员的编号从数据库中获取相应用户信息
LoginModel* dbLm = findLoginUser(lm->getLoginId());
if(dbLm != NULL)
{
//2. 对密码进行加密
string encryptPwd = encrptPwd(lm->getPassword());
//把加密后的密码设置回到登录数据模型中
lm->setPassword(encryptPwd);
//3. 判断是否匹配
bRet = match(lm, dbLm);
delete dbLm;
}
return bRet;
}
virtual ~LoginTemplate(){}
};
//普通用户登录控制的逻辑处理
class NormalLogin : public LoginTemplate
{
protected:
LoginModel* findLoginUser(string loginId)
{
//这里省略了数据访问,仅做示意,返回一个
//有默认的数据对象
LoginModel* lm = new LoginModel();
lm->setLoginId(loginId);
lm->setPassword("NormalPwd");
return lm;
}
};
//管理员登录的控制逻辑处理
class AdminLogin : public LoginTemplate
{
protected:
LoginModel* findLoginUser(string loginId)
{
//这里省略了数据访问,仅做示意,返回一个
//有默认的数据对象
LoginModel* lm = new LoginModel();
lm->setLoginId(loginId);
lm->setPassword("AdminPwd");
return lm;
}
string encrptPwd(string pwd)
{
//覆盖父类的方法,提供真正的加密实现(如md5、3DES等)
//这里省略了,直接输出示意一下
cout << "使用MD5进行密码加密" <<endl;
return pwd;
}
};
int main()
{
//准备登录人员的信息
LoginModel lm;
lm.setLoginId("admin");
lm.setPassword("AdminPwd");
//用这个帐号去模拟登录两个系统
LoginTemplate* adminLt = new AdminLogin();
LoginTemplate* normalLt = new NormalLogin();
//登录测试
bool flag = adminLt->login(&lm);
string result = flag?"成功": "失败";
cout <<"用户(" <<lm.getLoginId() <<")登录管理员后台:" << result << endl;
flag = normalLt->login(&lm);
result = flag?"成功": "失败";
cout <<"用户(" <<lm.getLoginId() <<")登录普通用户界面:" << result << endl;
delete adminLt;
delete normalLt;
return 0;
}
/*输出结果:
使用MD5进行密码加密
用户(admin)登录管理员后台:成功
用户(admin)登录普通用户界面:失败
*/
4. 模板方法的优缺点
(1)优点:实现代码复用。通过把子类的公共功能抽取出来,放到模板中去实现。
(2)缺点:算法骨架不容易升级。模板方法模式最基本的功能是通过模板的制定,把算法骨架完全固定下来。事实上模板和子类是非常耦合的,如果要对模板中的算法骨架进行变更,可能要求所有相关的子类进行相应的变化。所以抽取算法骨架时应尽量确保是不会变化的部分才放到模板中。
5. 模板方法的应用场景
(1)数据库访问的封装
(2)实现一个算法时,整体步骤很固定。但是某些部分易变,这部分可以抽象出来,留给子类去实现。
(3)各个子类中具有公共行为,应该抽取出来,集中在一个公共类中去实现,实现代码复用。
(4)需要控制子类扩展的情况。模板方法模式会在特定的点来调用子类的方法,这样只允许在这些点进行扩展。
6. 相关模式
(1)模板方法和工厂方法
模板方法可以通过工厂方法模式获取需要调用的对象
(2)模板方法模式和策略模式
①从表面上看,两个模式都能实现算法的封装,但模板方法封装的是算法的骨架,这个骨架是不变的,变化的是算法中某些步骤的具体实现;而策略模式是把某个步骤的具体实现算法封装起来,所有封装的算法对象是等价的,可以相互替换。
②可以在模板方法中使用策略模式,就是把那些变化的算法步骤使用实现策略模式来实现,但是具体选择哪个策略还是要由外部来确定,而整体的算法步骤,也就是算法骨架则由模板方法来定义了。

浙公网安备 33010602011771号