关于设计模式:策略模式初探

34次阅读

共计 6658 个字符,预计需要花费 17 分钟才能阅读完成。

引入

其实介绍设计模式自身并不难,难的在于其该如何在具体的场景中适合的应用,上周在介绍了 Shiro 之后,我本来打算趁热打铁介绍一下责任链模式,然而责任链模式自身并不简单,咱们接触的也不少,比方拦截器链,过滤器链,每个拦截器和过滤都有机会解决申请。

但在怎么的场景中能够引入责任链模式呢, 我其实也在查对应的材料,在 B 站上看了不少该模式的视频,然而底下的评论倒不是一边到的齐全叫好,底下的评论倒是说这是适度设计,不如策略模式,所以咱们还是要认分明一件事件,设计模式能够帮忙咱们解决一些问题,就像做饭的酱油,适合的应用可能让咱们的菜变的更美味,然而如果你用不好,酱油放多了或者有些菜不该放酱油,做的菜就会不好吃。这一点咱们能够在 Shiro(Java 畛域的平安框架) 对 Java 的加密库设计的评估,能够一窥二三。

The JDK/JCE’s Cipher and Message Digest (Hash) classes are abstract classes and quite confusing, requiring you to use obtuse factory methods with type-unsafe string arguments to acquire instances you want to use.

JDK 的明码和 MD5 相干的类是十分形象而且让人十分困惑的,要求开发者应用类型不平安的 String 参数中从简略工厂模式中获取对应的加密实例,这是十分愚昧的。

PS: 我感觉 Shiro 吐槽的不无道理,上面是 JDK 中应用 MD5 算法和 Shiro 中应用 MD5 算法的比照,大家能够领会下,兴许就会明确为什么 Shiro 的设计者吐槽 JDK 在加密库的设计是愚昧的。

// JDK 中的
private static void testMD5JDK() {
        try {
            String code = "hello world";
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] targetBytes = md.digest(code.getBytes());
            System.out.println(targetBytes);
        } catch (NoSuchAlgorithmException e) {e.printStackTrace();
        }
}
// 这是 Shiro 的, 我集体感觉 Shiro 的设计更好
private static void testMD5Shiro() {String hex = new Md5Hash("hello world").toHex();
    System.out.println(hex.getBytes());
}

所以我写设计模式的时候基本上会花很大的篇幅介绍这种模式的理念、以及利用场景,我还将 UML 图逐了进来,我不认为 UML 图有助于对设计模式的了解,因为看懂 UML 图自身又须要肯定的学习老本, 一般的图一样能表白设计模式的理念,并且不须要更多的预知概念。

为了介绍策略模式,咱们要引入这样一个场景 (每种设计模式都有本人的场景),假如你有一堆代码 if else, 每个判断对应的又是一个执行办法,这些办法目前曾经很宏大了, 像上面这样:

public class StrategyDemo {public void strategy(){if ( 条件 A){doA();
        }else if (条件 B){doB();
        }else if (条件 C){doC();
        }else if (条件 D){doD();
        }else if (条件 E){doE();
        }
    }
}

这里咱们又假设满足条件的办法解决的业务曾经不少了, 这个 StrategyDemo 的文件曾经很可观了,而且新的需要还在继续涌入,也就是说还会一直的退出 else if, 那该怎么升高 StrategyDemo 这个类的复杂度呢,一个文件太大也不利于前面人的保护,减少其余开发者的浏览老本,咱们能够将其类比到微服务架构上, 业务在演进的过程中,代码量一直的回升,服务须要面对的申请量也在一直回升,为了放慢开发速度、加强服务的申请能力,咱们将单体服务拆分成了微服务:

咱们这里简略介绍一下 Module,JDK 9 之前,JDK 原生给咱们提供的治理代码的单元是包,包外面放类,咱们借助 maven 的父子工程来实现模块化概念:

实质上也是一种拆分,咱们的本能就是喜爱简略的货色,那如何升高 StrategyDemo 这个类的复杂度呢,将这个类的体积减小一点呢,策略模式给出的答案是更进一步的繁多职责,每个类摊派一种办法,这样就将复杂度摊派到几个类身上,那具体怎么做呢?将判断和行为定义抽取到父类中,哪个子类满足对应的条件,由哪个子类来执行。一个比拟经典的例子就是对接各种短信平台,不同的短信平台有不同的行为,下面的 if else 显然是不利于咱们扩大的,这正是策略模式大显神通的场景:

