白话设计模式之(83):模板方法模式——深度剖析与实战应用
白话设计模式之(83):模板方法模式——深度剖析与实战应用
大家好!在软件开发的学习过程中,我们都在不断探寻如何让代码更高效、更易维护。设计模式作为编程领域的重要知识,为我们解决各种复杂问题提供了有效的思路和方法。今天,咱们继续深入探讨模板方法模式,不仅会详细剖析它的核心要点,还会结合实际场景,看看它在不同情况下的应用,尤其是与其他模式的关联。希望通过这篇博客,能和大家一起全面掌握模板方法模式,从理论到实践,深入理解其精髓,在实际编程中灵活运用,让代码更加简洁、健壮。
一、写作初衷
在软件开发的实际工作中,我们常常会遇到各种具有相似流程但又存在差异的功能需求。比如在文档中提到的登录控制功能,普通用户登录和工作人员登录虽然都包含验证环节,但具体的验证细节却有所不同;还有增删改查功能,不同业务场景下的实现步骤相似,但涉及到具体的数据表结构和操作细节时又各有差异。如果每次都针对这些相似功能重新编写代码,不仅会增加开发工作量,还会使代码变得冗长复杂,难以维护和扩展。模板方法模式正是为解决这类问题而诞生的。它通过定义一个通用的算法框架,将共性部分提取到父类,把具体实现细节留给子类,实现了代码的高效复用。我希望通过分享这篇博客,能和大家一起深入学习模板方法模式,从基础概念到实际应用中的各种细节,全面掌握这一模式,让大家在面对类似的代码复用和扩展需求时能够游刃有余,编写出更优雅、更健壮的代码。
二、模板方法模式核心要点回顾
(一)定义与概念
模板方法模式的定义是:定义一个操作中的算法的框架,而将一些步骤延迟到子类中。简单来说,就好比制作不同口味的蛋糕,制作蛋糕的基本流程(如准备材料、搅拌、烘焙、装饰)是固定的,这就是算法框架,我们把它定义在一个抽象类中作为模板方法。而不同口味蛋糕在材料选择、装饰方式等方面有所不同,这些具体的差异部分就由子类来实现。在编程中,通过这种方式,我们可以把共性的代码放在父类中复用,同时让子类根据具体需求去实现不同的部分,从而实现代码的灵活性和可扩展性。
(二)代码示例
以一个简单的图形绘制系统为例,系统中有不同类型的图形(如圆形、矩形),绘制图形的基本流程是相似的,但具体的绘制方式有所不同。
// 抽象图形绘制类
public abstract class ShapeDrawer {
// 模板方法,定义绘制图形的基本流程
public final void draw() {
prepareCanvas();
doDraw();
finishDrawing();
}
// 准备画布,所有图形绘制通用的简单实现
protected void prepareCanvas() {
System.out.println("准备画布");
}
// 完成绘制,所有图形绘制通用的简单实现
protected void finishDrawing() {
System.out.println("绘制完成,清理资源");
}
// 具体绘制图形,抽象方法,由子类实现
protected abstract void doDraw();
}
// 圆形绘制类
public class CircleDrawer extends ShapeDrawer {
@Override
protected void doDraw() {
System.out.println("绘制圆形");
}
}
// 矩形绘制类
public class RectangleDrawer extends ShapeDrawer {
@Override
protected void doDraw() {
System.out.println("绘制矩形");
}
}
// 客户端代码
public class DrawingApp {
public static void main(String[] args) {
// 创建圆形绘制对象并绘制
ShapeDrawer circle = new CircleDrawer();
circle.draw();
// 创建矩形绘制对象并绘制
ShapeDrawer rectangle = new RectangleDrawer();
rectangle.draw();
}
}
在这个示例中,ShapeDrawer类定义了绘制图形的模板方法draw,包含了绘制图形的基本流程。其中prepareCanvas和finishDrawing方法有通用的实现,而doDraw方法是抽象的,由子类根据不同图形类型的特点来实现。CircleDrawer和RectangleDrawer类分别继承自ShapeDrawer类,并实现了doDraw方法,展示了不同图形绘制的差异。客户端通过创建不同的子类对象并调用draw方法,实现了不同类型图形的绘制操作,同时复用了模板方法中的通用代码。
(三)应用场景
- 算法流程相似但细节不同的场景:在很多业务场景中都存在这样的情况,比如不同类型的报表生成,基本的报表生成步骤(如数据查询、数据处理、报表展示)是相似的,但不同报表的数据来源和展示格式可能不同。使用模板方法模式,可以将通用的报表生成流程定义在抽象类中,而具体的数据查询和展示格式由子类实现。
- 多个子类有公共行为的场景:当多个子类中存在一些公共的行为时,可以将这些公共行为提取到父类的模板方法中,提高代码的复用性。例如在一个图形绘制系统中,不同图形(如圆形、矩形、三角形)的绘制过程可能都包含初始化画笔、绘制图形、清理资源等步骤,这些公共步骤可以在父类中定义,子类只需实现具体的图形绘制逻辑。
- 框架设计:在框架设计中,模板方法模式也有广泛应用。框架定义了一些通用的流程和接口,让开发者根据具体需求去实现特定的功能。比如在Web开发框架中,请求处理的基本流程(如接收请求、解析请求、处理业务逻辑、返回响应)是固定的,但具体的业务逻辑处理由开发者编写的代码来实现。
(四)模板方法模式的结构与角色
- AbstractClass(抽象类):定义了模板方法,包含一个或多个抽象方法和具体方法。模板方法定义了算法的框架,抽象方法由子类实现,具体方法提供通用的实现。在图形绘制示例中,
ShapeDrawer类就是抽象类,它定义了draw模板方法,以及抽象方法doDraw和具体方法prepareCanvas、finishDrawing。 - ConcreteClass(具体子类):实现抽象类中的抽象方法,提供具体的实现细节。
CircleDrawer和RectangleDrawer类就是具体子类,它们分别实现了ShapeDrawer类中的doDraw方法,展示了不同图形绘制的具体实现。
(五)模板方法模式的优缺点
- 优点
- 提高代码复用性:将公共的代码提取到抽象类的模板方法中,减少了重复代码的编写,提高了代码的复用性。例如在图形绘制系统中,不同图形绘制的通用流程代码只需要编写一次,不同图形的特殊绘制部分由各自的子类实现。
- 便于维护和扩展:当需要修改算法的框架或通用部分时,只需要在抽象类中进行修改,所有子类都会受到影响,便于维护。同时,添加新的子类也很方便,只需要实现抽象类中的抽象方法即可,不会影响已有的代码。比如在图形绘制系统中,如果要添加新的图形类型,只需要创建一个新的子类并实现
doDraw方法即可。 - 符合开闭原则:模板方法模式通过将算法的框架和具体实现分离,使得在不修改现有代码的情况下,可以方便地添加新的实现。当需要增加新的图形类型或修改某个图形的绘制逻辑时,只需要修改或添加具体子类,而不需要修改抽象类中的模板方法,符合开闭原则。
- 缺点
- 增加系统复杂度:对于简单的业务场景,使用模板方法模式可能会增加系统的复杂度。因为需要定义抽象类和具体子类,增加了类的数量和代码的层级结构。例如,如果只是简单的两个功能模块,它们之间的相似性较少,使用模板方法模式可能会显得过于繁琐。
- 子类依赖抽象类:具体子类依赖于抽象类的定义,如果抽象类的接口或模板方法发生变化,可能会影响到所有的子类。这就要求在设计抽象类时要谨慎考虑,尽量保证接口的稳定性。比如在图形绘制系统中,如果抽象类
ShapeDrawer中的模板方法draw的参数或调用顺序发生变化,所有的子类都可能需要进行相应的修改。
三、模板方法模式的进阶内容
(一)模板的写法
在实现模板时,模板中通常包含多种类型的方法:
- 模板方法:定义算法骨架的核心方法,它规定了算法的执行顺序和整体流程。在图形绘制示例中,
draw方法就是模板方法,它整合了准备画布、绘制图形和完成绘制等步骤,形成了图形绘制的基本框架。 - 具体操作方法:在模板中直接实现某些固定步骤的方法。这些方法的算法固定且不常变化,为子类提供了通用功能。比如在图形绘制示例中,
prepareCanvas和finishDrawing方法就是具体操作方法,它们为所有图形的绘制过程提供了通用的实现。 - 抽象操作方法(原语操作):在模板中定义的抽象方法,是模板方法执行过程中必须调用但父类无法确定具体实现的操作,需要子类来实现。在图形绘制示例中,
doDraw方法就是抽象操作方法,不同的图形子类通过实现该方法来展示各自独特的绘制逻辑。 - 钩子操作方法:在模板中定义并提供默认实现的操作方法。子类可以根据自身需求有选择地覆盖这些方法来扩展功能。钩子操作不是必须的,但为子类提供了灵活扩展的点。例如,在一个更复杂的图形绘制系统中,可能有一个
beforeDraw的钩子方法,默认实现为空,但某些特殊图形在绘制前可能需要进行额外操作,此时子类就可以覆盖该方法来实现这些特殊需求。 - 工厂方法:在模板方法中,如果需要获取某些对象实例,可以考虑使用工厂方法模式。将具体创建对象的逻辑延迟到子类中实现,这样可以根据不同的子类需求创建不同类型的对象。比如在图形绘制系统中,如果不同图形需要不同的画笔对象,就可以在模板类中定义一个工厂方法,由子类来实现具体创建画笔的逻辑。
(二)Java回调与模板方法模式
在Java开发中,除了使用继承的方式实现模板方法模式,还可以通过Java回调技术来达到类似的效果。Java回调利用接口定义方法,通过动态绑定技术在运行时调用具体实现类中的方法,这可以看作是模板方法模式的一种变形实现。在这种实现方式中,我们可以使用匿名内部类来实现回调方法,使代码更加简洁灵活。
以登录控制功能为例,我们先定义一个回调接口,包含所有可能被扩展的方法:
// 登录控制的回调接口
public interface LoginCallback {
LoginModel findLoginUser(String loginId);
String encryptPwd(String pwd, LoginTemplate template);
boolean match(LoginModel lm, LoginModel dbLm, LoginTemplate template);
}
然后定义登录控制模板类,此时它不再是抽象类,所有抽象方法被删除,并且在模板方法中通过回调接口来调用具体实现:
// 登录控制模板类
public class LoginTemplate {
public final boolean login(LoginModel lm, LoginCallback callback) {
LoginModel dbLm = callback.findLoginUser(lm.getLoginId());
if (dbLm != null) {
String encryptPwd = callback.encryptPwd(lm.getPwd(), this);
lm.setPwd(encryptPwd);
return callback.match(lm, dbLm, this);
}
return false;
}
}
在客户端使用时,可以通过匿名内部类实现回调接口:
public class Client {
public static void main(String[] args) {
LoginModel lm = new LoginModel();
lm.setLoginId("admin");
lm.setPwd("password");
LoginTemplate loginTemplate = new LoginTemplate();
boolean result = loginTemplate.login(lm, new LoginCallback() {
@Override
public LoginModel findLoginUser(String loginId) {
// 具体实现查找用户逻辑
return new LoginModel();
}
@Override
public String encryptPwd(String pwd, LoginTemplate template) {
// 具体实现密码加密逻辑
return pwd;
}
@Override
public boolean match(LoginModel lm, LoginModel dbLm, LoginTemplate template) {
// 具体实现匹配逻辑
return true;
}
});
System.out.println("登录结果: " + result);
}
}
通过这种方式,我们利用Java回调技术实现了模板方法模式的功能,将具体的实现逻辑通过回调接口传递给模板类,增强了代码的灵活性和可扩展性。
(三)模板方法模式与相关模式的关系
- 与工厂方法模式的配合使用:模板方法模式可以通过工厂方法来获取需要调用的对象。比如在一个游戏开发场景中,游戏角色的创建和初始化有一套固定的流程(模板方法),但不同类型的角色(战士、法师等)创建方式不同。可以使用工厂方法模式来创建不同类型的角色对象,然后在模板方法中调用这些对象进行后续的初始化操作。这样可以将对象创建和使用的逻辑分离,提高代码的可维护性和扩展性。
- 与策略模式的区别与联系:模板方法模式和策略模式在功能上有些相似,都能实现算法的封装。但模板方法模式封装的是算法的骨架,变化的是算法中某些步骤的具体实现;而策略模式是把某个步骤的具体实现算法封装起来,所有封装的算法对象是等价的,可以相互替换。例如在文档中提到的排序功能,整体的排序算法框架(如先转换为数组、排序、再转换回列表)是固定的,这是模板方法模式的体现;而具体的比较大小算法(如按年龄升序、降序等)可以通过不同的
Comparator实现,这是策略模式的体现。可以在模板方法中使用策略模式,把那些变化的算法步骤通过策略模式来实现,而整体的算法步骤由模板方法来定义。
四、模板方法模式在实际场景中的应用——报价管理系统
(一)场景问题
在销售业务中,报价管理是一个复杂的问题。不同类型的客户(如普通客户、老客户、大客户)需要不同的报价策略,同时还需要考虑客户购买的数量、金额以及报价人员的权限等因素。为了演示,假设简化的报价管理功能如下:对普通客户或新客户报全价;对老客户统一折扣5%;对大客户统一折扣10%。
(二)不用模式的解决方案
如果不使用设计模式,可能会通过简单的条件判断来实现报价功能,代码如下:
// 价格管理类
public class Price {
// 报价方法,对不同类型的客户计算不同的价格
public double quote(double goodsPrice, String customerType) {
if ("普通客户".equals(customerType) || "新客户".equals(customerType)) {
return goodsPrice;
} else if ("老客户".equals(customerType)) {
return goodsPrice * 0.95;
} else if ("大客户".equals(customerType)) {
return goodsPrice * 0.9;
}
return goodsPrice;
}
}
这种实现方式虽然简单直接,但存在明显的问题。随着业务的扩展,比如增加新的客户类型或折扣规则,代码会变得越来越复杂,难以维护和扩展。而且,不同客户类型的报价计算逻辑混合在一起,代码的可读性较差。
(三)使用模板方法模式的解决方案
- 定义抽象的报价类,作为模板:
// 抽象报价类
public abstract class AbstractQuote {
// 模板方法,定义报价的基本流程
public final double quote(double goodsPrice, String customerType) {
double discount = getDiscount(customerType);
return calculatePrice(goodsPrice, discount);
}
// 获取折扣,抽象方法,由子类实现
protected abstract double getDiscount(String customerType);
// 计算价格,具体方法,提供通用的计算逻辑
protected double calculatePrice(double goodsPrice, double discount) {
return goodsPrice * (1 - discount);
}
}
- 创建具体的报价子类,继承自抽象报价类,实现抽象方法:
// 普通客户报价子类
public class NormalCustomerQuote extends AbstractQuote {
@Override
protected double getDiscount(String customerType) {
return 0;
}
}
// 老客户报价子类
public class OldCustomerQuote extends AbstractQuote {
@Override
protected double getDiscount(String customerType) {
return 0.05;
}
}
// 大客户报价子类
public class BigCustomerQuote extends AbstractQuote {
@Override
protected double getDiscount(String customerType) {
return 0.1;
}
}
更多推荐


所有评论(0)