跳转至

策略模式

图片的样式

➊单纯策略模式,没有结合其他设计模式

感觉策略模式更适合在实际开发中,有这样一种业务:它是有好多的逻辑,但是有很多不同的进入情况,可以是业务需要也可以是逻辑需要, 这个模式对于代码,可以简化业务逻辑,不至于将代码写成一坨,长长的几十上百行,同时又具有很好的扩展能力,可以很方便的加入新逻辑 最典型的场景,就是if条件的逻辑判断简化。

它无非四个步骤

  1. 抽象逻辑
  2. 要实现具体的逻辑策略
  3. 选一个不同的策略
  4. 调用策略具体的逻辑

但是它也有缺点

1️⃣如果 if-else的判断情况很多,那么对应的具体策略实现类也会很多
2️⃣没法俯视整个分派的业务逻辑,因为具体的逻辑被分拆

一次面试中,阿里面试官提到策略模式,他不这样认为,觉得都是一样需要看很多分支逻辑的,无论怎么改代码, 基础的业务逻辑就是无可更改的,天王老子来了该写的逻辑就是要写,该看的逻辑还是要看,策略模式更像是整理代码的一种方法, 就是不用也没法“俯视”所有逻辑。这也是一种全新的理解。

不大喜欢一些理论性质的解释,直接样例理解,自己更能懂,代码就是一种语言,它自己会表达清楚

🏀代码样例

首先要有一个接口,里面要包含用于判断每个业务的不同,每个业务具体的实现的方法,比如要计算快递的收费, 就要有一个判断每个快递的类型的方法,每一种快递到底是如何计费的。

/**
 * 策略模式接口设计
 */
public interface LogisticsService {

    /**
     * 1️⃣需要用来判断具体走那个逻辑
     */
    boolean isCurrentLogistics(Integer type);

    /**
     * 2️⃣计算逻辑
     */
    BigDecimal calculateFee(TransferFeeRequest transferFeeRequest);
}

同时具体如何计费,就要有快递的运输距离,单价,最重要的快递类型,这些都是具体的业务,参数到底如何写是业务决定的, 也可以有快递的重量,体积,形状等,每家快递公司的收费标准不一样,还有一些政策更改,体量优惠减免,是否有保价, 都会决定具体的逻辑。

@Data
public class TransferFeeRequest {

    /**
     * 距离
     */
    private BigDecimal distance;

    /**
     * 单价
     */
    private BigDecimal unitPrice;

    /**
     * 快递类型
     */
    private Integer type;
}

一般这种业务都是写成具体的服务,比如合作的快递公司有京东、顺丰、圆通、中通,那就是四个服务

/**
 * JD快递服务
 */
@Service
public class JDTransfercompany implements LogisticsService {
    private final BigDecimal pickFee = BigDecimal.TEN;

    private final BigDecimal minDistance = BigDecimal.valueOf(80);

    @Override
    public BigDecimal calculateFee(TransferFeeRequest transferFeeRequest) {
        BigDecimal distance = minDistance.compareTo(transferFeeRequest.getDistance()) > 0 ?
                minDistance : transferFeeRequest.getDistance();
        // do business
        return distance.multiply(transferFeeRequest.getUnitPrice()).add(pickFee);
    }

    @Override
    public boolean isCurrentLogistics(Integer type) {
        return Objects.equals(type, 1);
    }
}
/**
 * 顺丰快递服务
 */
@Service
public class SFTransfercompany implements LogisticsService {
    private final BigDecimal pickFee = BigDecimal.TEN;

    private final BigDecimal minDistance = BigDecimal.valueOf(60);

    @Override
    public BigDecimal calculateFee(TransferFeeRequest transferFeeRequest) {
        BigDecimal distance = minDistance.compareTo(transferFeeRequest.getDistance()) > 0 ?
                minDistance : transferFeeRequest.getDistance();
        // do business
        return distance.multiply(transferFeeRequest.getUnitPrice()).add(pickFee);
    }

    @Override
    public boolean isCurrentLogistics(Integer type) {
        return Objects.equals(type, 2);
    }
}
/**
 * 中通快递服务
 */
@Service
public class ZTTransfercompany implements LogisticsService {
    private final BigDecimal pickFee = BigDecimal.TEN;

