JAVA设计模式之策略模式

前言

本系列文章参考《设计模式之禅》、菜鸟教程网以及网上的一些文章进行归纳总结,并结合自身开发应用。设计模式的命名以《设计模式之禅》为准。

设计模式仅是一些开发者在日常开发中的编码技巧的汇总并非固定不变,可根据项目业务实际情况进行扩展和应用,切不可被这个束缚。更不要为了使用而使用,设计模式是一把双刃剑,过度的设计会导致代码的可读性下降,代码的体积增加。

系列文章不会详细介绍设计模式的《七大原则》,也不会对设计模式进行分类。这样只会增加学习和记忆的成本,也会导致使用时的思想固化,总在想这么设计是否符合xx原则,是否是xx设计模式,xx模式是什么类型等等,不是本系列文章的所希望看到的,目标只有一个,结合日常开发分享自身的应用,以提供一种代码优化的思路。

学习然后忘记,也许是一种最好的方式。

就像俗话说的那样:天下本没有路,走的人多了,就变成了路。在我看来,设计模式也一样,它并非是一种定律,而是前辈们总结下来的经验,我们学习并结合实际加以利用,而不是生搬硬套。

定义

官腔:定义一组算法,将每个算法都封装起来,并且使它们之间可以互换。

人话:有一个公共的逻辑接口或抽象类,且有一个控制类对实现了公共逻辑接口的类进行控制,在引用时知道该调用哪一个接口的策略类。

应用场景

样例1

为了方便没有了解过设计模式的小白快速了解,这里放一个网上到处都是的计算器模型。

在一个简单的计算器中我们需要关注计算器的运算符即可。我们假定这个计算器非常非常简单,仅支持加减乘除。

先定义一个运算符接口。

public interface IOperator {
    /**
     * 计算逻辑
     * @return 结果
     */
    int doOperator(int num1, int num2);
}
复制代码

代码比较简单,只做整形的计算。

加法实现:

public class AddOperator implements IOperator{

    @Override
    public int doOperator(int num1, int num2) {
        return num1 + num2;
    }
}

复制代码

乘法实现:

public class MultiplyOperator implements IOperator{
    @Override
    public int doOperator(int num1, int num2) {
        return num1 * num2;
    }
}

复制代码

减法和除法就不写了,参考上面即可。

上面这些IOperator的实现即为不同的运算策略。光有策略还不行,我们需要一个控制类,对其进行统一管理。所有的引用通过这个管理类来实现。管理类的实现方式有很多种,千万别局限于网上的一两个demo。

方式一:

通过构造函数将策略(IOperator)注入到管理类属性中,然后直接调用exec执行即可。

demo中相当于构建了一个加法的控制类,然后再执行传参和运算。

//控制类
public class StrategyManager_1 {
    private final IOperator operator;

    public StrategyManager_1(IOperator operator) {
        this.operator = operator;
    }
    
    public int exec(int num1, int num2){
        return operator.doOperator(num1, num2);
    }
    
    //引用方式
    public static void main(String[] args) {
        AddOperator operator = new AddOperator();
        StrategyManager_1 manager_1 = new StrategyManager_1(operator);
        int exec = manager_1.exec(1, 2);
        System.out.println(exec);
    }
}
复制代码

方式二:

还可以使用switch语句,通过输入的运算符进行匹配计算。

public class StrategyManager_2 {

    public static int exec(int num, int num1, String operator) {
        switch (operator) {
            case "+" :
                return new AddOperator().doOperator(num, num1);
            case "*" :
                return new MultiplyOperator().doOperator(num, num1);
            default:
                return 0;
        }
    }

    public static void main(String[] args) {
        StrategyManager_2.exec(1, 2, "+");
    }
}
复制代码

这种方式不需要使用者去构建策略对象,有点类似于简单的工厂模式,只需要传入一个标识即可,且调用方式更加简单,个人更推荐这种方式。实际开发中也不建议让使用者自己去封装策略对象,即增加了调用者的学习成本,也可能会引起各种意想不到的错误。要考虑后人是在你的基础上开发的,他们不知道也不需要知道你的策略对象,因为策略对象是你的业务的封装,而使用者并不知道。

真实情况下的计算器,难道要用户去创建一个处理加法的策略对象AddOperator吗?显然不可能,用户只关系输入的数字、运算符和结果。

实现的方式可能还有很多,待各位自行发现。

样例2

跟大家分享一下,在一个springboot项目中,如何编写一个策略模式的接口。

