一、缘起
变化--是软件设计的永恒主题,如何管理变化带来的复杂性?
设计模式的艺术性和复杂度就在于如何分析,并发现系统中的变化点和稳定点,并使用特定的设计方法来应对这种变化。
在软件构建过程中,对于某一项任务,它常常有稳定的整体操作结构,但各个子步骤却有很多改变的需求,或者由于固有的原因(比如框架与应用之间的关系)而无法和任务的整体结构同时实现。
如何在确定稳定操作结构的前提下,来灵活应对各个子步骤的变化或者晚期实现需求?我们下面看下模板方法模式是怎样应对这样的需求变化的。
二、定义
GOF给出的模板方法模式的定义:
定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。Template Method使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
三、应用举例
1、需求
应用系统一般都需要系统登录控制的功能,有些系统有多个登录控制的功能,比如,普通用户可以登录前台,进行相应的业务操作,工作人员可以登录后台,进行相应的系统管理或业务处理。
2、不用设计模式的解决方案
普通用户登录和工作人员登录是不同的模块,有不同的页面、不同的逻辑处理和不同的数据存储,因此,在实现上完全作为两个独立的小模块来完成。
先看看普通用户登录的逻辑处理:
/** * 普通用户登录控制的逻辑处理 */ public class NormalLogin { /** * 判断登录数据是否正确,也就是是否能登录成功 * @param lm 封装登录数据的Model * @return true表示登录成功,false表示登录失败 */ public boolean login(LoginModel lm) { //1:从数据库获取登录人员的信息, 就是根据用户编号去获取人员的数据 UserModel um = this.findUserByUserId(lm.getUserId()); //2:判断从前台传递过来的登录数据,和数据库中已有的数据是否匹配 //先判断用户是否存在,如果um为null,说明用户肯定不存在 //但是不为null,用户不一定存在,因为数据层可能返回new UserModel(); //因此还需要做进一步的判断 if (um != null) { //如果用户存在,检查用户编号和密码是否匹配 if (um.getUserId().equals(lm.getUserId()) && um.getPwd().equals(lm.getPwd())) { return true; } } return false; } /** * 根据用户编号获取用户的详细信息 * @param userId 用户编号 * @return 对应的用户的详细信息 */ private UserModel findUserByUserId(String userId) { // 这里省略具体的处理,仅做示意,返回一个有默认数据的对象 UserModel um = new UserModel(); um.setUserId(userId); um.setName("test"); um.setPwd("test"); um.setUuid("User0001"); return um; } }
对应的LoginModel:
/** * 描述登录人员登录时填写的信息的数据模型 */ public class LoginModel { private String userId,pwd; public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; } public String getPwd() { return pwd; } public void setPwd(String pwd) { this.pwd = pwd; } }
对应的UserModel:
/** * 描述用户信息的数据模型 */ public class UserModel { private String uuid,userId,pwd,name; public String getUuid() { return uuid; } public void setUuid(String uuid) { this.uuid = uuid; } public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; } public String getPwd() { return pwd; } public void setPwd(String pwd) { this.pwd = pwd; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
再看看工作人员登录的逻辑处理:
/** * 工作人员登录控制的逻辑处理 */ public class WorkerLogin { /** * 判断登录数据是否正确,也就是是否能登录成功 * @param lm 封装登录数据的Model * @return true表示登录成功,false表示登录失败 */ public boolean login(LoginModel lm) { //1:根据工作人员编号去获取工作人员的数据 WorkerModel wm = this.findWorkerByWorkerId(lm.getWorkerId()); //2:判断从前台传递过来的用户名和加密后的密码数据,和数据库中已有的数据是否匹配 //先判断工作人员是否存在,如果wm为null,说明工作人员肯定不存在 //但是不为null,工作人员不一定存在, //因为数据层可能返回new WorkerModel();因此还需要做进一步的判断 if (wm != null) { //3:把从前台传来的密码数据,使用相应的加密算法进行加密运算 String encryptPwd = this.encryptPwd(lm.getPwd()); //如果工作人员存在,检查工作人员编号和密码是否匹配 if (wm.getWorkerId().equals(lm.getWorkerId()) && wm.getPwd().equals(encryptPwd)) { return true; } } return false; } /** * 对密码数据进行加密 * @param pwd 密码数据 * @return 加密后的密码数据 */ private String encryptPwd(String pwd){ //这里对密码进行加密,省略了 return pwd; } /** * 根据工作人员编号获取工作人员的详细信息 * @param workerId 工作人员编号 * @return 对应的工作人员的详细信息 */ private WorkerModel findWorkerByWorkerId(String workerId) { // 这里省略具体的处理,仅做示意,返回一个有默认数据的对象 WorkerModel wm = new WorkerModel(); wm.setWorkerId(workerId); wm.setName("Worker1"); wm.setPwd("worker1"); wm.setUuid("Worker0001"); return wm; } }
对应的LoginModel:
/** * 描述登录人员登录时填写的信息的数据模型 */ public class LoginModel{ private String workerId,pwd; public String getWorkerId() { return workerId; } public void setWorkerId(String workerId) { this.workerId = workerId; } public String getPwd() { return pwd; } public void setPwd(String pwd) { this.pwd = pwd; } }
对应的WorkerModel:
/** * 描述工作人员信息的数据模型 */ public class WorkerModel { private String uuid,workerId,pwd,name; public String getUuid() { return uuid; } public void setUuid(String uuid) { this.uuid = uuid; } public String getWorkerId() { return workerId; } public void setWorkerId(String workerId) { this.workerId = workerId; } public String getPwd() { return pwd; } public void setPwd(String pwd) { this.pwd = pwd; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
3、有何问题
两种登录的实现很相似,现在是两个独立模块来实现。如果今后有新的需求要扩展功能,比如要添加“控制同一个编号同时只能登录一次”的功能,那么这两个模块都需要修改。
总结起来,主要有两个明显的问题:一是重复或相似代码太多,没有做到复用;而是扩展起来不方便,每次需要修改原有代码,违反开闭原则。
4、使用模板方法模式来解决问题
使用模板方法模式来解决刚才的问题。定义出一个抽象的父类,在这个父类中定义模板方法,这个模板方法应该实现进行登录控制的整体的算法步骤。对于公共的功能,就放到这个父类中实现,而这个父类无法决定的功能,就延迟到子类去实现。
首先数据模型可以统一起来。当然,如果各个子类实现需要其他的数据,还可以自行扩展。统一的LoginModel:
/** * 封装进行登录控制所需要的数据 */ public class LoginModel { /** * 登录人员的编号,通用的,可能是用户编号,也可能是工作人员编号 */ private String loginId; /** * 登录的密码 */ private String pwd; public String getLoginId() { return loginId; } public void setLoginId(String loginId) { this.loginId = loginId; } public String getPwd() { return pwd; } public void setPwd(String pwd) { this.pwd = pwd; } }
然后定义公共的登录控制算法骨架:
/** * 登录控制的模板 */ public abstract class LoginTemplate { /** * 判断登录数据是否正确,也就是是否能登录成功 * @param lm 封装登录数据的Model * @return true表示登录成功,false表示登录失败 */ public final boolean login(LoginModel lm){ //1:根据登录人员的编号去获取相应的数据 LoginModel dbLm = this.findLoginUser(lm.getLoginId()); if(dbLm!=null){ //2:对密码进行加密 String encryptPwd = this.encryptPwd(lm.getPwd()); //把加密后的密码设置回到登录数据模型里面 lm.setPwd(encryptPwd); //3:判断是否匹配 return this.match(lm, dbLm); } return false; } /** * 根据登录编号来查找和获取存储中相应的数据 * @param loginId 登录编号 * @return 登录编号在存储中相对应的数据 */ public abstract LoginModel findLoginUser(String loginId); /** * 对密码数据进行加密 * @param pwd 密码数据 * @return 加密后的密码数据 */ public String encryptPwd(String pwd){ return pwd; } /** * 判断用户填写的登录数据和存储中对应的数据是否匹配得上 * @param lm 用户填写的登录数据 * @param dbLm 在存储中对应的数据 * @return true表示匹配成功,false表示匹配失败 */ public boolean match(LoginModel lm,LoginModel dbLm){ if(lm.getLoginId().equals(dbLm.getLoginId()) && lm.getPwd().equals(dbLm.getPwd())){ return true; } return false; } }
普通用户登录控制的逻辑处理:
/** * 普通用户登录控制的逻辑处理 */ public class NormalLogin extends LoginTemplate{ public LoginModel findLoginUser(String loginId) { // 这里省略具体的处理,仅做示意,返回一个有默认数据的对象 LoginModel lm = new LoginModel(); lm.setLoginId(loginId); lm.setPwd("testpwd"); return lm; } }
工作人员登录控制的逻辑处理:
/** * 工作人员登录控制的逻辑处理 */ public class WorkerLogin extends LoginTemplate{ public LoginModel findLoginUser(String loginId) { // 这里省略具体的处理,仅做示意,返回一个有默认数据的对象 LoginModel lm = new LoginModel(); lm.setLoginId(loginId); lm.setPwd("workerpwd"); return lm; } public String encryptPwd(String pwd){ //覆盖父类的方法,提供真正的加密实现 //这里对密码进行加密,比如使用:MD5、3DES等等,省略了 System.out.println("使用MD5进行密码加密"); return pwd; } }
当来个新的需求,普通用户登录逻辑处理需要修改时,新增一个普通用户登录加强版逻辑处理:
/** * 普通用户登录控制加强版的逻辑处理 */ public class NormalLogin2 extends LoginTemplate{ public LoginModel findLoginUser(String loginId) { // 这里省略具体的处理,仅做示意,返回一个有默认数据的对象 //注意一点:这里使用的是自己需要的数据模型了 NormalLoginModel nlm = new NormalLoginModel(); nlm.setLoginId(loginId); nlm.setPwd("testpwd"); nlm.setQuestion("testQuestion"); nlm.setAnswer("testAnswer"); return nlm; } public boolean match(LoginModel lm,LoginModel dbLm){ //这个方法需要覆盖,因为现在进行登录控制的时候, //需要检测4个值是否正确,而不仅仅是缺省的2个 //先调用父类实现好的,检测编号和密码是否正确 boolean f1 = super.match(lm, dbLm); if(f1){ //如果编号和密码正确,继续检查问题和答案是否正确 //先把数据转换成自己需要的数据 NormalLoginModel nlm = (NormalLoginModel)lm; NormalLoginModel dbNlm = (NormalLoginModel)dbLm; //检查问题和答案是否正确 if(dbNlm.getQuestion().equals(nlm.getQuestion()) && dbNlm.getAnswer().equals(nlm.getAnswer())){ return true; } } return false; } }
增强的普通用户的数据模型:
/** * 封装进行登录控制所需要的数据,在公共数据的基础上, * 添加具体模块需要的数据 */ public class NormalLoginModel extends LoginModel{ /** * 密码验证问题 */ private String question; /** * 密码验证答案 */ private String answer; public String getQuestion() { return question; } public void setQuestion(String question) { this.question = question; } public String getAnswer() { return answer; } public void setAnswer(String answer) { this.answer = answer; } }
客户端:
public class Client { public static void main(String[] args) { //准备登录人的信息 LoginModel lm = new LoginModel(); lm.setLoginId("admin"); lm.setPwd("workerpwd"); //准备用来进行判断的对象 LoginTemplate lt = new WorkerLogin(); LoginTemplate lt2 = new NormalLogin(); //进行登录测试 boolean flag = lt.login(lm); System.out.println("可以登录工作平台="+flag); boolean flag2 = lt2.login(lm); System.out.println("可以进行普通人员登录="+flag2); //准备登录人的信息 NormalLoginModel nlm = new NormalLoginModel(); nlm.setLoginId("testUser"); nlm.setPwd("testpwd"); nlm.setQuestion("testQuestion"); nlm.setAnswer("testAnswer"); //准备用来进行判断的对象 LoginTemplate lt3 = new NormalLogin2(); //进行登录测试 boolean flag3 = lt3.login(nlm); System.out.println("可以进行普通人员加强版登录="+flag3); } }
四、总结
Template Method模式是一种非常基础性的设计模式,在面向对象系统中有着大量的应用。它用最简洁的机制(成员函数的多态性)为很多应用程序框架提供了灵活的扩展点,是代码复用方面的基本实现结构。
被Template Method调用的成员函数可以具有实现,也可以没有任何实现(抽象方法),但一般推荐将它们设置为protected方法。