    private final BigDecimal minDistance = BigDecimal.valueOf(40);

    @Override
    public BigDecimal calculateFee(TransferFeeRequest transferFeeRequest) {
        BigDecimal distance = minDistance.compareTo(transferFeeRequest.getDistance()) > 0 ? 
                minDistance : transferFeeRequest.getDistance();
        // do business
        return distance.multiply(transferFeeRequest.getUnitPrice()).add(pickFee);
    }

    @Override
    public boolean isCurrentLogistics(Integer type) {
        return Objects.equals(type, 3);
    }
}
/**
 * 圆通快递服务
 */
@Service
public class YTTransfercompany implements LogisticsService {
    private final BigDecimal pickFee = BigDecimal.TEN;

    private final BigDecimal minDistance = BigDecimal.valueOf(20);

    @Override
    public BigDecimal calculateFee(TransferFeeRequest transferFeeRequest) {
        BigDecimal distance = minDistance.compareTo(transferFeeRequest.getDistance()) > 0 ?
                minDistance : transferFeeRequest.getDistance();
        // do business
        return distance.multiply(transferFeeRequest.getUnitPrice()).add(pickFee);
    }

    @Override
    public boolean isCurrentLogistics(Integer type) {
        return Objects.equals(type, 4);
    }
}

都是类似的,本来可能是if-else一撸到底,现在变成分开实现(例子只是例子,比较简)

那我们在使用的时候就简单一些了,用stream流就能调用

🐔使用方式

    /**
     * 策略模式需要把所有的可能都注入进来
     */
    private final List<LogisticsService> logisticsServices;

    public BigDecimal calculateFee(@RequestBody TransferFeeRequest transferFeeRequest) {
        // 遍历,根据每个类中的具体实现类来判断到底走那个逻辑
        LogisticsService logisticsService = logisticsServices.stream()
                .filter(logistics -> logistics.isCurrentLogistics(transferFeeRequest.getType()))
                .findFirst()
                .orElse(null);
        // 一定要有对应匹配为空对不上的兜底逻辑,也可以单独列出一个实现类来写
        if (logisticsService == null) {
            throw new RuntimeException("没有对应的快递计算方式");
        }
        return logisticsService.calculateFee(transferFeeRequest);
    }

我现在也只讲设计模式如何用,使用场景等,但是对它的理解实在是见仁见智了。其他理论化的介绍就不写了。


➋策略模式结合工厂方法模式

这个模式相较于单纯策略模式,加入了工厂方法,将一些获取逻辑再度封装一下,这样使用的时候只要简单的获取, 没有单纯策略模式那样需要自己写获取对象并判断的逻辑

🏀代码样例

首先还是一样需要一个接口,里面写一些业务需要的方法

public interface ActionService {
    /**可以在下面自己扩展新的方法以适应实际业务*/
    void doAction();
}

之后就是实现它,每一个实现类,对应一种不同的业务分支情况

@Service
public class ActionService1 implements ActionService {
    @Override
    public void doAction() {
        System.out.println("该分支下业务实际要做得而事情");
    }
}
@Service
public class ActionService2 implements ActionService {
    @Override
    public void doAction() {
        System.out.println("该分支下业务实际要做得而事情");
    }
}

最后也是最重要的一步,有一个工厂类方便封装判断逻辑(这里将理解写到代码注释里)

public class ActionServiceFactory {
    /**
     * 维护一个MAP,作为用来放策略的容器,可以随时添加新的
     */
    private static final Map<String, ActionService> ACTION_SERVICE_MAP = new HashMap<>();

    static {
        ACTION_SERVICE_MAP.put("action1", new ActionService1());
        ACTION_SERVICE_MAP.put("action2", new ActionService2());
        // .....继续添加新策略
    }

    private ActionServiceFactory() {
    }

    /**
     * 使用内部类单例模式,统一对象的获取
     */
    private static class SingletonHolder {
        private static final ActionServiceFactory INSTANCE = new ActionServiceFactory();
    }

    public static ActionServiceFactory getInstance() {
        return SingletonHolder.INSTANCE;
    }

