从if-else到策略模式

策略模式

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.总结

感觉策略模式,最重要的就是定义出接口和这个选择策略的逻辑。

策略模式,感觉非常淋漓尽致地体验了面向对象多态的特点。

相关推荐
步行cgn9 小时前
YAML 的语法规则
spring boot·后端
骇客野人9 小时前
SpringBoot电商系统用户注册、登录、留存及数据埋点设计与落地实施方案
java·spring boot·后端
IT_陈寒12 小时前
React的状态更新坑得我差点加班到天亮
前端·人工智能·后端
码事漫谈13 小时前
一天搭出 80% 的 Agent,然后卡在 95% 到死
后端
leonkay13 小时前
C# 锁机制——【2】深入原理
开发语言·后端·spring·c#·个人开发
江湖十年14 小时前
在 Go 中使用 dyno 包处理动态对象
后端·面试·go
八角丶14 小时前
Node 网络编程 —— TLS 模块
javascript·后端·node.js
山岚的运维笔记14 小时前
mysql 专业笔记 -- 第 8 章:使用变量
运维·数据库·笔记·后端·学习·mysql·dba
平头哥AI15 小时前
Day 01 | go run 跑通第一个 Go 程序,go build 留下一个能拷走的 exe
开发语言·后端·golang
ZGG00316 小时前
线程池详解:从 7 大参数到生产避坑
java·数据库·后端