public abstract class AbstractSmsStrategy {

    /**
     * 不同短信平台的发送短信的形式不一样
     * 有的给 sdk, 有的用 HTTP, 所以这里用形象办法
     * @return
     */
    public abstract boolean sendSmsStrategy(String mobile,String content);

    /**
     * support 相当于 if, 哪个子类满足该条件, 哪个子类发送短信
     * @return
     */
    public abstract boolean support(String name);
}

public class AliSmsStrategy extends AbstractSmsStrategy{

    /**
     * 对应的办法
     * @return
     */
    @Override
    public boolean sendSmsStrategy(String mobile,String content) {System.out.println("阿里发送短信平台胜利.....");
        return false;
    }

     @Override
    public boolean support(String name) {return "ali".equals(name);
    }
}

public class TencentSmsStrategy extends AbstractSmsStrategy{
    @Override
    public boolean sendSmsStrategy(String mobile, String content) {System.out.println("腾讯发送短信平台胜利.....");
        return false;
    }

    @Override
    public boolean support(String name) {return "tencent".equals(name);
    }
}
public class SmsStrategyContext {

    public AbstractSmsStrategy abstractSmsStrategy;

    public SmsStrategyContext(AbstractSmsStrategy abstractSmsStrategy) {this.abstractSmsStrategy = abstractSmsStrategy;}

    public  boolean sendSms(String mobile,String content){boolean result = abstractSmsStrategy.sendSmsStrategy(mobile, content);
        return result;
    }
}
public class StrategyDemo {public static void main(String[] args) {AbstractSmsStrategy abstractSmsStrategy = new AliSmsStrategy();
        SmsStrategyContext smsStrategyContext = new SmsStrategyContext(abstractSmsStrategy);
        smsStrategyContext.sendSms("","");
    }
}

大多数讲策略模式其实都是这么讲的,定义具体的形象策略由子类来实现, 而后再 new 出一个具体的策略,给上下文,由上下文来执行具体的发短信策略。

那我为啥要通过一个上下文来调对应的办法, 我间接用形象的基类调对应的策略办法不行吗?像上面这样:

public class StrategyDemo {public static void main(String[] args) {AbstractSmsStrategy abstractSmsStrategy = new AliSmsStrategy();
        abstractSmsStrategy.sendSms("","");
    }
}

那是因为这个上下文在以后的场景下目前还是比拟多余的, 上下文在这个模式中就是屏蔽人和具体策略的交互, 如果你对接过短信平台,你会发现短信平台,短信平台通常还要指定地址、账号、明码。那么咱们能够在上下文中加载一些配置或者做一些预处理,存储短信的发送日志,上下文存在的意义就是尽可能的屏蔽加载策略的细节。咱们能够将上下文进行革新,大家能够在这个例子中领会到上下文存在的意义。

public class SmsStrategyContext {

    public AbstractSmsStrategy abstractSmsStrategy;

    public SmsStrategyContext(AbstractSmsStrategy abstractSmsStrategy) {this.abstractSmsStrategy = abstractSmsStrategy;}

    /**
     * 其实对应的操作还有很多。* 发短信的时候, 咱们个别是将模板存储在数据库中.
     * 业务方在调用的时候, 咱们从数据库中取出对应的模板。* 须要发送连贯的时候, 还须要将长链接解决成短链接。* 你看上下文的作用体现进去了吧。* 通常发送短信还有异步解决, 所以咱们这里还能够再做一个线程池,
     * 这样看上下文的作用是不是体现进去了。* @param mobile
     * @param content
     * @return
     */
    public  boolean sendSms(String mobile,String content){
        // 生成短信日志, 这里伪装有一个短信日志表.
        MessageLog messageLog = new MessageLog();
        boolean result = abstractSmsStrategy.sendSmsStrategy(mobile, content);
        // 这里伪装执行对应的入库操作
        return result;
    }
}

留神这个上下文不用肯定要严格依照设计模式中肯定是要注入对应的策略, 而后来调用,咱们要了解,设置上下文的目标是尽可能的屏蔽执行对应策略的细节,让调用方仅提供手机号和模板 ID,就能够实现短信的发送。模板形式其实还能够做进一步的变型,在这个例子中咱们还要显式的 new 进去对应的策略,那不能不能 new 啊,我给你一个标识符,你来依据标识符来加载对应的策略不行吗?让调用方少干点事,无效的解耦合。那其实下面的策略模式能够革新成上面这样:

