模板模式:设计与实践

2 阅读14分钟

模板模式:设计与实践

一、什么是模板模式

1. 基本定义

模板模式(Template Pattern)是一种行为型设计模式,由《设计模式:可复用面向对象软件的基础》(GOF著作)定义为:定义一个操作中的算法骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤

该模式通过在抽象类中定义一个算法的骨架(模板方法),将算法中不变的部分(固定步骤)在抽象类中实现,而将易变的部分(可变步骤)声明为抽象方法,由子类根据自身需求实现。核心是实现“算法骨架与具体实现的分离”,确保算法结构稳定的同时,允许子类灵活定制部分步骤。

2. 核心思想

模板模式的核心在于骨架固定与细节延迟。当系统中存在多个算法,这些算法的整体流程一致但部分步骤存在差异时,通过提取公共流程作为模板方法,将差异化步骤抽象为方法,可避免重复编码,同时保证算法结构的一致性。这种设计既体现了“复用”的原则(公共流程只实现一次),又遵循了“开闭”原则(新增算法只需扩展子类,无需修改模板)。

二、模板模式的特点

1. 算法骨架固定

抽象类中的模板方法定义了算法的整体流程,子类无法修改流程结构,只能定制具体步骤。

2. 步骤分类明确

算法步骤分为固定步骤(在抽象类中实现)和可变步骤(由抽象方法声明,子类实现)。

3. 代码复用率高

公共流程在抽象类中统一实现,避免多个子类重复编码,减少冗余。

4. 扩展性强

新增算法只需实现子类,重写可变步骤,无需修改模板类和其他子类,符合开闭原则。

5. 反向控制

父类调用子类的方法(好莱坞原则:“不要调用我们,我们会调用你”),子类被动适应父类的流程。

特点说明
算法骨架固定模板方法定义流程结构,子类无法修改
步骤分类明确分为固定步骤(抽象类实现)和可变步骤(子类实现)
代码复用率高公共流程统一实现,减少重复代码
扩展性强新增算法只需扩展子类,无需修改模板
反向控制父类调用子类方法,子类适应父类流程

三、模板模式的标准代码实现

1. 模式结构

模板模式包含两个核心角色:

  • 抽象类(AbstractClass):定义算法的骨架,包含一个模板方法(templateMethod)和若干个基本方法(固定步骤和抽象方法)。
  • 具体子类(ConcreteClass):继承抽象类,实现抽象类中的抽象方法,完成算法中与自身相关的可变步骤。

2. 代码实现示例

2.1 抽象类(定义模板)
/**
 * 抽象类:定义算法骨架
 */
public abstract class AbstractClass {

    /**
     * 模板方法:算法的骨架
     * 声明为final,防止子类修改流程
     */
    public final void templateMethod() {
        // 步骤1:固定步骤A
        stepA();

        // 步骤2:可变步骤B(子类实现)
        stepB();

        // 步骤3:固定步骤C
        stepC();

        // 步骤4:可变步骤D(子类实现)
        stepD();

        // 步骤5:固定步骤E
        stepE();
    }

    /**
     * 固定步骤A:在抽象类中实现
     */
    private void stepA() {
        System.out.println("执行固定步骤A");
    }

    /**
     * 固定步骤C:在抽象类中实现
     */
    private void stepC() {
        System.out.println("执行固定步骤C");
    }

    /**
     * 固定步骤E:在抽象类中实现
     */
    private void stepE() {
        System.out.println("执行固定步骤E");
    }

    /**
     * 可变步骤B:抽象方法,子类实现
     */
    protected abstract void stepB();

    /**
     * 可变步骤D:抽象方法,子类实现
     */
    protected abstract void stepD();
}
2.2 具体子类(实现可变步骤)
/**
 * 具体子类1:实现算法A
 */
public class ConcreteClassA extends AbstractClass {

    @Override
    protected void stepB() {
        System.out.println("算法A:执行可变步骤B(方式一)");
    }

    @Override
    protected void stepD() {
        System.out.println("算法A:执行可变步骤D(方式一)");
    }
}

/**
 * 具体子类2:实现算法B
 */
public class ConcreteClassB extends AbstractClass {