最近正好写过一个对外开放的接口,用于接受其他项目同步来的用户信息。但是不同的项目同步来的用户信息的结构和内容是不同的,有些需要进行定制业务开发。

以此背景为例,假设有两个项目(A 和 B)需要同步数据给我。

条件:

1.A项目传入的仅有用户姓名和手机号,B项目传入的仅有身份证和姓名。

2.A项目需要接受完成后同步发送短信,B项目需要同步的时候根据身份证计算年龄和生日。

3.任何一种情况同步前和完成后都需要记录日志。

1.创建入参对象

//剥离出来公共的参数
@Data
public class ReceiveBean {
    private String name;
    /**
    * 来源
    */
    private int source;
}

//B项目同步来的参数
@EqualsAndHashCode(callSuper = true)
@Data
public class BReceiveBean extends ReceiveBean{
    private String idNo;
}

//A项目同步来的参数
@EqualsAndHashCode(callSuper = true)
@Data
public class AReceiveBean extends ReceiveBean{
    private String phone;
}
复制代码

实体bean中使用了lombok相关的注解代替了显示的get/set方法。

ReceiveBean中有一个关键的source字段用来区别渠道的来源,可用其他的方式代替。

2.定义顶层的策略接口

public interface IReceiveService {

    /**
     * 接受的参数为json格式的字符串
     * 由调用者进行转换
     * @param bean 入参
     */
    void receive(ReceiveBean bean);

}
复制代码

使用字符串或Map对象能提供更好的兼容性,也可以在内部进行转换,这里为了方便外部的引用将转换的过程转交给调用者。

3.抽象层面接口实现

@SuppressWarnings("unused")
public abstract class AbstractReceiveService implements IReceiveService{

    //可通过覆盖这个方法定制接受之前的处理,替换bean的属性
    protected void receivePre(ReceiveBean bean) {
        //记录日志
    }

    //通过覆盖这个方法定制接受之后的处理
    //这个可能会不满足,若需要的话可以添加一个map类型的参数,用来保存处理过程中产生数据
    protected void receiveAfter(ReceiveBean bean) {
        //记录日志
    }

    //需要被重写,各类型的定制服务
    protected abstract void doAction(ReceiveBean bean);

    @Override
    public void receive(ReceiveBean bean) {
        //业务开始处理之前的操作
        receivePre(bean);
        //业务开始处理
        doAction(bean);
        //业务处理之后的操作
        receiveAfter(bean);
    }
}
复制代码

抽象类内部实现了receive方法,这里要看具体的业务,虽然有定制的服务,但总体逻辑相同的可以这么实现,若差别较大,可在子类中覆盖receive方法,自行实现,否则就默认走相同的流程。

整体还算比较简单,receive方法内进行了参数的转换,并且调用了receivePrereceiveAfter进行业务前后的处理,两个方法内部都做了日志保存,这属于默认操作,也可以通过覆盖这两个方法进行业务和参数的定制,可以影响后续的处理。doAction方法可以酌情处理,我将其作为一个抽象方法,要求子类必须去实现各自的业务处理操作,也可以和receivePrereceiveAfter连个方法一样提供默认实现。

拿其中一个A项目的实现类看一下:

@Service
public class AReceiveService extends AbstractReceiveService{

    @Override
    protected void doAction(ReceiveBean bean) {
        //保存记录
        // 使用的时候需要对bean进行强转
        AReceiveBean ab = (AReceiveBean) bean;
    }


    @Override
    protected void receivePre(ReceiveBean bean) {
        super.receivePre(bean);
        //记录日志的同时进行其他操作
        // 使用的时候需要对bean进行强转
        AReceiveBean ab = (AReceiveBean) bean;
    }

    @Override
    protected void receiveAfter(ReceiveBean bean) {
        super.receiveAfter(bean);
        //发送短信
        // 使用的时候需要对bean进行强转
        AReceiveBean ab = (AReceiveBean) bean;
    }
}
复制代码

内部可选择性的覆盖方法,若非必要的话。

4.控制类

@SuppressWarnings("unused")
public enum ReceiveSourceEnum {
    A(1, "aReceiveService") {
        @Override
        public ReceiveBean coverJson(String json) {
            return JSON.parseObject(json, AReceiveBean.class);
        }
    },
    B(2, "bReceiveService") {
        @Override
        public ReceiveBean coverJson(String json) {
            return JSON.parseObject(json, BReceiveBean.class);
        }
    };

    private final int source;

    private final String serviceName;