public class SmsStrategyContextPlus {private static final List<AbstractSmsStrategy> SMS_STRATEGY_LIST = new ArrayList<>();

    /**
     * 加载所有的短信策略
     */
    static {AbstractSmsStrategy aliSmsStrategy = new AliSmsStrategy();
        AbstractSmsStrategy tencentSmsStrategy = new TencentSmsStrategy();
        SMS_STRATEGY_LIST.add(aliSmsStrategy);
        SMS_STRATEGY_LIST.add(tencentSmsStrategy);
    }
    /**
     * 略去产生日志, 长链转短链。* 线程池等操作
     * @param mobile
     * @param templateKey
     * @param platform 哪个平台, 如果怕调用方写错, 其实这里能够做成枚举值
       * platform 也能够从配置中加载, 在前台选中启用哪个短信平台。* 这里其实能够做的很灵便。* @return
     */
    public boolean sendSms(String mobile,String templateKey,String platform){
        boolean sendResult = false;
        for (AbstractSmsStrategy abstractSmsStrategy : SMS_STRATEGY_LIST) {if (abstractSmsStrategy.support(platform)){sendResult = abstractSmsStrategy.sendSmsStrategy(mobile,templateKey);
            }
        }
        return sendResult;
    }
}

在 Spring 中其实咱们能够这么用:

@Component
public class SmsStrategyContextSpringDemo {
    @Autowired
    private List<AbstractSmsStrategy> strategyList ;

    public boolean sendSms(String mobile,String templateKey,String platform){
        boolean sendResult = false;
        for (AbstractSmsStrategy abstractSmsStrategy : strategyList) {if (abstractSmsStrategy.support(platform)){sendResult = abstractSmsStrategy.sendSmsStrategy(mobile,templateKey);
            }
        }
        return sendResult;
    }
}

留神这个上下文的作用,对调用方屏蔽掉执行对应的策略的细节,不用太过拘泥其实现,我集体的认识是只有是对调用方屏蔽了执行对应策略的细节,就是一个 Context。

在 shiro 中的应用

下面讲的策略模式其实在 Java 畛域外面的平安框架 Shiro 中也有所体现,Shiro 帮咱们写好了登录代码,所谓的登录就是拿前台提交的登录信息和数据源中的用户信息进行比对,数据源其实有很多种, JDBC、LDAP 等等。Shiro 形象出了 Realm 接口来对立的从数据源中获取信息,Shiro 反对多数据源,那 Shiro 是怎么依据对应的数据源来加载用户信息的呢?其实也是通过策略形式来散发的, AuthenticatingRealm 有两个形象办法:

  • doGetAuthenticationInfo 交给对应的 realm 来实现获取登录信息
  • supports 是否由以后 realm 来获取数据源

总结一下

咱们通常心愿类小一点,这样浏览起来就快一点,如果你有在一个类外面放了很多判断,这个类曾经很宏大了,无妨尝试用策略模式来进行散发,来升高代码的复杂度,这样保护起来也容易,或者就是预判,像是零碎外面接入短信平台一样,短信平台商有很多,即便你的零碎曾经反对了出名的短信服务商了,然而还不够,客户心愿用他本人的短信服务商,防止每接入一个短信服务商都要加一个判断,咱们能够采取策略形式,接入一个短信服务商,继承父类,实现具体的发短信操作即可,通用操作像生成短信日志、异步发送咱们放在上下文外面进行操作,也不必编写反复代码,要留神领会上下文的思维,上下文存在的意义在于升高客户端调用的难度,屏蔽加载对应策略的细节,简略点尽量简略点。某种程度上,我了解设计模式是一种预判或者升高代码复杂度的一种形式,在策略模式中就是散发代码,将满足条件的行为挪动到对应的子类中,尽可能的缩小改变,策略模式的判断,以后 if else 很多了,即便不会再减少判断,在繁多文件中拥挤太多代码,也让读懂代码的人头疼,为了升高复杂度,我将这些代码依照繁多职责进行散发。预判的是以后是只有一个 if else,然而随着业务的倒退,会接入越来越多的短信平台,所以这里用上下文做通用操作,接入一个短信平台实现对应的办法,缩小代码的改变量,让代码的可维护性和可浏览性更高。

参考资料

  • Java 9 的模块化 – 壮士断 ” 腕 ” 的涅槃 https://zhuanlan.zhihu.com/p/…

正文完
 0