设计模式(十五):策略模式(StrategyPattern)实际应用

star2017 1年前 ⋅ 274 阅读

要在实际项目中应用策略模式,最好先仔细了解策略模式的定义和相关概念,可参考 设计模式(九):策略模式(Strategy Pattern)

策略模式(Strategy Pattern):定义一系列算法(算法家族),并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。

使用了策略模式,在系统设计层面是满足 里氏替换原则开放封闭原则 的,每个算法可以相互替换,在不修改已有算法的情况下易于扩展。

关于if...else

网上和公众号上很多标题党,用干掉 if...else...类的文章标题吸引眼球来描述策略模式如何替换 if...else...,个人认为如果思路限于替换 if...else... 是比较狭隘的。

查看 Spring 某些源码,也会看到有多层 if...else...嵌套的的使用。if...else... 在一些分支判断处理上是简单直接明了的。稍有个1年以上开发经验都应该不会写一大堆的嵌套 if...else....(超过三层的嵌套),最简单的优化处理是抽出为独立的方法。

要使用设计模式的重点是关注可扩展,可维护。使用策略模式替换 if...else... 的核心是各个算法独立,可以相互替换互不影响。

策略模式应用

业务场景

一个典型的业务场景是电商的会员折扣,节假日促销活动等,要计算不同等级的会员的优惠折扣:

  • 电商平台会员有多个等级,为会员设置对应的优惠折扣,其中 VVIP会员 8 折、VIP会员折扣 9 折,普通用户没有折扣三种。
  • 系统需要在用户付款时,根据用户的会员等级选择相应的优惠折扣,计算出应付金额。
  • 节假日促销,VVIP 会员还可享满 109 减 20 元的优惠。
  • 又一个需求,如果VVIP 或 VIP 会员到期且在一周内,且未使用过临时折扣,则按临时折扣只计算一次,并引导用户再次开通会员。

if...else伪代码

public BigDecimal calPrice(BigDecimal orderPrice, String buyerType) {

    if (VVIP) {
        if (8折价格 > 109 元) {
            returen 8折价格 - 20;
        }
        returen 8折价格;
    }

    if (VIP) {
        return 9 折价格;
    }

    if (普通用户) {
        if( VVIP 或 VIP 刚过期且未使用临时折扣) {
            更新临时折扣次数();
            return 9折价格;
        }
        return 原价;
    }
    return 原价;
}

以上是 if...else....实现的伪代码,有多个判断和嵌套,虽然能实现业务需求,但站在系统设计层面上,可维护性和可扩展性都非常低,不符合设计原则。

策略模式

针对类似这种算法规则,可以相互替换且不相互影响的,可以引入策略模式来提升代码的可维护性和可扩展性。

定义一个抽象接口,计算价格的方法,具体实现由策略子类实现:

public interface UserPayService {

    /**
     * 计算应付价格
     */
    public BigDecimal quote(BigDecimal orderPrice);
}

定义计算各个种会员价格的策略类:

public class VVIPPayService implements UserPayService {

    @Override
    public BigDecimal quote(BigDecimal orderPrice) {
        if (8折价格 > 109 元) {
            returen 8折价格 - 20;
        }
        returen 8折价格;
    }
}

public class VIPPayService implements UserPayService {

    @Override
    public BigDecimal quote(BigDecimal orderPrice) {
        return 9 折价格;
    }
}

public class NormalPayService implements UserPayService {

    @Override
    public BigDecimal quote(BigDecimal orderPrice) {
        if( VVIP 或 VIP 刚过期且未使用临时折扣) {
            更新临时折扣次数();
            return 9折价格;
        }
        return 原价;
    }
}

引入了策略之后,可以按照如下方式进行价格计算:

public class Test {

    public static void main(String[] args) {
        UserPayService strategy = new VVIPPayService();
        BigDecimal quote = strategy.quote(300);
        System.out.println("VVIP会员应付金额:" + quote.doubleValue());

        strategy = new VIPPayService();
        quote = strategy.quote(300);
        System.out.println("VIP会员应付金额:" + quote.doubleValue());
    }
}

上面的示例的实现方式就与 JDK 自带的线程池管理器需要手动指定拒绝策略是一样的。

