浅聊策略模式(Strategy Pattern)和模板方法模式(Template Method Pattern)的应用区别


Java策略模式(Strategy Pattern)和模板方法模式(Template Method Pattern)的应用区别。

需求一:上游系统会下发10+个文件,文件名称有固定的前缀和后缀,中间有一部分是按规律可变的。每个文件对应的不同表的数据,每个表的字段个数、字段长度都是独立的。

比如文件xxx.xxx.202509171.001.gz、xxx.xxx.202509171.002.gz

20250917是日期,每天下发文件的日期,1 是标识全量文件-0标识增量文件,目前只需要按照标识为1的来,001表示文件内容标识,表示文件内是table-A的数据。同理,002表示table-B的数据。

实现:

pubic abstract class FileHandleAbstract<T>{
    public abstract String getFileName();
    
    public abstract T trans(String[] arr);

    protected abstract Integer batchInsert(List<T> list);
    
    public void execute() {
        // sftp读取远程服务器文件代码
        
        String filePath = "本地文件保存路径" + getFileName();
        int insertDataCount = 0;
        boolean success = false;
        try (GZIPInputStream gzipInputStream = new GZIPInputStream(new FileInputStream(filePath))) {
            BufferedReader bf = new BufferedReader(new InputStreamReader(gzipInputStream));
            List<String[]> list = new ArrayList<>();
            int insertBatchNum = 0;
            // 逐行读取文件的数据
            String line = "";
            String temp;
            while ((temp = bf.readLine()) != null) {
                if (temp.contains("dip-control")) {
                    break;
                }
                // 由于文件的行末换行符 不是 \n ,需要规范成 \n,所以bf.readLine()方法在读取到换行时,不能作为读取一行结束
                temp = temp + "\n";
                line += temp;
                if (!temp.endsWith("行分隔符")) {
                    continue;
                }
                /**
                 * 对单行数据进行分隔 数据分隔符 \n SOH \n
                 * 由于分隔符从 "\n" 换成了 "\nSOHSOH\n",在解析切割的时候,行末会多余一行
                 * 故需舍弃读出来的一列:通过匹配 SOH SOH \n ,再换掉
                 */
                line = line.replace("行分隔符", "");
                String[] fields = line.split("行分隔符", -1);
                if (fields.length != getTableColumnCount()) {
                    log.error("{} 文件的实际列数和对应表的列数对应不上,实际文件列数:{},table Column:{}", fileName, fields.length, getTableColumnCount());
                    return;
                }

                list.add(fields);
                if (list.size() == 5000) {
                    batchAdd(list);
                }

            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private void batchAdd(List<String[]> list) {
        List<T> insertList = new ArrayList<>();
        for (String[] fields : list) {
            T trans = trans(fields);
            insertList.add(trans);
        }
        batchInsert(insertList);

    }
    
    
}

采取模板方法模式,定义一个抽象父类 FileHandleAbstract,定义一个public的方法execute,主要代码逻辑:

  1. 通过sftp的方式连接远程服务器。
  2. sftp连接上后将文件下载到本机服务器上,避免后续解析文件是网络波动导致文件中断,解析不完整。
  3. sftp下载完成后,断开sftp连接。
  4. 开始解析下载到本地的文件。文件采用流式读取,边读边解析,按照指定的列分隔符和行分隔符将文件内容进行分割,存放到List。循环处理,每读取5000条处理一次。
  5. 对读取到的5000条数据进行转换,映射到具体的列字段上,每个子类对应一个表进行处理。这一步由子类来实现方法public abstract T trans(String[] arr);,子类继承FileHandleAbstract
  6. 字段类型转换完成,对转换后的集合进行批量插入。这一步由子类来实现方法protected abstract Integer batchInsert(List<T> list);,子类继承FileHandleAbstract

需求二:上游系统推送的日间更新数据到kafka,子系统通过订阅kafka的消息获取数据。子系统按照约定好的topic的key订阅获取message,消息头中包含键值对Table-Name:表名。子系统可以根据表名来区分推送的消息。

在处理kafka接收上游数据时,采用 策略模式+简单工厂方法模式,在工厂中根据表名动态创建实现类,在子类中实现具体消息处理逻辑。


这两种行为型设计模式都旨在封装和复用算法,但它们的侧重点和实现机制截然不同。理解它们的区别对于在正确场景下选择正确的模式至关重要。

核心思想总结(快速理解)

特性策略模式 (Strategy)模板方法模式 (Template Method)
核心思想“换芯” - 定义算法家族,使之可相互替换。“定框” - 定义算法骨架,允许子类重写特定步骤。
实现方式对象组合 (Has-A)类继承 (Is-A)
关注点灵活地替换整个算法稳定地复用算法结构
控制权客户端拥有控制权,主动选择策略。父类拥有控制权,子类被动填充细节。
代码复用通过组合不同的策略对象来复用行为。通过继承在父类中复用算法骨架。
灵活性。运行时动态改变算法。相对较低。编译时通过创建子类来改变行为。
开闭原则对扩展开放(新增策略类),对修改关闭(无需改上下文)。对扩展开放(重写钩子方法),对修改关闭(不修改模板方法)。

深入分析

1. 策略模式 (Strategy Pattern)

意图:定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。策略模式使得算法可以独立于使用它的客户而变化。

结构

  • 策略 (Strategy): 一个接口,定义了所有支持算法的公共方法。(例如:PaymentStrategy
  • 具体策略 (ConcreteStrategy): 实现了策略接口的具体算法类。(例如:CreditCardPayment, PayPalPayment
  • 上下文 (Context): 持有一个策略对象的引用,用一个ConcreteStrategy对象来配置。上下文是策略的使用者。(例如:ShoppingCart

Java代码示例

java

// 策略接口
public interface PaymentStrategy {
    void pay(int amount);
}

// 具体策略A
public class CreditCardPayment implements PaymentStrategy {
    private String cardNumber;
    public CreditCardPayment(String cardNumber) { this.cardNumber = cardNumber; }
    @Override
    public void pay(int amount) {
        System.out.println("Paid " + amount + " using Credit Card: " + cardNumber);
    }
}

// 具体策略B
public class PayPalPayment implements PaymentStrategy {
    private String email;
    public PayPalPayment(String email) { this.email = email; }
    @Override
    public void pay(int amount) {
        System.out.println("Paid " + amount + " using PayPal: " + email);
    }
}

// 上下文
public class ShoppingCart {
    private PaymentStrategy paymentStrategy;
    private int totalAmount = 0;

    public void setPaymentStrategy(PaymentStrategy strategy) {
        this.paymentStrategy = strategy;
    }
    public void addItem(int price) { totalAmount += price; }
    public void checkout() {
        paymentStrategy.pay(totalAmount); // 委托给策略对象
        totalAmount = 0;
    }
}

// 客户端使用
public class Client {
    public static void main(String[] args) {
        ShoppingCart cart = new ShoppingCart();
        cart.addItem(100);
        cart.addItem(200);

        // 运行时动态选择策略
        cart.setPaymentStrategy(new CreditCardPayment("1234-5678-9012"));
        cart.checkout();

        // 可以轻松切换策略
        cart.addItem(50);
        cart.setPaymentStrategy(new PayPalPayment("user@example.com"));
        cart.checkout();
    }
}

应用场景

  • 一个系统需要动态地在几种算法中选择一种。
  • 有许多相关的类,仅仅是行为有差异。
  • 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现。(避免了大量的if-else或switch-case)

2. 模板方法模式 (Template Method Pattern)

意图:定义一个操作中的算法骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重新定义该算法的某些特定步骤。

结构

  • 抽象类 (AbstractClass): 定义抽象的原始操作(Primitive Operations)和模板方法。模板方法给出了算法骨架。
  • 具体子类 (ConcreteClass): 实现抽象类中定义的原始操作,以完成算法中与特定子类相关的步骤。

Java代码示例

java

// 抽象类 - 定义算法骨架
public abstract class Game {
    // 模板方法 (通常声明为final,防止子类重写算法结构)
    public final void play() {
        initialize();   // 步骤1
        startPlay();    // 步骤2
        endPlay();      // 步骤3
    }

    // 基本方法 - 已实现(也可声明为Hook方法,空实现)
    protected void initialize() {
        System.out.println("Game Initialized! Default setup.");
    }

    // 抽象方法 - 必须由子类实现
    protected abstract void startPlay();
    protected abstract void endPlay();
}

// 具体子类A
public class Cricket extends Game {
    @Override
    protected void startPlay() {
        System.out.println("Cricket Game Started. Enjoy the match!");
    }
    @Override
    protected void endPlay() {
        System.out.println("Cricket Game Finished!");
    }
}

// 具体子类B
public class Football extends Game {
    @Override
    protected void initialize() { // 可选择重写已有实现的方法
        System.out.println("Football Game Initialized! Players are ready.");
    }
    @Override
    protected void startPlay() {
        System.out.println("Football Game Started. Kick off!");
    }
    @Override
    protected void endPlay() {
        System.out.println("Football Game Finished!");
    }
}

// 客户端使用
public class Client {
    public static void main(String[] args) {
        Game game = new Cricket();
        game.play(); // 调用模板方法,执行定义好的流程

        System.out.println();
        game = new Football();
        game.play();
    }
}

输出:

text

Game Initialized! Default setup.
Cricket Game Started. Enjoy the match!
Cricket Game Finished!

Football Game Initialized! Players are ready.
Football Game Started. Kick off!
Football Game Finished!

应用场景

  • 一次性实现一个算法的不变部分,并将可变的行为留给子类来实现。
  • 各子类中公共的行为应被提取出来并集中到一个公共父类中,以避免代码重复。(代码复用的经典技巧)
  • 控制子类的扩展。模板方法只在特定点调用“钩子”(Hook)方法,这样就只允许在这些点进行扩展(例如:JdbcTemplate)。

关键区别与如何选择

方面策略模式模板方法模式
关系类型组合关系 (Has-A)。上下文拥有一个策略。继承关系 (Is-A)。具体子类一个抽象父类。
运行时行为可以动态改变setStrategy()方法允许在运行时切换整个算法。编译时确定。通过new ConcreteClass()确定行为,行为在对象创建后通常不变。
代码复用通过委托复用策略对象的行为。通过继承在父类中复用算法骨架代码。
算法完整性每个策略完整实现整个算法。父类和子类协同工作,共同完成一个完整的算法。
设计原则优先使用组合而非继承。更符合开闭原则(新增策略无需修改上下文)。合理使用继承,将不变部分封装起来,扩展可变部分。

如何选择?

  • 当你需要完全替换算法或行为时,使用策略模式。
    • 场景:支付方式(信用卡、支付宝、微信支付)、排序算法(快速排序、归并排序)、导航策略(驾车、步行、公交)。客户端需要主动做出选择。
  • 当你需要保持算法结构稳定,但允许其中某些步骤可变化或扩展时,使用模板方法模式。
    • 场景JdbcTemplate中的execute()方法定义了流程(获取连接、执行语句、处理异常、释放连接),你只需要实现PreparedStatementCallback这个“步骤”。框架生命周期(初始化、启动、销毁)、数据解析流程(打开文件、读取头、解析数据、关闭文件)。

总结一句话:策略模式让你换掉整个“发动机”,而模板方法模式让你只换掉发动机里的“几个活塞”。

SpringBoot
JAVA-技能点
知识点
设计模式