    /**
     * 这里就是将获取判断的逻辑封装进工厂里面,不需要使用的时候写,不然用一次写一次
     */
    private static ActionService getActionService(String actionCode) {
        ActionService actionService = ACTION_SERVICE_MAP.get(actionCode);
        // 一定要有兜底逻辑
        if (ObjectUtil.isNull(actionService)) {
            throw new RuntimeException("非法 actionCode");
        }
        return actionService;
    }

    /**
     * 统一获取,使得该方法作为工厂的生产物
     */
    public void doAction(String actionCode) {
        getActionService(actionCode).doAction();
    }
}

🐔使用方式

这样的话,使用的时候就很简洁又简单了,一行代码就行,无论在那里使用都一样

@RestController
@RequestMapping("/test")
public class TestController {

    @PostMapping("")
    public void strategyFactory(String actionCode) {
        // 这里的actionCode也要根据实际业务改动
        ActionServiceFactory.getInstance().doAction(actionCode);
    }
}

➌ Map+函数式接口的变种策略模式

这个就没有前面的策略模式那样,需要定义一个接口加实现类,直接将你的业务放到服务里面, 每一个策略就是一个方法,这样就可以避免业务策略多的时候,实现类很多的情况,同时又保留了 工厂模式的简单获取的优点

🏀代码样例

首先写一个服务,里面一个业务策略就是一个方法,每个方法写策略对应的逻辑

@Service
public class GrantTypeSerive {
    public String redPaper(String resourceId) {
        // 红包的发放方式
        return "每周末9点发放";
    }
    public String shopping(String resourceId) {
        // 购物券的发放方式
        return "每周三9点发放";
    }
    public String QQVip(String resourceId) {
        // qq会员的发放方式
        return "每周一0点开始秒杀";
    }

    public String WXVip(String resourceId) {
        // 微信会员的发放方式
        return "微信会员";
    }
}

然后再写一个服务用于封装判断逻辑,方便获取

/**
 * 就是将对应的if-else分支判断依据作为key,要走的逻辑作为value,匹配对应结果达到目的
 */
@Service
@RequiredArgsConstructor
public class QueryGrantTypeService {
    private final GrantTypeSerive grantTypeSerive;
    private final Map<String, Function<String, String>> grantTypeMap = Maps.newHashMap();

    /**
     * 初始化业务分派逻辑,代替了if-else部分
     * key: 优惠券类型
     * value: lambda表达式,最终会获得该优惠券的发放方式
     */
    @PostConstruct
    public void dispatcherInit() {
        grantTypeMap.put("红包", grantTypeSerive::redPaper);
        grantTypeMap.put("购物券", grantTypeSerive::shopping);
        grantTypeMap.put("qq会员", grantTypeSerive::QQVip);
        grantTypeMap.put("微信会员", grantTypeSerive::WXVip);
    }

    public String getResult(String resourceType) {
        // Controller根据 优惠券类型 `resourceType`、编码`resourceId` 去查询 发放方式`grantType`
        Function<String, String> result = grantTypeMap.get(resourceType);
        if (result != null) {
            // 传入`resourceId` 执行这段表达式获得String型的grantType
            // 这里的id还是需要根据具体的业务区别,有时候可能压根没有这种东西
            return result.apply("resourceId");
        }
        // 兜底逻辑
        return "查询不到该优惠券的发放方式";
    }
}

🐔使用方式

这样只需要注入并获取就可以了(其实也可以在写一个单例,这样都不用注入了,但是这样就和spring的容器有一点冲突)

@RestController
@RequiredArgsConstructor
public class GrantTypeController {

    private final QueryGrantTypeService queryGrantTypeService;

    @PostMapping("/grantType")
    public String test(String resourceName) {
        return queryGrantTypeService.getResult(resourceName);
    }
}

➍ 基于注解的策略模式

这个是自己看B站up生生学来的一种,也记录一下,其实看代码就能懂它的设计和优缺点,它归根还是使用map的方式,只是怎么把map的值放进去会不一样, 它就是使用注解,然后去找对应策略类的注解值,再放入map去匹配每一次请求的入参类型。我觉得它好的一点就是使用枚举管理, 还有就是使用了注解来设置策略类的类型,这样就比较简介一点,一般策略模式都是要使用一些方法干扰到策略类的业务,这个就只有一个注解,侵入性小一点。 反正这里也做一个记录吧。

