Redian新闻
>
设计模式最佳实践探索—策略模式

设计模式最佳实践探索—策略模式

公众号新闻

根据不同的应用场景与意图,设计模式主要分为创建型模式、结构型模式和行为型模式三类。本文主要探索行为型模式中的策略模式如何更好地应用于实践中。



前言


在软件开发的过程中,需求的多变性几乎是不可避免的,而作为一名服务端开发人员,我们所设计的程序应尽可能支持从技术侧能够快速、稳健且低成本地响应纷繁多变的业务需求,从而推进业务小步快跑、快速迭代。设计模式正是前辈们针对不同场景下不同类型的问题,所沉淀下来的一套程序设计思想与解决方案,用来提高代码可复用性、可维护性、可读性、稳健性以及安全性等。下面是设计模式的祖师爷GoF(Gang of Four,四人帮)的合影,感受一下大佬的气质~



灵活应用设计模式不仅可以使程序本身具有更好的健壮性、易修改性和可扩展性,同时它使得编程变得工程化,对于多人协作的大型项目,能够降低维护成本、提升多人协作效率。根据不同的应用场景与意图,设计模式主要分为三类,分别为创建型模式、结构型模式和行为型模式。本文主要探索行为型模式中的策略模式如何更好地应用于实践中。


使用场景


策略模式属于对象的行为模式,其用意是针对一组可替换的算法,将每一个算法封装到具有共同接口的独立的类中,使得算法可以在不影响到客户端(算法的调用方)的情况下发生变化,使用策略模式可以将算法的定义与使用隔离开来,保证类的单一职责原则,使得程序整体符合开闭原则。