    @Override
    protected void stepB() {
        System.out.println("算法B:执行可变步骤B(方式二)");
    }

    @Override
    protected void stepD() {
        System.out.println("算法B:执行可变步骤D(方式二)");
    }
}
2.3 客户端使用示例
/**
 * 客户端:使用模板模式
 */
public class Client {
    public static void main(String[] args) {
        // 执行算法A
        AbstractClass algorithmA = new ConcreteClassA();
        System.out.println("=== 执行算法A ===");
        algorithmA.templateMethod();

        // 执行算法B
        AbstractClass algorithmB = new ConcreteClassB();
        System.out.println("\n=== 执行算法B ===");
        algorithmB.templateMethod();
    }
}

3. 代码实现特点总结

角色核心职责代码特点
抽象类(AbstractClass)定义算法骨架,实现固定步骤,声明可变步骤包含final修饰的模板方法(templateMethod),固定步骤为普通方法,可变步骤为抽象方法
具体子类(ConcreteClass)实现抽象类中的抽象方法,定制可变步骤继承抽象类,重写所有抽象方法,不修改模板方法和固定步骤

四、支付框架设计中模板模式的运用

模板模式在数据加密加签中的实现为例,说明模板模式在支付系统中的具体应用:

1. 场景分析

支付系统中,数据的加密与加签是保障交易安全的核心环节,不同商户或渠道可能要求不同的加密算法(如AES、RSA)和加签算法(如SHA256、MD5),但整体流程存在固定规律:

  • 数据准备:对原始数据进行格式化(去除空值、排序),确保输入统一
  • 加密处理:对数据进行加密(算法可变)
  • 加签处理:对加密后的数据进行加签(算法可变)
  • 结果组合:将加密数据、签名、算法标识等组合为最终输出格式

使用模板模式可将固定流程(数据准备、结果组合)在抽象类中实现,将加密和加签的具体逻辑延迟到子类,实现“流程标准化+算法可定制”的设计目标。

2. 设计实现

2.1 抽象模板类
import java.util.Map;
import java.util.TreeMap;

/**
 * 加密加签模板抽象类
 * 定义固定流程,抽象可变步骤
 */
public abstract class AbstractEncryptSignTemplate {

    /**
     * 模板方法:加密加签完整流程
     * 声明为final,确保流程不可修改
     */
    public final DataPackage process(Map<String, String> rawData, String secret, String signKey) {
        // 步骤1:数据准备(固定步骤)
        Map<String, String> preparedData = prepareData(rawData);

        // 步骤2:加密处理(可变步骤,子类实现)
        String encryptedData = encrypt(preparedData, secret);

        // 步骤3:加签处理(可变步骤,子类实现)
        String signature = sign(encryptedData, signKey);

        // 步骤4:结果组合(固定步骤)
        return combineResult(preparedData, encryptedData, signature);
    }

    /**
     * 固定步骤:数据准备
     * 统一格式化原始数据(去空、排序)
     */
    private Map<String, String> prepareData(Map<String, String> rawData) {
        // 移除空值
        rawData.entrySet().removeIf(entry -> entry.getValue() == null || entry.getValue().isEmpty());
        // 按key排序(确保加签顺序一致)
        return new TreeMap<>(rawData);
    }

    /**
     * 可变步骤:加密处理
     * 由子类实现具体加密算法
     */
    protected abstract String encrypt(Map<String, String> data, String secret);

    /**
     * 可变步骤:加签处理
     * 由子类实现具体加签算法
     */
    protected abstract String sign(String encryptedData, String signKey);

    /**
     * 固定步骤:结果组合
     * 封装原始数据、加密数据、签名及算法标识
     */
    private DataPackage combineResult(Map<String, String> rawData, String encryptedData, String signature) {
        DataPackage result = new DataPackage();
        result.setRawData(rawData);
        result.setEncryptedData(encryptedData);
        result.setSignature(signature);
        result.setEncryptAlgorithm(getEncryptAlgorithm());
        result.setSignAlgorithm(getSignAlgorithm());
        return result;
    }

    // 获取算法标识(子类实现)
    protected abstract String getEncryptAlgorithm();
    protected abstract String getSignAlgorithm();
}
2.2 数据模型定义
import java.util.Map;

