策略模式 (Strategy)
同一类问题有多种做法:导出可以是 CSV、Excel 或 PDF,重试可以是固定间隔或指数退避,加密可以是 AES 或 RSA。如果在调用处写死 if (format == CSV) ... else if (format == Excel) ...,每加一种做法就要改这一段,也很难单独测某一种算法。
策略模式的做法是:把每种做法封装成一个「策略」对象,调用方(上下文)只持有一个策略引用,干活时转给当前策略;要换做法就换策略实例,不改上下文逻辑。这样算法和调用方解耦,加新算法主要是加新类,运行时也可以按配置或参数选策略。
一、问题从哪来?
Section titled “一、问题从哪来?”很多地方会遇到「同一类操作,多种实现」:例如数据导出有多种格式、调用外部接口有多种重试方式、日志有多种落盘方式。如果在使用处根据类型写一长串 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)); // 5ctx.setStrategy(new MultiplyStrategy());System.out.println(ctx.doSomething(2, 3)); // 6下面把「策略」换成导出格式(CSV / Excel),上下文换成导出服务,策略以 Bean 形式存在,由调用方按格式参数选择后委托。
五、实战:报表导出格式策略
Section titled “五、实战:报表导出格式策略”后台里经常要「把同一份数据按不同格式导出」:CSV 给分析用、Excel 给业务看、有的还要 PDF。如果导出入口里根据 format 写死分支,每加一种格式就要改同一段代码。用策略模式:每种格式是一个策略,导出服务只持有一个「导出器」接口,按请求的格式选对应策略并委托,导出逻辑都落在各自策略里。
这里用 CSV 和 Excel 两种格式举例;策略接口返回字节数组或写入输出流均可,示例里用「返回 byte[]」简化。
5.1 导出数据与策略接口
Section titled “5.1 导出数据与策略接口”先假定要导出的是一张二维表:表头 + 多行,策略根据格式生成不同内容。
package com.example.demo.strategy;
import lombok.Data;import java.util.List;
@Datapublic 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);}5.2 具体策略:CSV 与 Excel
Section titled “5.2 具体策略:CSV 与 Excel”每个策略只关心一种格式的生成逻辑;CSV 用逗号拼接、Excel 用 Apache POI 等,按项目习惯实现即可。
package com.example.demo.strategy;
import org.springframework.stereotype.Component;import java.nio.charset.StandardCharsets;import java.util.stream.Collectors;
@Componentpublic 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;
@Componentpublic 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@RequiredArgsConstructorpublic 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> strategies,init() 里按 formatName() 建成 Map,之后按参数 format 取策略并调用 export(table)。加一种新格式只需新增一个实现 ExportStrategy 的 Bean,不用改 ExportService。
5.4 在接口里用
Section titled “5.4 在接口里用”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")@RequiredArgsConstructorpublic 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),符合开闭原则。