🏀代码样例

当时up使用的是开会员的例子,根据充值金额的大小设置不同的会员等级。枚举类就放你的策略。

public enum UserType {
    // 普通
    NORMAL(recharge -> recharge > 0 && recharge <= 100),
    // 小会员
    SMALL(recharge -> recharge > 100 && recharge <= 1000),
    // 大会员
    BIG(recharge -> recharge > 1000 && recharge <= 10000),
    // 特别会员
    SUPER(recharge -> recharge > 10000 && recharge <= 100000),
    // 私人助理会员
    PERSONAL(recharge -> recharge > 100000);

    private final IntPredicate support;

    UserType(IntPredicate support) {
        this.support = support;
    }

    public static UserType typeOf(int recharge) {
        for (UserType value : values()) {
            if (value.support.test(recharge)) {
                return value;
            }
        }
        return null;
    }
}

然后搞一个接口,用于设置不同策略类的枚举值,到时候方便匹配。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface SupportUserType {
    UserType value();
}

这个是策略类的接口设计,其实都一样,就是多了一个通过枚举查找对应的值的方法。

public interface CustomerService {
    String findCustomer();

    default UserType findUserTypeFromService(CustomerService customerService) {
        SupportUserType annotation = AopUtils.getTargetClass(customerService).getAnnotation(SupportUserType.class);
        return annotation.value();
    }
}
@Service
@SupportUserType(UserType.BIG)
public class BigRCustomerService implements CustomerService {
    @Override
    public String findCustomer() {
        System.out.println("大R玩家客服");
        return "大R玩家客服";
    }
}
@Service
public class DefaultCustomerService implements CustomerService {
    @Override
    public String findCustomer() {
        return "找不到客服";
    }
}
@Service
@SupportUserType(UserType.NORMAL)
public class NormalCustomerService implements CustomerService {
    @Override
    public String findCustomer() {
        System.out.println("普通玩家客服");
        return "普通玩家客服";
    }
}
@Service
@SupportUserType(UserType.PERSONAL)
public class PersonalCustomerService implements CustomerService {
    @Override
    public String findCustomer() {
        return "专属客服";
    }
}
@Service
@SupportUserType(UserType.SMALL)
public class SmallRCustomerService implements CustomerService {
    @Override
    public String findCustomer() {
        System.out.println("小R玩家客服");
        return "小R玩家客服";
    }
}
@Service
@SupportUserType(UserType.SUPER)
public class SuperRCustomerService implements CustomerService {
    @Override
    public String findCustomer() {
        System.out.println("超R玩家客服");
        return "超R玩家客服";
    }
}

🐔使用方式

使用的时候,无非就是要解决策略匹配的问题,这个类型的策略模式本质还是使用的是map匹配。所以还是要找一个map放进去, 这里使用的是注入一个map,在set方法里面使用stream的api搞一个map出来,这样的话在使用的接口里面根据对应的入参获取对应的策略类即可。

@RestController
@RequestMapping("/customer")
public class CustomerController {
    private Map<UserType, CustomerService> customerServiceMap;

    @Autowired
    private DefaultCustomerService defaultCustomerService;

    @GetMapping("/{recharge}")
    public String customer(@PathVariable(value = "recharge") int recharge) {
        UserType userType = UserType.typeOf(recharge);
        CustomerService customerService = customerServiceMap.getOrDefault(userType, defaultCustomerService);
        return customerService.findCustomer();
    }

    @Autowired
    public void setCustomerServiceMap(List<CustomerService> customerServiceList) {
        this.customerServiceMap = customerServiceList.stream()
                        .filter(customerService
                                -> customerService.getClass().isAnnotationPresent(SupportUserType.class))
                        .collect(Collectors.toMap(customerService
                                -> customerService.findUserTypeFromService(customerService), Function.identity()));
        if (this.customerServiceMap.size() != UserType.values().length) {
            throw new IllegalArgumentException("有用户类型没有对应的策略");
        }
    }
}

❺ 策略+责任链+组合模式(以合同签章为例)

这个是我在看微信公众号 “Java基基” 的时候了解到的 TODO 整理文章内容

原文链接🔗:《进阶玩法:策略+责任链+组合实现合同签章》