/**
 * 加密加签数据包
 * 封装加密加签过程中的数据载体
 */
public class DataPackage {
    private Map<String, String> rawData; // 原始数据
    private String encryptedData; // 加密后数据
    private String signature; // 签名
    private String encryptAlgorithm; // 加密算法
    private String signAlgorithm; // 加签算法

    // getter和setter方法...
}
2.3 具体算法实现
2.3.1 AES加密+SHA256加签实现
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;
import java.util.Map;

/**
 * AES加密+SHA256加签实现
 */
public class AesSha256Template extends AbstractEncryptSignTemplate {

    @Override
    protected String encrypt(Map<String, String> data, String secret) {
        try {
            // AES加密实现(固定密钥长度16位)
            SecretKeySpec keySpec = new SecretKeySpec(secret.getBytes(), "AES");
            Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
            cipher.init(Cipher.ENCRYPT_MODE, keySpec);

            // 将排序后的map转为字符串后加密
            String dataStr = mapToString(data);
            byte[] encrypted = cipher.doFinal(dataStr.getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(encrypted);
        } catch (Exception e) {
            throw new RuntimeException("AES加密失败", e);
        }
    }

    @Override
    protected String sign(String encryptedData, String signKey) {
        try {
            // SHA256加签(加密数据+密钥组合后哈希)
            String signStr = encryptedData + "&key=" + signKey;
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] hash = digest.digest(signStr.getBytes(StandardCharsets.UTF_8));
            return bytesToHex(hash);
        } catch (Exception e) {
            throw new RuntimeException("SHA256加签失败", e);
        }
    }

    @Override
    protected String getEncryptAlgorithm() {
        return "AES";
    }

    @Override
    protected String getSignAlgorithm() {
        return "SHA256";
    }

    // 辅助方法:map转字符串(key=value&key=value)
    private String mapToString(Map<String, String> data) {
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, String> entry : data.entrySet()) {
            if (sb.length() > 0) sb.append("&");
            sb.append(entry.getKey()).append("=").append(entry.getValue());
        }
        return sb.toString();
    }

    // 辅助方法:字节数组转十六进制
    private String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }
}
2.3.2 RSA加密+MD5加签实现
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Map;

/**
 * RSA加密+MD5加签实现
 */
public class RsaMd5Template extends AbstractEncryptSignTemplate {

    @Override
    protected String encrypt(Map<String, String> data, String publicKey) {
        try {
            // RSA公钥加密
            PublicKey pubKey = KeyFactory.getInstance("RSA")
                .generatePublic(new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey)));

            String dataStr = mapToString(data);
            javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance("RSA");
            cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, pubKey);

            byte[] encrypted = cipher.doFinal(dataStr.getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(encrypted);
        } catch (Exception e) {
            throw new RuntimeException("RSA加密失败", e);
        }
    }

    @Override
    protected String sign(String encryptedData, String privateKey) {
        try {
            // MD5加签(使用私钥)
            PrivateKey priKey = KeyFactory.getInstance("RSA")
                .generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)));

            java.security.Signature signature = java.security.Signature.getInstance("MD5withRSA");
            signature.initSign(priKey);
            signature.update(encryptedData.getBytes(StandardCharsets.UTF_8));

            byte[] signBytes = signature.sign();
            return Base64.getEncoder().encodeToString(signBytes);
        } catch (Exception e) {
            throw new RuntimeException("MD5加签失败", e);
        }
    }

    @Override
    protected String getEncryptAlgorithm() {
        return "RSA";
    }

    @Override
    protected String getSignAlgorithm() {
        return "MD5withRSA";
    }

    // 复用AES实现中的mapToString方法
    private String mapToString(Map<String, String> data) {
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, String> entry : data.entrySet()) {
            if (sb.length() > 0) sb.append("&");
            sb.append(entry.getKey()).append("=").append(entry.getValue());
        }
        return sb.toString();
    }
}
2.4 客户端使用示例
import java.util.HashMap;
import java.util.Map;

/**
 * 加密加签服务(客户端)
 */
public class EncryptSignService {