    /**
     * 在此定制转换成不同类型的bean入参
     *
     * @param json json格式入参
     * @return 结果
     */
    public abstract ReceiveBean coverJson(String json);


	//获取枚举资源
    public static ReceiveSourceEnum getWithSource(int source) {
        for (ReceiveSourceEnum sourceEnum : ReceiveSourceEnum.values()) {
            if (sourceEnum.getSource() == source) {
                return sourceEnum;
            }
        }
       throw new DemoException("source类型不存在!");
    }
	//获取服务
    public static IReceiveService getReceiveService(int source) {
        ReceiveSourceEnum sourceEnum = getWithSource(source);
        Assert.notNull(sourceEnum, "source is null");
        IReceiveService receiveService = (IReceiveService)SpringContextUtil.getBean(sourceEnum.getServiceName());
        Assert.notNull(sourceEnum, "service is null");
        return receiveService;
    }

    ReceiveSourceEnum(int source, String serviceName) {
        this.source = source;
        this.serviceName = serviceName;
    }

    public int getSource() {
        return source;
    }

    public String getServiceName() {
        return serviceName;
    }
}
复制代码

ReceiveSourceEnum作为控制类,内部提供两个静态的方法getReceiveServicegetWithSource,获取服务和枚举类型。

枚举类中定义了一个用于将json格式字符串转换成对象的抽象方法coverJson。因为接受的参数为json格式字符串,这样可以提高接口的扩展性,内部转换成对应的参数实体类方便后续操作。

getReceiveService内部通过服务名称从spring容器中换取bean。这种方式可以静态的调用服务且方法简单。

换取的方法网上有很多,这里只提供一种:

@Component
public class SpringContextUtil implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringContextUtil.applicationContext = applicationContext;
    }

    //获取applicationContext
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    //通过name获取 Bean.
    public static Object getBean(String name) {
        return getApplicationContext().getBean(name);
    }

    //通过class获取Bean.
    public static <T> T getBean(Class<T> clazz) {
        return getApplicationContext().getBean(clazz);
    }

    //通过name,以及Clazz返回指定的Bean
    public static <T> T getBean(String name, Class<T> clazz) {
        return getApplicationContext().getBean(name, clazz);
    }

}

复制代码

5.引用方法

使用springboot的单元测试。

注意:
不能使用main函数直接引用,否则会发现SpringContextUtil中的ApplicationContext永远都是null,原因是classloader导致的。

@RunWith(SpringRunner.class)
@SpringBootTest
public class TestBean {

    @Test
    public void testContextUtil() {
        ReceiveBean bean = new ReceiveBean();
        bean.setName("123");
        bean.setSource(1);
        String paramStr = JSON.toJSONString(bean);
        ReceiveBean receiveBean = coverJsonToBean(paramStr);
        ReceiveSourceEnum.getReceiveService(receiveBean.getSource()).receive(receiveBean);
    }

    private static ReceiveBean coverJsonToBean(String json){
        JSONObject jo = JSONObject.parseObject(json);
        if (!jo.containsKey("source")) {
            throw new DemoException("传入参数缺失:source");
        }
        //存在强转错误的风险
        int source = (int)jo.get("source");
        ReceiveSourceEnum sourceEnum = ReceiveSourceEnum.getWithSource(source);
        return sourceEnum.coverJson(json);
    }
}
复制代码

不过这样也可以看出来对于使用者来说,调用非常简单。若后续对接了C项目或其他项目,仅需扩展AbstractReceiveService类和ReceiveSourceEnum的类型即可。

这里仅是我的一种思路,一种参考,肯定还有其他更好的实现方式。

UML图

图片来自于菜鸟教程。(偷懒中= =)

小结

最早了解策略模式是为了优化代码中大量的if-else语句,虽然一些情况下可以通过switch语句和enum类去实现,但扩展性并不好,且代码会分散在当前业务类中,当对接方比较少时还可以勉强看。比较多的时候,就会发现,一个入口方法内,整合了大量的定制业务,阅读和维护都时分困难,并且业务类变得时分庞大。加之缺少注释说明,后人维护起来简直是噩梦级的。

策略模式下,将不同的渠道提取公共的方法置于父类中,将定制的业务在各自的子类中实现,使得整体流程时分清晰,且业务类不会无限膨胀。修改和扩展只要不涉及公共部分,可放心变更,不用担心牵一发而动全身,每种渠道的定制业务相对独立。

若对接方完全无法适用公共的部分,也不用担心,大可直接覆盖策略方法,重写业务逻辑,依然可以保证同一个入口。