工厂模式

但在业务系统中,是需要根据用户会员等级来匹配对应的策略,根据用户会员等级标识返回计算策略对象。

public BigDecimal calPrice(BigDecimal orderPrice,String vipType) {
     if (vipType == VVIP) {
        UserPayService strategy = new VVIPPayService();
        return strategy.quote(orderPrice);
     }

     if (vipType == VIP) {
        UserPayService strategy =  new VIPPayService();
        return strategy.quote(orderPrice);
     }

     if (vipType == normal) {
        UserPayService strategy = new NormalPayService();
        return strategy.quote(orderPrice);
     }
     return 原价;
}

使用 if...else...switch 判断用户会员类型,返回对应的计费策略并计算应付金额。

基于 Spring 框架,通常会把策略类注册为 Spring 容器中的 Bean,需要使用时再人容器中取出该 Bean,而不是在代码中 New 一个实例来使用。

public BigDecimal calPrice(BigDecimal orderPrice, String vipType) {
     if (vipType == VVIP) {
        UserPayService strategy = SpringContextHolder.getBean(VVIPPayService.class);
        return strategy.quote(orderPrice);
     }

     if (vipType == VIP) {
        UserPayService strategy = SpringContextHolder.getBean(VIPPayService.class);
        return strategy.quote(orderPrice);
     }

     if (vipType == normal) {
        UserPayService strategy = SpringContextHolder.getBean(NormalPayService.class);
        return strategy.quote(orderPrice);
     }
     return 原价;
}

策略模式的使用,在客户端侧必须理解这些算法的区别,以便选择合适的算法。即在选择策略时仍需要执行分支判断。

结合工厂模式单例模式,可以将策略类缓存起来,创建一个策略工厂专门用于生产和获取策略类。

public class UserPayServiceStrategyFactory {

    private static Map<String,UserPayService> services = new ConcurrentHashMap<String,UserPayService>();

    public  static UserPayService getByUserType(String type){
        return services.get(type);
    }

    public static void register(String userType,UserPayService userPayService) {
        Assert.notNull(userType,"userType can't be null");
        services.put(userType,userPayService);
    }
}

可以在系统启动时就初化一个各个会员等级计费策略对象实例,或使用单例模式,在每次使用时判断,不存在则创建并缓存,类似于 Spring Bean。调用如下:

public BigDecimal calPrice(BigDecimal orderPrice,User user) {
     String vipType = user.getVipType();
     UserPayService strategy = UserPayServiceStrategyFactory.getByUserType(vipType);
     return strategy.quote(orderPrice);
}

上面的工厂类定义了一个线程安全的 ConcurrentHashMap,用来保存所有的策略类的实例,提供一个 getByUserType 方法,可以根据类型直接获取对应的策略类实例。这个 Map 实际就相当于本地缓存了。

工厂类还定义了一个 Register 方法,借助该方法将策略类的 Bean 添加到 Map 中。

Spring Bean 的注册

借助 Spring 中提供的 InitializingBean 接口,该接口为 Bean 提供了属性初始化后的处理方法 afterPropertiesSet()。凡是继承该接口的类,在 Bean 的属性初始化后都会执行该方法。

接下来将各个策略类稍作改造即可:

@Service
public class VVIPPayService implements UserPayService,InitializingBean {

    @Override
    public BigDecimal quote(BigDecimal orderPrice) {
        if (8折价格 > 109 元) {
            returen 8折价格 - 20;
        }
        returen 8折价格;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        UserPayServiceStrategyFactory.register("VVIP",this);
    }
}

@Service
public class VIPPayService implements UserPayService ,InitializingBean{

    @Override
    public BigDecimal quote(BigDecimal orderPrice) {
        return 9 折价格;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        UserPayServiceStrategyFactory.register("VIP",this);
    }
}

@Service  
public class NormalPayService implements UserPayService,InitializingBean {

    @Override
    public BigDecimal quote(BigDecimal orderPrice) {
        if( VVIP 或 VIP 刚过期且未使用临时折扣) {
            更新临时折扣次数();
            return 9折价格;
        }
        return 原价;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        UserPayServiceStrategyFactory.register("NORMAL",this);
    }
}

