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

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

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

相关推荐
fulton44 分钟前
AI写小说失败的第一原因是什么
后端
fulton1 小时前
这段文字有AI味"到底怎么判断
后端
fulton1 小时前
为什么AI写的小说节奏总是崩
后端
fulton1 小时前
为什么AI写到30章角色就崩人设
后端
fulton1 小时前
AI写小说每章都要过安检?8级质量门禁详解
后端
fulton1 小时前
AI写长篇小说为什么30章必崩
后端
fulton1 小时前
AI写到第30章就"失忆"?15条状态轨详解
后端
fulton1 小时前
这是核心创意卡最反直觉的地方:5项不可修改的设定,反而让大纲设计变得**更容易**而非更难。 | 维度 | 无约束 | 有5项锚点 | |---|---|
后端
小林ixn1 小时前
单例模式:从弹窗管理到全局状态,一个模式搞定
前端·javascript·设计模式