    // 根据商户配置获取对应的模板
    public AbstractEncryptSignTemplate getTemplate(String merchantId) {
        // 模拟从配置中心获取商户算法配置
        MerchantConfig merchantConfig = new MerchantConfig();
        String encryptAlg = merchantConfig.getEncryptAlgorithm(merchantId);
        String signAlg = merchantConfig.getSignAlgorithm(merchantId);

        // 根据算法选择模板
        if ("AES".equals(encryptAlg) && "SHA256".equals(signAlg)) {
            return new AesSha256Template();
        } else if ("RSA".equals(encryptAlg) && "MD5withRSA".equals(signAlg)) {
            return new RsaMd5Template();
        } else {
            throw new IllegalArgumentException("不支持的算法组合");
        }
    }

    public static void main(String[] args) {
        EncryptSignService service = new EncryptSignService();
        Map<String, String> data = new HashMap<>();
        data.put("orderId", "PAY123456");
        data.put("amount", "100.00");
        data.put("timestamp", "1620000000000");

        // 商户A使用AES+SHA256
        AbstractEncryptSignTemplate aesTemplate = service.getTemplate("MERCHANT_A");
        DataPackage aesResult = aesTemplate.process(data, "aesKey123456", "signKey789");
        System.out.println("AES+SHA256处理结果:" + aesResult.getEncryptedData() + ",签名:" + aesResult.getSignature());

        // 商户B使用RSA+MD5
        AbstractEncryptSignTemplate rsaTemplate = service.getTemplate("MERCHANT_B");
        DataPackage rsaResult = rsaTemplate.process(data, 
            "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQ...", // RSA公钥
            "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJc..."); // RSA私钥
        System.out.println("RSA+MD5处理结果:" + rsaResult.getEncryptedData() + ",签名:" + rsaResult.getSignature());
    }
}

// 模拟商户配置类
class MerchantConfig {
    public String getEncryptAlgorithm(String merchantId) {
        // 实际中从数据库或配置中心获取
        return "MERCHANT_A".equals(merchantId) ? "AES" : "RSA";
    }
    public String getSignAlgorithm(String merchantId) {
        return "MERCHANT_A".equals(merchantId) ? "SHA256" : "MD5withRSA";
    }
}

3. 模式价值体现

  • 流程标准化:固定步骤(数据准备、结果组合)在抽象类中统一实现,确保所有加密加签流程遵循相同规范(如数据必须排序后加密,避免因顺序问题导致验签失败)
  • 算法灵活扩展:新增算法组合(如SM4加密+SM3加签)只需添加新的模板子类,无需修改现有流程代码,符合开闭原则
  • 职责清晰:抽象类专注于流程骨架设计,子类专注于具体算法实现,符合单一职责原则,便于单独测试和维护
  • 支付场景适配性:满足不同渠道的安全要求(如银联要求RSA加密,微信支持AES加密),通过模板切换快速适配,降低接入成本
  • 非侵入式增强:固定流程中可嵌入日志、监控等通用逻辑(如记录加密耗时、加签成功率),所有子类自动受益

五、开源框架中模板模式的运用

Spring JDBC的JdbcTemplate为例,说明模板模式在开源框架中的典型应用:

1. 核心实现分析

Spring JDBC是Spring框架中用于简化数据库操作的模块,其JdbcTemplate类基于模板模式,将数据库操作的固定流程(如获取连接、创建语句、处理异常、释放资源)封装为模板方法,将可变步骤(如SQL执行、结果映射)交由用户通过回调接口实现。

1.1 抽象模板(JdbcTemplate)

JdbcTemplate中定义了数据库操作的模板方法(如queryupdate),实现了固定步骤:

public class JdbcTemplate {
    // 模板方法:查询操作
    public <T> T query(String sql, RowMapper<T> rowMapper, Object... args) {
        // 固定步骤1:获取数据库连接
        Connection conn = getConnection();
        PreparedStatement stmt = null;
        ResultSet rs = null;
        try {
            // 固定步骤2:创建PreparedStatement
            stmt = conn.prepareStatement(sql);
            // 固定步骤3:设置参数
            setParameters(stmt, args);
            // 固定步骤4:执行查询
            rs = stmt.executeQuery();
            // 可变步骤:映射结果(由rowMapper实现)
            return rowMapper.mapRow(rs, 0);
        } catch (SQLException e) {
            // 固定步骤5:处理异常
            handleException(e);
        } finally {
            // 固定步骤6:释放资源
            closeResources(rs, stmt, conn);
        }
        return null;
    }