只需要每一个策略类都实现 InitializingBean 接口,并实现其 afterPropertiesSet 方法,在这个方法中调用 UserPayServiceStrategyFactory.register 即可。

这样,在 Spring 初始化的时候,当创建 VVIPPayService、VIPPayService 和 NormalPayService 的时候,会在 Bean 的属性初始化之后,把这个 Bean 注册到 UserPayServiceStrategyFactory 中的 Map 里。

注解实现策略模式

业务场景

开发支付系统,集成支付宝和微信支付,支付宝和微信有多种支付方式,如, JSAPI,付款码,Native 等方式,客户端传入支付渠道和支付方式,选择正确的支付策略。

注解实现

  1. 支付订单实体类

    @Data
    public class PayOrder {
    
        /**
         * 支付渠道
         */
        private String channel;
        /**
         * 支付方式
         */
        private String payMethod;
        /**
         * 订单号
         */
        private String payNo;
        /**
         * 支付
         */
        private BigDecimal amount;
    }
    
  2. 定义支付处理接口抽象

    public interface PayHandler {
        void handle(PayOrder payOrder);
    }
    
  3. 创建支付处理注解,来标识该类是处理何种支付方式的支付单

    该注解加了 @Service 注解,具体的支付处理类注册为 Spring 容器中的 Bean。

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Service
    public @interface PayHandlerType {
        String payMethod();
    }
    
  4. 创建支付处理类:PayService.payService方法中,通过策略(支付方式)决定选择哪一个 PayHandler 去处理支付单。

    在 PayService 中,维护了一个 payHandlerMap,它的 key 为支付方式,value 为对应的支付处理器 Handler。通过@Autowired 去初始化 orderHandleMap(示例使用了一个 lambda 表达式,List 转 Map 并去重处理)。

    @Service
    public class PayService {
    
        private Map<String, PayHandler> payHandlerMap;
    
        @Autowired
        public void setPayHandlerMap(List<PayHandler> payHandlers) {
            // 注入各种类型的订单处理类
            payHandlerMap = payHandlers.stream().collect(
                    Collectors.toMap(payHandler -> AnnotationUtils.findAnnotation(payHandler.getClass(), PayHandlerType.class).get().payMethod(),
                            v -> v, (v1, v2) -> v1));
        }
    
        public void payService(PayOrder payOrder) {
            // ...一些前置处理
    
            // 通过订单来源确定对应的handler
            PayHandler payHandler = payHandlerMap.get(payOrder.getPayMethod());
            payHandler.handle(payOrder);
    
            // ...一些后置处理
        }
    }
    
  5. 支付渠道和支付方式的具体处理类,例如 处理支付宝JSAPI 和 微信JSAPI 的支付。

    支付宝JSAPI支付:

    @PayHandlerType(payMethod = "AliJsapi")
    public class AliJSAPIPayHandler implements PayHandler {
        @Override
        public void handle(PayOrder payOrder) {
            System.out.println("处理支付宝JSAPI支付");
        }
    }
    

    微信JSAPI支付:

    @PayHandlerType(payMethod = "WeChatJsapi")
    public class WeChatJSAPIPayHandler implements PayHandler {
        @Override
        public void handle(PayOrder payOrder) {
            System.out.println("处理微信JSAPI支付");
        }
    }
    

总结优化

以上示例还可以引入模板方法模式,将判断 VIP 类型和调用计费方法定义到抽象模板方法类中做为流程规范。

还有 UserPayServiceStrategyFactory.register 调用的时候,第一个参数需要传一个字符串,可以使用使用枚举,或者在每个策略类中自定义一个 getUserType 方法来进行优化。

如果对策略模式和工厂模式了解的话,上面示例并不是严格意义上面的策略模式和工厂模式。首先,策略模式中重要的 Context 角色在这里没有,没有 Context,也就没有用到组合的方式,而是使用工厂代替了。

对于设计模式的学习,重点是学习其思想,其次是在实际业务中的代码实现。

参考资料

  1. 刚来的大神彻底干掉了代码中的if else...
  2. 别再用if-else了,用注解去代替他吧
更多内容请访问:IT源点

相关文章推荐

全部评论: 0

    我有话说: