跳转到内容

策略模式 (Strategy)

同一类问题有多种做法:导出可以是 CSV、Excel 或 PDF,重试可以是固定间隔或指数退避,加密可以是 AES 或 RSA。如果在调用处写死 if (format == CSV) ... else if (format == Excel) ...,每加一种做法就要改这一段,也很难单独测某一种算法。

策略模式的做法是:把每种做法封装成一个「策略」对象,调用方(上下文)只持有一个策略引用,干活时转给当前策略;要换做法就换策略实例,不改上下文逻辑。这样算法和调用方解耦,加新算法主要是加新类,运行时也可以按配置或参数选策略。


很多地方会遇到「同一类操作,多种实现」:例如数据导出有多种格式、调用外部接口有多种重试方式、日志有多种落盘方式。如果在使用处根据类型写一长串 if-else 或 switch,会有几个问题:使用处越来越臃肿;加一种实现就要改同一块代码;某一种实现也不好单独做单元测试。

更稳妥的做法是:把每种实现抽成一个策略类,实现同一个策略接口;使用处只依赖接口,持有一个策略引用(或通过参数传入),干活时调用策略的方法。选哪种策略由上层在构造或运行时通过 setStrategy 注入,使用处不写分支。这样「做什么」和「怎么做」分开,符合开闭原则,也方便用不同策略做测试或 A/B。


二、策略接口、具体策略与上下文

Section titled “二、策略接口、具体策略与上下文”
  • 策略接口 (Strategy):定义算法的统一入口,例如 execute(Input in)export(Data data)。调用方只依赖这个接口。
  • 具体策略 (ConcreteStrategy):实现接口,每种策略对应一种算法或一种实现。策略内部可以无状态,也可以持有配置(如分隔符、超时时间)。
  • 上下文 (Context):使用策略的一方,持有一个策略引用;对外提供业务方法(如 doExport()),内部把请求转给当前策略。可以提供 setStrategy(Strategy s) 在运行时切换策略,也可以在每次调用时按参数选择策略再委托。

客户端(或上层服务)负责「选哪个策略」并注入或传给上下文;上下文不关心具体是哪种实现,只负责转发。


上下文持有策略引用,业务方法委托给策略;策略可替换。

classDiagram
    class Context {
        -strategy Strategy
        +setStrategy(Strategy s) void
        +doSomething() void
    }
    class Strategy {
        <<interface>>
        +execute()* void
    }
    class ConcreteStrategyA {
        +execute() void
    }
    class ConcreteStrategyB {
        +execute() void
    }
    Context o-- Strategy
    Strategy <|.. ConcreteStrategyA
    Strategy <|.. ConcreteStrategyB

不依赖框架,用接口和两个策略把「上下文委托给当前策略」跑通。

public interface Strategy {
int execute(int a, int b);
}
public class AddStrategy implements Strategy {
@Override
public int execute(int a, int b) { return a + b; }
}
public class MultiplyStrategy implements Strategy {
@Override
public int execute(int a, int b) { return a * b; }
}
public class Context {
private Strategy strategy;
public void setStrategy(Strategy strategy) {
this.strategy = strategy;
}
public int doSomething(int a, int b) {
return strategy.execute(a, b);
}
}
Context ctx = new Context();
ctx.setStrategy(new AddStrategy());
System.out.println(ctx.doSomething(2, 3)); // 5
ctx.setStrategy(new MultiplyStrategy());
System.out.println(ctx.doSomething(2, 3)); // 6

下面把「策略」换成导出格式(CSV / Excel),上下文换成导出服务,策略以 Bean 形式存在,由调用方按格式参数选择后委托。


后台里经常要「把同一份数据按不同格式导出」:CSV 给分析用、Excel 给业务看、有的还要 PDF。如果导出入口里根据 format 写死分支,每加一种格式就要改同一段代码。用策略模式:每种格式是一个策略,导出服务只持有一个「导出器」接口,按请求的格式选对应策略并委托,导出逻辑都落在各自策略里。

这里用 CSV 和 Excel 两种格式举例;策略接口返回字节数组或写入输出流均可,示例里用「返回 byte[]」简化。

先假定要导出的是一张二维表:表头 + 多行,策略根据格式生成不同内容。

package com.example.demo.strategy;
import lombok.Data;
import java.util.List;
@Data
public class ExportTable {
private List<String> headers;
private List<List<String>> rows;
}
package com.example.demo.strategy;
public interface ExportStrategy {
String formatName(); // 如 "csv", "xlsx"
byte[] export(ExportTable table);
}