以手淘中商详页的店铺卡片为例,店铺卡片主要包含店铺名称、店铺logo、店铺类型以及店铺等级等信息,其中不同店铺类型的店铺等级计算逻辑是不同的,为了获取店铺等级,可以采用如下所示代码:


 if (Objects.equals("淘宝", shopType)) {   // 淘宝店铺等级计算逻辑   // return 店铺等级; } else if (Objects.equals("天猫", shopType)) {   // 天猫店铺等级计算逻辑   // return 店铺等级 } else if (Objects.equals("淘特", shopType)) {   // 淘特店铺等级计算逻辑   // return 店铺等级 } else {   //  ... }

这种写法虽然实现简单,但使得各类店铺等级计算逻辑与程序其他逻辑相耦合,未来如果要对其中一种计算逻辑进行更改或者新增加一种计算逻辑,将不得不对原有代码进行更改,违背了OOP的单一职责原则与开闭原则让代码的维护变得困难。若项目本身比较复杂,去改动项目原有的逻辑是一件非常耗时又风险巨大的事情。此时我们可以采取策略模式来处理,将不同类型的店铺等级计算逻辑封装到具有共同接口又互相独立的类中,其核心类图如下所示:



这样一来,程序便具有了良好的可扩展性与易修改性,若想增加一种新的店铺等级计算逻辑,则可将其对应的等级计算逻辑单独封装成ShopRankHandler接口的实现类即可,同样的,若想对其中一种策略的实现进行更改,在相应的实现类中进行更改即可,而不用侵入原有代码中去开发。


最佳实践探索


本节仍以店铺等级的处理逻辑为例,探索策略模式的最佳实践。当使用策略模式的时候,会将一系列算法用具有相同接口的策略类封装起来,客户端想调用某一具体算法,则可分为两个步骤:1、某一具体策略类对象的获取;2、调用策略类中封装的算法。比如客户端接受到的店铺类型为“天猫”,则首先需要获取TmShopRankHandleImpl类对象,然后调用其中的算法进行天猫店铺等级的计算。在上述两个步骤中,步骤2是依赖于步骤1的,当步骤1完成之后,步骤2也随之完成,因此上述步骤1成为整个策略模式中的关键。


下面列举几种策略模式的实现方式,其区别主要在于具体策略类对象获取的方式不同,对其优缺点进行分析,并探索其最佳实践。


  暴力法


  1. 店铺等级计算策略接口

    public interface ShopRankHandler {    /**    * 计算店铺等级    * @return 店铺等级    */        public String calculate();}
  2. 各类型店铺等级计算策略实现类

    淘宝店

    public class TbShopRankHandleImpl implements ShopRankHandler{    @Override    public String calculate() {        // 具体计算逻辑        return rank;    }}

    天猫店

    public class TmShopRankHandleImpl implements ShopRankHandler{    @Override    public String calculate() {        // 具体计算逻辑        return rank;    }}

    淘特店

    public class TtShopRankHandleImpl implements ShopRankHandler{    @Override    public String calculate() {        // 具体计算逻辑        return rank;    }}
  3. 客户端调用

    // 根据参数调用对应的算法计算店铺等级public String acqurireShopRank(String shopType) {    String rank = StringUtil.EMPTY_STRING;    if (Objects.equals("淘宝", shopType)) {        // 获取淘宝店铺等级计算策略类        ShopRankHandler shopRankHandler = new TbShopRankHandleImpl();        // 计算店铺等级        rank = shopRankHandler.calculate();    } else if (Objects.equals("天猫", shopType)) {        // 获取天猫店铺等级计算策略类        ShopRankHandler shopRankHandler = new TmShopRankHandleImpl();        // 计算店铺等级        rank = shopRankHandler.calculate();    } else if (Objects.equals("淘特", shopType)) {        // 获取淘特店铺等级计算策略类        ShopRankHandler shopRankHandler = new TtShopRankHandleImpl();        // 计算店铺等级        rank = shopRankHandler.calculate();    } else {        //  ...    }    return rank;}


  • 效果

至此,当我们需要新增策略类时,需要做的改动如下:

  1. 新建策略类并实现策略接口

  2. 改动客户端的if else分支


  • 优点

  1. 将店铺等级计算逻辑单独进行封装,使其与程序其他逻辑解耦,具有良好的扩展性。

  2. 实现简单,易于理解。


  • 缺点

客户端与策略类仍存在耦合,当需要增加一种新类型店铺时,除了需要增加新的店铺等级计算策略类,客户端需要改动if else分支,不符合开闭原则。


  第一次迭代(枚举+简单工厂)


有没有什么方法能使客户端与具体的策略实现类彻底进行解耦,使得客户端对策略类的扩展实现“零”感知?在互联网领域,没有什么问题是加一层解决不了的,我们可以在客户端与众多的策略类之间加入工厂来进行隔离,使得客户端只依赖工厂,而具体的策略类由工厂负责产生,使得客户端与策略类解耦,具体实现如下所示:


  1. 枚举类

    public enum ShopTypeEnum {    TAOBAO("A","淘宝"),    TMALL("B", "天猫"),    TAOTE("C", "淘特");        @Getter    private String type;    @Getter    private String desc;    ShopTypeEnum(String type, String des) {        this.type = type;        this.desc = des;    }}
  2. 店铺等级计算接口

    public interface ShopRankHandler {    /**    * 计算店铺等级    * @return 店铺等级    */        String calculate();}
  3. 各类型店铺等级计算策略实现类

    淘宝店

    public class TbShopRankHandleImpl implements ShopRankHandler{       @Override    public String calculate() {        // 具体计算逻辑        return rank;    }}

    天猫店

    public class TmShopRankHandleImpl implements ShopRankHandler{    @Override    public String calculate() {        // 具体计算逻辑        return rank;    }}

    淘特店

    public class TtShopRankHandleImpl implements ShopRankHandler{    @Override    public String calculate() {        // 具体计算逻辑        return rank;    }}
  4. 策略工厂类

    @Componentpublic class ShopRankHandlerFactory {        // 初始化策略beans    private static final Map<String, ShopRankHandler> GET_SHOP_RANK_STRATEGY_MAP = ImmutableMap.<String, ShopRankHandler>builder()        .put(ShopTypeEnum.TAOBAO.getType(), new TbShopRankHandleImpl())        .put(ShopTypeEnum.TMALL.getType(), new TmShopRankHandleImpl())        .put(ShopTypeEnum.TAOTE.getType(), new TtShopRankHandleImpl())        ;
    /** * 根据店铺类型获取对应的获取店铺卡片实现类 * * @param shopType 店铺类型 * @return 店铺类型对应的获取店铺卡片实现类 */ public ShopRankHandler getStrategy(String shopType) { return GET_SHOP_RANK_STRATEGY_MAP.get(shopType); }
    }
  5. 客户端调用

    @ResourceShopRankHandlerFactory shopRankHandlerFactory;// 根据参数调用对应的算法计算店铺等级public String acqurireShopRank(String shopType) {    ShopRankHandler shopRankHandler = shopRankHandlerFactory.getStrategy(shopType);    return Optional.ofNullable(shopRankHandler)        .map(shopRankHandle -> shopRankHandle.calculate())        .orElse(StringUtil.EMPTY_STRING);}


  • 效果

至此,当我们需要新增策略类时,需要做的改动如下:

  1. 新建策略类并实现策略接口

  2. 增加枚举类型

  3. 工厂类中初始化时增加新的策略类

相比上一种方式,策略类与客户端进行解耦,无需更改客户端的代码。


  • 优点

将客户端与策略类进行解耦,客户端只面向策略接口进行编程,对具体策略类的变化(更改、增删)完全无感知,符合开闭原则。


  • 缺点

需要引入额外的工厂类,使系统结构变得复杂。

当新加入策略类时,工厂类中初始化策略的部分仍然需要改动。


  第二次迭代(利用Spring框架初始化策略beans)


在枚举+简单工厂实现的方式中,利用简单工厂将客户端与具体的策略类实现进行了解耦,但工厂类中初始化策略beans的部分仍然与具体策略类存在耦合,为了进一步解耦,我们可以利用Spring框架中的InitializingBean接口与ApplicationContextAware接口来实现策略beans的自动装配。InitializingBean接口中的afterPropertiesSet()方法在类的实例化过程当中执行,也就是说,当客户端完成注入ShopRankHandlerFactory工厂类实例的时候,afterPropertiesSet()也已经执行完成。因此我们可以通过重写afterPropertiesSet()方法,在其中利用getBeansOfType()方法来获取到策略接口的所有实现类,并存于Map容器之中,达到工厂类与具体的策略类解耦的目的。相比于上一种实现方式,需要改动的代码如下:


  1. 店铺等级计算接口

    public interface ShopRankHandler {    /**    * 获取店铺类型的方法,接口的实现类需要根据各自的枚举类型来实现,后面就不贴出实现类的代码    * @return 店铺等级    */    String getType();    /**    * 计算店铺等级    * @return 店铺等级    */    String calculate();}
  2. 策略工厂类

    @Componentpublic class ShopRankHandlerFactory implements InitializingBean, ApplicationContextAware {
    private ApplicationContext applicationContext; /** * 策略实例容器 */ private Map<String, ShopRankHandler> GET_SHOP_RANK_STRATEGY_MAP;
    /** * 根据店铺类型获取对应的获取店铺卡片实现类 * * @param shopType 店铺类型 * @return 店铺类型对应的获取店铺卡片实现类 */ public ShopRankHandler getStrategy(String shopType) { return GET_SHOP_RANK_STRATEGY_MAP.get(shopType); }
    @Override public void afterPropertiesSet() { Map<String, ShopRankHandler> beansOfType = applicationContext.getBeansOfType(ShopRankHandler.class);
    GET_SHOP_RANK_STRATEGY_MAP = Optional.ofNullable(beansOfType) .map(beansOfTypeMap -> beansOfTypeMap.values().stream() .filter(shopRankHandle -> StringUtils.isNotEmpty(shopRankHandle.getType())) .collect(Collectors.toMap(ShopRankHandler::getType, Function.identity()))) .orElse(new HashMap<>(8)); }
    @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; }
    }


  • 效果

至此,当我们需要新增策略类时,需要做的改动如下:

  1. 新建策略类并实现策略接口

  2. 增加枚举类型

相比于上一种方式,可以省略工厂类在初始化策略beans时要增加新的策略类这一步骤。


  • 优点

借助Spring框架完成策略beans的自动装配,使得策略工厂类与具体的策略类进一步解耦。


  • 缺点

需要借助Spring框架来完成,不过在Spring框架应用如此广泛的今天,这个缺点可以忽略不计。


  最终迭代(利用泛型进一步提高策略工厂复用性)


经过上面两次迭代以后,策略模式的实现已经变得非常方便,当需求发生改变的时候,我们再也不用手忙脚乱了,只需要关注新增或者变化的策略类就好,而不用侵入原有逻辑去开发。但是还有没有改进的空间呢?


设想一下有一个新业务同样需要策略模式来实现,如果为其重新写一个策略工厂类,整个策略工厂类中除了新的策略接口外,其他代码均与之前的策略工厂相同,出现了大量重复代码,这是我们所不能忍受的。为了最大程度避免重复代码的出现,我们可以使用泛型将策略工厂类中的策略接口参数化,使其变得更灵活,从而提高其的复用性。


理论存在,实践开始!代码示意如下:


  1. 定义泛型接口

    public interface GenericInterface<E> {     E getType();}
  2. 定义策略接口继承泛型接口

    public interface StrategyInterfaceA extends GenericInterface<String>{
    String handle();}public interface StrategyInterfaceB extends GenericInterface<Integer>{
    String handle();}public interface StrategyInterfaceC extends GenericInterface<Long>{
    String handle();}
  3. 实现泛型策略工厂

    public class HandlerFactory<E, T extends GenericInterface<E>> implements InitializingBean, ApplicationContextAware {    private ApplicationContext applicationContext;    /**     * 泛型策略接口类型     */    private Class<T> strategyInterfaceType;
    /** * java泛型只存在于编译期,无法通过例如T.class的方式在运行时获取其类信息 * 因此利用构造函数传入具体的策略类型class对象为getBeansOfType()方法 * 提供参数 * * @param strategyInterfaceType 要传入的策略接口类型 */ public HandlerFactory(Class<T> strategyInterfaceType) { this.strategyInterfaceType = strategyInterfaceType; } /** * 策略实例容器 */ private Map<E, T> GET_SHOP_RANK_STRATEGY_MAP; /** * 根据不同参数类型获取对应的接口实现类 * * @param type 参数类型 * @return 参数类型对应的接口实现类 */ public T getStrategy(E type) { return GET_SHOP_RANK_STRATEGY_MAP.get(type); }
    @Override public void afterPropertiesSet() { Map<String, T> beansOfType = applicationContext.getBeansOfType(strategyInterfaceType); System.out.println(beansOfType);
    GET_SHOP_RANK_STRATEGY_MAP = Optional.ofNullable(beansOfType) .map(beansOfTypeMap -> beansOfTypeMap.values().stream() .filter(strategy -> StringUtils.isNotEmpty(strategy.getType().toString())) .collect(Collectors.toMap(strategy -> strategy.getType(), Function.identity()))) .orElse(new HashMap<>(8)); System.out.println(GET_SHOP_RANK_STRATEGY_MAP); }
    @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; }}
  4. 有了上述泛型策略工厂类,当我们需要新建一个策略工厂类的时候,只需要利用其构造函数传入相应的策略接口即可。生成StrategyInterfaceA、StrategyInterfaceB与StrategyInterfaceC接口的策略工厂如下:

    public class BeanConfig {    @Bean    public HandlerFactory<String, StrategyInterfaceA> strategyInterfaceAFactory(){        return new HandlerFactory<>(StrategyInterfaceA.class);    }    @Bean    public HandlerFactory<Integer, StrategyInterfaceB> strategyInterfaceBFactory(){        return new HandlerFactory<>(StrategyInterfaceB.class);    }    @Bean    public HandlerFactory<Long, StrategyInterfaceC> strategyInterfaceCFactory(){        return new HandlerFactory<>(StrategyInterfaceC.class);    }  }


  • 效果

此时,若想新建一个策略工厂,则只需将策略接口作为参数传入泛型策略工厂即可,无需再写重复的样板代码,策略工厂的复用性大大提高,也大大提高了我们的开发效率。


  • 优点

将策略接口类型参数化,策略工厂不受接口类型限制,成为任意接口的策略工厂。


  • 缺点

系统的抽象程度、复杂度变高,不利于直观理解。


结束语


学习设计模式,关键是学习设计思想,不能简单地生搬硬套,灵活正确地应用设计模式可以让我们在开发中取得事半功倍的效果,但也不能为了使用设计模式而过度设计,要合理平衡设计的复杂度和灵活性。


本文是对策略模式最佳实践的一次探索,不一定是事实上的最佳实践,欢迎大家指正与讨论。


END



开源最终的价值不仅是获客


这里有最新开源资讯、软件更新、技术干货等内容
点这里 ↓↓↓ 记得 关注✔ 标星⭐ 哦~

微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
2022金字招牌最佳实践典范智能化“竞赛下半场”,什么才是可量产的最佳实践?一文学会 ByteHouse 搭建数仓最佳实践AI自动剪辑生成视频探索实践干货 | 高频多因子存储的最佳实践那年火车上的故事 (上集)(十五)金星撞火星中国工商银行基于eBPF技术的云原生可观测图谱探索与实践Java对象拷贝原理剖析及最佳实践邓拓吴晗廖沫沙的《燕山夜话》, 老毛还真没冤枉他仨直播预告:动力电池的冰与火之歌,最佳实践方向在哪里?留学被处分需要申诉,这种方式最失败?!零信任策略下K8s安全监控最佳实践(K+)商学院|“大风流创新”:长江商学院的实践与探索年度“ESG最佳实践机构”碧桂园创投:CVC的双碳硬思考创业邦2022中国创投机构ESG最佳实践奖重磅发布!京东位列2022新型实体企业100强第二,创造供应链数实融合最佳实践字节跳动在 Rust 微服务方向的探索和实践 | QCon为业务场景打造技术矩阵,网易智企畅谈融合通信与 AI 商业化最佳实践 | Q推荐聊一聊分布式锁的设计模型内核代码量不到一万行、GitHub star超5k,国产开源物联网操作系统TencentOS Tiny的探索与实践一年 100% 云原生化,众安保险架构演进的探索与实践 | 卓越技术团队访谈录构建下一代万亿级云原生消息架构:Apache Pulsar 在 vivo 的探索与实践C++ 类设计和实现的十大最佳实践Kubernetes上千规模Pod最佳实践vivo 云原生容器探索和落地实践 | Q推荐七大策略规模以上机构11月业绩快报:主观股票多头策略本月表现最佳,同比10月涨幅9.38%大厂面试必问的设计模式,看这一篇就够了渔歌子(2):一朵云来轻雨淋【广发策略戴康团队】内外资动向寻迹行业配置线索——周末五分钟全知道(10月第4期)突围电商大促场景,得物在高可用上的探索与实践 | 卓越技术团队访谈录使用现代Java调整经典设计模式树老叶嫩----枯木逢春(温哥华一周散记/多图)期待值++!上海乐高乐园概念设计模型首次公布也假借贾屎尿诗一回服务 50+ 业务线,Apache Pulsar 在科大讯飞 SRE 的探索与实践
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。