    // 其他固定步骤实现(获取连接、设置参数、释放资源等)
    private Connection getConnection() { ... }
    private void setParameters(PreparedStatement stmt, Object[] args) { ... }
    private void closeResources(ResultSet rs, PreparedStatement stmt, Connection conn) { ... }
}
1.2 可变步骤(回调接口)

RowMapper作为回调接口,扮演抽象方法的角色,由用户实现结果映射逻辑:

// 回调接口(类似抽象方法)
public interface RowMapper<T> {
    T mapRow(ResultSet rs, int rowNum) throws SQLException;
}

// 用户实现(类似具体子类)
public class UserRowMapper implements RowMapper<User> {
    @Override
    public User mapRow(ResultSet rs, int rowNum) throws SQLException {
        User user = new User();
        user.setId(rs.getLong("id"));
        user.setName(rs.getString("name"));
        return user;
    }
}
1.3 客户端使用

用户通过实现RowMapper接口,专注于结果映射,无需关心连接管理等固定步骤:

public class UserDao {
    private JdbcTemplate jdbcTemplate;

    public User getUserById(Long id) {
        String sql = "SELECT id, name FROM user WHERE id = ?";
        // 调用模板方法,传入回调接口实现
        return jdbcTemplate.query(sql, new UserRowMapper(), id);
    }
}

2. 模板模式在Spring JDBC中的价值

  • 简化开发:用户无需编写获取连接、释放资源等重复代码,专注于SQL和结果映射
  • 资源管理自动化:模板方法确保连接、语句等资源被正确释放,避免资源泄漏
  • 异常统一处理:模板方法统一处理SQL异常,转换为Spring的异常体系,简化异常处理
  • 扩展性强:支持多种数据库操作(查询、更新、批量操作),通过不同回调接口实现差异化逻辑

六、总结

1. 模板模式的适用场景

  • 当多个算法的整体流程一致,但部分步骤存在差异时
  • 当需要避免重复编码,提取公共流程作为模板时
  • 当需要确保算法结构稳定,不允许子类修改流程时
  • 当需要控制子类的扩展,只允许重写特定步骤时

2. 模板模式与其他模式的区别

  • 与策略模式:两者都用于封装变化,但策略模式封装的是完整算法,模板模式封装的是算法的部分步骤,前者是“算法整体替换”,后者是“算法步骤定制”
  • 与工厂模式:工厂模式专注于对象创建,模板模式专注于算法流程,前者是创建型模式,后者是行为型模式
  • 与建造者模式:两者都涉及步骤的组合,但建造者模式强调产品的构建过程和部件组装,模板模式强调算法流程的固定与步骤定制

3. 支付系统中的实践价值

  • 标准化流程:确保核心流程(如支付、退款、对账)的一致性,减少人为错误
  • 降低维护成本:公共逻辑集中维护,修改一处即可影响所有子类,减少冗余代码
  • 加速业务迭代:新增业务场景时,只需实现差异化步骤,复用成熟模板,缩短开发周期
  • 增强安全性:关键步骤(如加密、签名)在模板中强制执行,避免子类遗漏,降低安全风险
  • 团队协作高效:框架开发者负责模板设计,业务开发者专注于业务逻辑实现,分工明确

4. 实践建议

  • 合理划分步骤:明确固定步骤和可变步骤,避免将过多逻辑放入模板或子类
  • 模板方法设为final:防止子类修改算法骨架,确保流程稳定性
  • 使用钩子方法:在模板中添加可选的钩子方法(空实现的方法),允许子类选择性扩展,增强灵活性
  • 避免模板膨胀:模板类不应包含过多功能,可通过组合其他类实现复杂逻辑
  • 文档化模板流程:清晰说明模板方法的执行步骤和各步骤的作用,便于子类实现

模板模式通过“骨架固定与细节延迟”的思想,有效解决了支付系统中流程标准化与个性化需求的矛盾,既保证了核心流程的稳定性和安全性,又为业务扩展提供了灵活的支撑,是支付框架设计中实现“复用与扩展”平衡的重要手段。