策略模式
1.定义
策略模式是一种行为型设计模式,核心思想是:
定义一系列算法,把他们一个个封装起来,并且使他们可以互相替换
用大白话说就是:
同一个功能有很多不同的做法,把每种做法都单独写成一个类,用的时候按需跳一个来执行
2.从if-else到策略模式
比如我们有一个导出功能,支持md/json/Excel三种不同的导出
我们的controller,可能就得这样写
java
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/export")
public class ExportController {
@GetMapping
public ResponseEntity<byte[]> export(String type){
byte[] bytes = new byte[0];
String fileName = "export";
if(type.equals("excel")){
// bytes = excel的导出
fileName += ".xlsx";
}else if(type.equals("json")){
// bytes = json的导出
fileName += ".json";
}else if(type.equals("md")){
// bytes = md的导出
fileName += ".md";
}else{
throw new IllegalArgumentException("不支持的导出类型:"+type);
}
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=\"" + fileName +"\"")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(bytes);
}
}
这样写,有啥问题?
想一想,如果我们需要新增一个导出的时候,比如pdf导出,是不是得修改这个controller,这就违反了开闭原则
开闭原则:对扩展开放,对修改关闭
用人话解释:
当需求变化,要加新功能时,尽量通过"新增代码"来实现,而不是去改已有得代码
所以我们得修改,抽象出一个接口,让其他不同格式的导出类去继承这个接口。
java
import com.hao.strategy.domain.Order;
import java.util.List;
/**
* 导出业务类,所有支持导出的类,必须继承这个接口
*/
public interface ExportService {
byte[] export(List<Order> orderList);
}
//这个Order类是为了演示方便
import lombok.Data;
import java.math.BigDecimal;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order {
private String id;
private String productName;
private BigDecimal totalAmount;
}
然后我们去让不同的导出类去实现他
java
/**
* Json导出类
*/
@Service
@RequiredArgsConstructor
public class JsonExport implements ExportService {
private final ObjectMapper objectMapper;
@Override
public byte[] export(List<Order> orderList) {
try {
return objectMapper.writerWithDefaultPrettyPrinter()
.writeValueAsBytes(orderList);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
}
java
/**
* Md导出类
*/
@Service
public class MdExport implements ExportService {
@Override
public byte[] export(List<Order> orderList) {
StringBuilder builder = new StringBuilder();
builder.append("# 订单列表\n\n");
builder.append("| 订单ID | 商品名称 | 总价 |\n");
builder.append("| --- | --- | --- |\n");
for (Order order : orderList) {
builder.append("| ")
.append(order.getId()).append(" | ")
.append(order.getProductName()).append(" | ")
.append(order.getTotalAmount()).append(" |\n");
}
return builder.toString().getBytes(StandardCharsets.UTF_8);
}
}
java
import com.hao.strategy.domain.Order;
import com.hao.strategy.service.ExportService;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
/**
* Excel导出类
*/
@Service
public class ExcelExport implements ExportService {
@Override
public byte[] export(List<Order> orderList) {
try (//XSSFWorkbook 用来生成.xlsx文件
XSSFWorkbook workbook = new XSSFWorkbook();
ByteArrayOutputStream out = new ByteArrayOutputStream();){
//创建一个工作表
XSSFSheet sheet = workbook.createSheet("订单列表");
//创建表头
XSSFRow header = sheet.createRow(0);
header.createCell(0).setCellValue("订单ID");
header.createCell(1).setCellValue("商品名称");
header.createCell(2).setCellValue("订单金额");
//填充数据
for (int i = 0; i < orderList.size(); i++) {
Order order = orderList.get(i);
XSSFRow row = sheet.createRow(i + 1);
row.createCell(0).setCellValue(order.getId());
row.createCell(1).setCellValue(order.getProductName());
row.createCell(2).setCellValue(
order.getTotalAmount() != null ? order.getTotalAmount().doubleValue() : 0.0
);
}
workbook.write(out);
return out.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
//需要引入的依赖
// <!-- Source: https://mvnrepository.com/artifact/org.apache.poi/poi-ooxml -->
// <dependency>
// <groupId>org.apache.poi</groupId>
// <artifactId>poi-ooxml</artifactId>
// <version>5.5.1</version>
// <scope>compile</scope>
// </dependency>
然后我们就需要修改我们的原有controller,现在最重要的是如何根据用户传进来的type来选择对应的service,我们可以用枚举和Java的注解来实现。详情如下:
java
import lombok.Getter;
import java.util.function.Predicate;
/**
* 支持导出的枚举类型
*/
@Getter
public enum ExportType {
MD("md", value -> value.equals("md")),
JSON("json",value -> value.equals("json")),
EXCEL("xlsx",value -> value.equals("xlsx"));
private final String type;
private final Predicate<String> predicate;
ExportType(String type,Predicate<String> predicate){
this.type = type;
this.predicate = predicate;
}
public static ExportType getTypeByName(String typeName){
for (ExportType value : values()) {
if(value.predicate.test(typeName)) return value;
}
return null; //如果没有匹配到返回空,比如word导出直接返回空
}
}
java
/**
* 标记在 impl service层上,用于表明该实现类,支持哪种类型的导出
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface EnableExportType {
ExportType value();
}
java
/**
* Json导出类
*/
@RequiredArgsConstructor
@Service
@EnableExportType(ExportType.JSON) //加上注解,表明该类支持哪种类型的导出
public class JsonExport implements ExportService {
......
}
/**
* Excel导出类
*/
@Service
@EnableExportType(ExportType.EXCEL) //加上注解,表明该类支持哪种类型的导出
public class ExcelExport implements ExportService {
......
}
/**
* Md导出类
*/
@Service
@EnableExportType(ExportType.MD)
public class MdExport implements ExportService {
......
}
完事具备,现在开始修改controller
java
import com.hao.strategy.annotation.EnableExportType;
import com.hao.strategy.domain.Order;
import com.hao.strategy.enums.ExportType;
import com.hao.strategy.service.ExportService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.lang.NonNull;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("/export")
public class ExportController {
@Autowired
private List<ExportService> exportServiceList; //拿到所有实现了ExportService的实现类
@GetMapping
public ResponseEntity<byte[]> export(String type){
//拿到所有的模拟数据
List<Order> orderList = getOrders();
ExportType typeByName = ExportType.getTypeByName(type);
if(typeByName == null){
throw new IllegalArgumentException("不支持的导出类型:"+type);
}
List<ExportService> serviceList = exportServiceList.stream()
.filter(exportService -> exportService.getClass().isAnnotationPresent(EnableExportType.class))
.toList();
if(serviceList.size() != ExportType.values().length){
throw new IllegalArgumentException("导出实现类数量与 ExportType 枚举类数量不匹配"); //实现类的数量不等于支持枚举的数量,说明实现类上每加对应的注解,或者没更新枚举类
}
byte[] bytes = new byte[0];
String fileName = "export.";
for (ExportService exportService : serviceList) {
EnableExportType annotation = exportService.getClass().getAnnotation(EnableExportType.class);
if (annotation.value().equals(typeByName)) {
bytes = exportService.export(orderList);
fileName += annotation.value().getType();//这里为了演示方便,我设置枚举的type的时候,就是导出的后缀名
break;
}
}
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=\"" + fileName +"\"")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(bytes);
}
@NonNull
private static List<Order> getOrders() {
//先构造一个orderList虚假数据
List<Order> orderList = new ArrayList<>();
orderList.add(new Order("10001","大米手机", BigDecimal.valueOf(4000)));
orderList.add(new Order("10002","麦克电脑", BigDecimal.valueOf(8000)));
orderList.add(new Order("10003","vr眼镜", BigDecimal.valueOf(4000)));
return orderList;
}
}
这样修改之后,以后想新增一种格式导出的时候,只需要新增一个枚举类,然后新增一个实现类即可。
现在还可以优化的地方,每次一个请求进入到controller,都需要遍历这个exportServiceList,时间上可以优化,我们可以用
@PostConstruct初始化一个Map,只要能拿到枚举类型,就能找到对应的service类,只需要初始化的时候进行一次就行,后面每次都是直接通过map取就行
java
import com.hao.strategy.annotation.EnableExportType;
import com.hao.strategy.domain.Order;
import com.hao.strategy.enums.ExportType;
import com.hao.strategy.service.ExportService;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.lang.NonNull;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/export")
public class ExportController {
@Autowired
private List<ExportService> exportServiceList; //拿到所有实现了ExportService的实现类
private Map<ExportType,ExportService> typeExportServiceMap = new HashMap<>();
@PostConstruct
public void init(){
typeExportServiceMap = exportServiceList.stream()
.filter(s->s.getClass().isAnnotationPresent(EnableExportType.class))
.collect(Collectors.toMap(
s->s.getClass().getAnnotation(EnableExportType.class).value(),
Function.identity()
));
if(typeExportServiceMap.size() != ExportType.values().length){
throw new IllegalArgumentException("导出实现类数量与 ExportType 枚举类数量不匹配"); //实现类的数量不等于支持枚举的数量,说明实现类上每加对应的注解,或者没更新枚举类
}
}
@GetMapping
public ResponseEntity<byte[]> export(String type){
//拿到所有的模拟数据
List<Order> orderList = getOrders();
ExportType typeByName = ExportType.getTypeByName(type);
if(typeByName == null){
throw new IllegalArgumentException("不支持的导出类型:"+type);
}
byte[] bytes = new byte[0];
String fileName = "export.";
ExportService exportService = typeExportServiceMap.get(typeByName);
if(exportService==null){
throw new RuntimeException("未找到对应的export实现类");
}
bytes = exportService.export(orderList);
fileName += typeByName.getType();//这里为了演示方便,我设置枚举的type的时候,就是导出的后缀名
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=\"" + fileName +"\"")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(bytes);
}
@NonNull
private static List<Order> getOrders() {
//先构造一个orderList虚假数据
List<Order> orderList = new ArrayList<>();
orderList.add(new Order("10001","大米手机", BigDecimal.valueOf(4000)));
orderList.add(new Order("10002","麦克电脑", BigDecimal.valueOf(8000)));
orderList.add(new Order("10003","vr眼镜", BigDecimal.valueOf(4000)));
return orderList;
}
}
3.总结
感觉策略模式,最重要的就是定义出接口和这个选择策略的逻辑。
策略模式,感觉非常淋漓尽致地体验了面向对象多态的特点。