每个策略只关心一种格式的生成逻辑;CSV 用逗号拼接、Excel 用 Apache POI 等,按项目习惯实现即可。

package com.example.demo.strategy;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.util.stream.Collectors;
@Component
public class CsvExportStrategy implements ExportStrategy {
@Override
public String formatName() { return "csv"; }
@Override
public byte[] export(ExportTable table) {
StringBuilder sb = new StringBuilder();
sb.append(String.join(",", table.getHeaders())).append("\n");
for (List<String> row : table.getRows()) {
sb.append(row.stream().map(s -> "\"" + s.replace("\"", "\"\"") + "\"")
.collect(Collectors.joining(","))).append("\n");
}
return sb.toString().getBytes(StandardCharsets.UTF_8);
}
}
package com.example.demo.strategy;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Component;
import java.io.ByteArrayOutputStream;
import java.util.List;
@Component
public class ExcelExportStrategy implements ExportStrategy {
@Override
public String formatName() { return "xlsx"; }
@Override
public byte[] export(ExportTable table) {
try (Workbook wb = new XSSFWorkbook(); ByteArrayOutputStream out = new ByteArrayOutputStream()) {
Sheet sheet = wb.createSheet("data");
Row headerRow = sheet.createRow(0);
int col = 0;
for (String h : table.getHeaders()) {
headerRow.createCell(col++).setCellValue(h);
}
int rowNum = 1;
for (List<String> row : table.getRows()) {
Row r = sheet.createRow(rowNum++);
for (int i = 0; i < row.size(); i++) {
r.createCell(i).setCellValue(row.get(i));
}
}
wb.write(out);
return out.toByteArray();
} catch (Exception e) {
throw new RuntimeException("export excel failed", e);
}
}
}

5.3 上下文:导出服务按格式选策略

Section titled “5.3 上下文:导出服务按格式选策略”

导出服务持有一组策略(例如 Map<formatName, Strategy>),根据请求的格式取策略并委托;不写 if-else 分支。

package com.example.demo.strategy;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class ExportService {
private final List<ExportStrategy> strategies;
private Map<String, ExportStrategy> strategyMap;
@javax.annotation.PostConstruct
public void init() {
strategyMap = strategies.stream()
.collect(Collectors.toMap(ExportStrategy::formatName, Function.identity()));
}
public byte[] export(ExportTable table, String format) {
ExportStrategy strategy = strategyMap.get(format);
if (strategy == null) {
throw new IllegalArgumentException("unsupported format: " + format);
}
return strategy.export(table);
}
}

容器会把所有 ExportStrategy 的实现 Bean 注入到 List<ExportStrategy> strategiesinit() 里按 formatName() 建成 Map,之后按参数 format 取策略并调用 export(table)。加一种新格式只需新增一个实现 ExportStrategy 的 Bean,不用改 ExportService。

Controller 或别的服务准备好 ExportTable(从查询结果组装),再按前端或参数传入的格式调导出服务。

package com.example.demo.strategy;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/report")
@RequiredArgsConstructor
public class ReportController {
private final ExportService exportService;
@GetMapping("/export")
public ResponseEntity<byte[]> export(
@RequestParam String format,
@RequestParam(required = false) String reportId) {
ExportTable table = loadReportData(reportId); // 从库或服务取数
byte[] body = exportService.export(table, format);
String contentType = "csv".equalsIgnoreCase(format)
? "text/csv" : "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=report." + format)
.contentType(MediaType.parseMediaType(contentType))
.body(body);
}
private ExportTable loadReportData(String reportId) {
// 实际从 repository 或 service 加载
ExportTable t = new ExportTable();
t.setHeaders(List.of("A", "B", "C"));
t.setRows(List.of(List.of("1", "2", "3")));
return t;
}
}

调用方只关心「要什么格式、拿到 byte[]」,选哪种策略在 ExportService 内部按 format 完成,符合策略模式里「上下文委托、算法可替换」的用法。


策略模式把一组可互换的算法封装成独立策略类,上下文只持有一个策略引用(或按参数选策略)并委托,使算法与调用方解耦,便于扩展新算法和单独测试。上面导出示例里,每种格式是一个实现 ExportStrategy 的 Bean,导出服务通过容器注入的列表建 Map 再按格式选择策略,无需在业务代码里写格式分支;若要支持 PDF,只需新增一个策略 Bean 并实现 export(ExportTable),符合开闭原则。