SpringBoot使用策略模式彻底消除if-else

SpringBoot使用策略模式完成if-else分支跳转的优化

问题简介

 最近一直有小伙伴和同事询问楼主为什么最近没有更新博客啊,楼主无奈。主要是因为楼主最近一直在苦读的过程中,12月份完成了两本书的阅读,一本是Brain《Java并发编程实战》,另外一本则是12月29日完成的瑞·达利欧满怀真诚的著作**《原则》**一书,这本书也是之前我徒弟用获得的购书卡给我买的,这本书主要分为三个部分,一是瑞的经历,另外就是两部分瑞通过实践的原则:生活原则和工作原则,看了之后也是收获很多,自己也要在自己的生活之余创建属于自己的生活原则和工作原则,这样在遇到事情和问题时,才能有条不紊的去解决。在这里简单的摘要两段对我影响比较深的话,也算是给读者做一个推荐:

 p170用五步流程实现你的人生愿望:

  1. 有明确的目标
  2. 找到阻碍你实现这些目标的问题,并且不容忍问题、
  3. 准确诊断问题,找到问题的根源。
  4. 规划可以解决问题的方案
  5. 做一切必要的事来践行这些方案,实现成果。

p410在桥水,我们秉持的主要共同价值观是从事有意义的工作,发展有意义的人际交往,做到极度求真和极度透明,愿意以开放的心态探索严酷的现实(包括正视自身缺点),有主人翁精神,敢于追求卓越,愿意做困难但有益的事情。

p317创意择优=极度求真+极度透明+可信度加权的决策。

喜欢阅读的人,别错过这本书。

闲言少叙,本文主要是阐述如何使用策略模式彻底去除if-else分支的跳转逻辑。因此楼主先提出一个场景出来,然后根据这个场景进行逐步的解决。

 楼主在最近的工作中,有一个简单的需求,如下图所示:

SpringBoot使用策略模式彻底消除if-else_第1张图片

 模块B与模块A之间存在着许多的消息,而且消息的类型不同,数据内容也有所差异,早期的时候,消息类型较少,比如说只有三种消息,此时我们尚且可以通过if-else或者switch的方式应对,可随着项目的进展,分支越来越多,最终成为了如下的模样:

    @Async
    public void dealFuseUploadData(byte[] data) {
        JSONObject fuseUploadInfo = JSONObject.parseObject(new String(data));
        final Integer messageType = fuseUploadInfo.getInteger("type");
        if(messageType == MessageType.END_TYPE_TEST){
            webSocketService.sendMsg(fuseUploadInfo);
            return;
        }
        
        JSONObject message = fuseUploadInfo.getJSONObject("data");
        switch (messageType) {
            case MessageType.FUSE_TARGET:
                handleFuseTarget(message);
                break;
            case MessageType.DEVICE_STATE:
                handleDeviceMessage(message);
                break;
            case MessageType.TRACK:
                handleTrackMessage(message);
                break;
            case MessageType.DEVICE_OPERATION_RESPONSE:
                // 处理设备响应信息
                webSocketService.sendMsg(new DeviceOperationResponseMessageTo(message));
                break;
            case MessageType.MESSAGE:
                // 处理设备实时报文信息
                log.info("设备实时报文:"+fuseUploadInfo);
                handleDeviceRealTimeMessage(message);
                break;
            case MessageType.MESSAGE_LENGTH:
                // 处理设备报文字节数
                log.info("设备报文字节数:"+fuseUploadInfo);
                deviceService.setDeviceMessageBytes(message.getString("deviceId"), message);
                break;
            default:
                break;
        }
    }

 可以看到,这样的代码让我很痛苦,函数名dealFuseUploadData也是起初的名字,当很明显,这个函数已经承担了过多的职责,它需要知道太多的跳转接口了,而这些转换其实并非应该由函数自己来做。由于这些函数比如说handleDeviceRealTimeMessage、handleFuseTarget的提取和不断的函数提炼,导致此时该类文件也将近400多行了,这都是敏感的信号,这个函数需要重构了,需要把过多的职责拆分出去。并且这个函数的扩展性不足,当前已经有了7种消息类型,未来有8种的时候,我们首先要添加case分支,然后要在MessageType类中添加消息类型嘛,然后还要添加对应的处理过程,这显然不是符合面向对象的一种设计,我们想要的是具有可扩展性的程序实现,比如说,新添加一种类型,我们希望跳转的类不需要做任何改动,只需要添加相应的类型处理器来实现有关的处理即可。那这样的设想应该怎么实现呢?

使用多态替换条件表达式

 我们可以看到,上述的问题其实最主要的还是聚焦于条件表达式的处理,在《重构 改善既有代码的设计中》,Martin Folwer曾经在第九章专门讨论过相关的内容:

SpringBoot使用策略模式彻底消除if-else_第2张图片

 换句话说,我们用策略模式,相当于就是在使用面向对象中的多态取代条件表达式的战略目的。

SpringBoot使用策略模式彻底消除if-else_第3张图片

 因此,其实解决本问题的过程,相当于就是我们要提炼出一个继承体系来了。

策略模式描述

 策略模式主要组成如下图所示:

SpringBoot使用策略模式彻底消除if-else_第4张图片

  • Strategy Strategy角色负责决定实现策略所必需的接口(API)。在示例程序中,由strategy接口扮演此角色。 特定策略的抽象。
  • ConcreteStrategy(具体策略)ConcreteStrategy角色负责实现Strategy角色的接口(API),即负责实现具体的策略(战略、方向、方法和算法)。
  • Context(上下文,运行特定策略的类。)负责使用Strategy角色。Context角色保存了ConcreteStrategy角色的实例,并使用ConcreteStrategy角色去实现需求(总之,还是要调用Strategy角色的接口(API))。可以认为由Context处理了跳转的功能。
  • 策略模式的目的:策略模式定义了一系列算法,封装了每个算法,并使它们可以互换。

在Java设计模式第p56页,辛格说:“行为模式的一个特定情况,是我们需要改变解决一个问题与另一个问题的方式。”但正如开闭原则所说,改变是不好的,而扩展是好的。因此,我们可以将两块代码封装在一个类中,而不是用一部分代码替换另一部分代码。然后可以创建代码所需要依赖累的抽象。

问题

假设有这么一个需求:

一个电商系统,当用户消费满1000 金额,可以根据用户VIP等级,享受打折优惠。根据用户VIP等级,计算出用户最终的费用。

  • 普通会员 不打折
  • 白银会员 优惠50元
  • 黄金会员 8折
  • 白金会员 优惠50元,再打7折

最简答的编码实现如下:

private static double getResult(long money, int type) {

    double result = money;

    if (money >= 1000) {
        if (type == UserType.SILVER_VIP.getCode()) {

            System.out.println("白银会员 优惠50元");
            result = money - 50;
        } else if (type == UserType.GOLD_VIP.getCode()) {

            System.out.println("黄金会员 8折");
            result = money * 0.8;
        } else if (type == UserType.PLATINUM_VIP.getCode()) {

            System.out.println("白金会员 优惠50元,再打7折");
            result = (money - 50) * 0.7;
        } else {
            System.out.println("普通会员 不打折");
            result = money;
        }
    }

    return result;
}

重构启程-引入策略继承体系

这是我们功能的基础,我们从这个进一步进行优化,但不论怎么说,这是第一版,功能已经实现,只是代码的质量需要提升。

public interface Strategy {

    // 计费方法
    double compute(long money);
}

// 普通会员策略
public class OrdinaryStrategy implements Strategy {

    @Override
    public double compute(long money) {
        System.out.println("普通会员 不打折");
        return money;
    }
}

// 白银会员策略
public class SilverStrategy implements Strategy {

    @Override
    public double compute(long money) {

        System.out.println("白银会员 优惠50元");
        return money - 50;
    }
}

// 黄金会员策略
public class GoldStrategy implements Strategy{

    @Override
    public double compute(long money) {
        System.out.println("黄金会员 8折");
        return money * 0.8;
    }
}

// 白金会员策略
public class PlatinumStrategy implements Strategy {
    @Override
    public double compute(long money) {
        System.out.println("白金会员 优惠50元,再打7折");
        return (money - 50) * 0.7;
    }
}

上述从if-else到Strategy继承体系的过程,是通过考察if-else分支中的主要过程,通过money获得result的过程,提炼成了Strategy抽象策略类。

使用具体策略类

private static double getResult(long money, int type) {

    double result = money;

    if (money >= 1000) {
        if (type == UserType.SILVER_VIP.getCode()) {

            result = new SilverStrategy().compute(money);
        } else if (type == UserType.GOLD_VIP.getCode()) {

            result = new GoldStrategy().compute(money);
        } else if (type == UserType.PLATINUM_VIP.getCode()) {

            result = new PlatinumStrategy().compute(money);
        } else {
            result = new OrdinaryStrategy().compute(money);
        }
    }

    return result;
}

此处很关键,要比较敏感,if-else都做了什么。善于发现美的眼睛很重要。

发现重复-程序员智慧的双眼

SpringBoot使用策略模式彻底消除if-else_第5张图片

拆分获取策略和计算的过程

首先,这个过程还是不符合只做一件事。把获取计算策略和计算的过程耦合在一起了,因此,我们进行再一步的分离。把获取策略和计算分离,使得函数更加符合单一职责

private static double getResult(long money, int type) {

    if (money < 1000) {
        return money;
    }

    Strategy strategy;

    if (type == UserType.SILVER_VIP.getCode()) {
        strategy = new SilverStrategy();
    } else if (type == UserType.GOLD_VIP.getCode()) {
        strategy = new GoldStrategy();
    } else if (type == UserType.PLATINUM_VIP.getCode()) {
        strategy = new PlatinumStrategy();
    } else {
        strategy = new OrdinaryStrategy();
    }

    return strategy.compute(money);
}

上述代码使用了卫语句,这并不重要,我们关注一下if-else分支,可以看到,此时就把策略的获取和分支的计算给拆分开了。

上述if-else分支,可以继续提炼成函数,根据类型获取策略

private static double getResult(long money, int type) {
	Strategy strategy = getStrategy(type);
    return strategy.compute(money);
}

private Strategy getStrategy(UserType type) {
	Strategy strategy;

    if (type == UserType.SILVER_VIP.getCode()) {
        strategy = new SilverStrategy();
    } else if (type == UserType.GOLD_VIP.getCode()) {
        strategy = new GoldStrategy();
    } else if (type == UserType.PLATINUM_VIP.getCode()) {
        strategy = new PlatinumStrategy();
    } else {
        strategy = new OrdinaryStrategy();
    }
    return stratety;
}

上述代码就相对来说比较简单来了,我们甚至可以在getResult函数中使用内联方式,因为代码足够简单嘛,获取了strategy,然后直接调用了,没有必要独占一个变量名,没有意义(我是Martin Folwer的忠实粉丝

private static double getResult(long money, int type) {
    return getStrategy(type).compute(money);
}

终极移除if-else

SpringBoot使用策略模式彻底消除if-else_第6张图片

 但是if-else仍然存在,接下来就是关键的进一步优化了,使用了工厂模式和策略模式整合在一起。请认真体会。

private Strategy getStrategy(UserType type) {
	Strategy strategy;

    if (type == UserType.SILVER_VIP.getCode()) {
        strategy = new SilverStrategy();
    } else if (type == UserType.GOLD_VIP.getCode()) {
        strategy = new GoldStrategy();
    } else if (type == UserType.PLATINUM_VIP.getCode()) {
        strategy = new PlatinumStrategy();
    } else {
        strategy = new OrdinaryStrategy();
    }
    return stratety;
}

 作为程序员,走到了这一步,其实我们可以再仔细观察,上述的if-else的主要结构主要是通过type获取具体的策略模式,这是一种映射关系MappingMap就是映射数据结构结构),而映射关系的图示关系如下图所示:

SpringBoot使用策略模式彻底消除if-else_第7张图片

由于我们在实际情况下,策略模式变化较少,而且数量固定,因此,我们可以直接生成所有的策略模式,这样在之后只需要直接调用这些策略模式即可。Map是直观的,接下来的代码采用了把类型信息放置在具体类中,更加符合面向对象嘛,然后使用List结构存储系统中存在的所有具体策略

public interface Strategy {

    double compute(long money);

    // 返回 type变化,工冻加了类型返回
    int getType();
}


public class OrdinaryStrategy implements Strategy {

    @Override
    public double compute(long money) {
        System.out.println("普通会员 不打折");
        return money;
    }

    // 添加 type 返回
    @Override
    public int getType() {
        return UserType.ORDINARY.getCode();
    }
}

public class SilverStrategy implements Strategy {

    @Override
    public double compute(long money) {

        System.out.println("白银会员 优惠50元");
        return money - 50;
    }

    // type 返回
    @Override
    public int getType() {
        return UserType.SILVER_VIP.getCode();
    }
}

....省略剩下 Strategy

此时,我们可以做的是把所有的策略,因此提炼出策略工厂类,如下:

public class StrategyFactory {

    private Map<Integer, Strategy> map;

    /*
    构造器,其实实现了从List到Map的转换
    */
    public StrategyFactory() {

        List<Strategy> strategies = new ArrayList<>();

        strategies.add(new OrdinaryStrategy());
        strategies.add(new SilverStrategy());
        strategies.add(new GoldStrategy());
        strategies.add(new PlatinumStrategy());
        strategies.add(new PlatinumStrategy());

        // 看这里 看这里 看这里!哈哈哈,好的好的。
        map = strategies.stream()
            .collect(Collectors.toMap(Strategy::getType, strategy -> strategy));

        /* 等同上面
        map = new HashMap<>();
        for (Strategy strategy : strategies) {
            map.put(strategy.getType(), strategy);
        }*/
    }

    public static class Holder {
        public static StrategyFactory instance = new StrategyFactory();
    }

    public static StrategyFactory getInstance() {
        return Holder.instance;
    }

    public Strategy get(Integer type) {
        return map.get(type);
    }
}

因此,至此getStrategy函数则可以进一步重构:

private Strategy getStrategy(UserType type) {
	Strategy strategy;

    if (type == UserType.SILVER_VIP.getCode()) {
        strategy = new SilverStrategy();
    } else if (type == UserType.GOLD_VIP.getCode()) {
        strategy = new GoldStrategy();
    } else if (type == UserType.PLATINUM_VIP.getCode()) {
        strategy = new PlatinumStrategy();
    } else {
        strategy = new OrdinaryStrategy();
    }
    return stratety;
}

我们把根据类型获取具体策略的的过程委托给策略工厂类StrategyFactory,则getStrategy函数重构为:

private Strategy getStrategy(UserType type) {
	return StrategyFactory.getInstance().get(type);
}

至此,我们便使用了策略模式完成了对于代码的重构,可以看到已经没有了if-else分支。

最终效果如下:

private static double getResult(long money, int type) {

    if (money < 1000) {
        return money;
    }

    Strategy strategy = StrategyFactory.getInstance().get(type);

    if (strategy == null){
        throw new IllegalArgumentException("please input right type");
    }

    return strategy.compute(money);
}

当然上述的函数过程,采用了对于getStrategy(type)直接内联掉了,因为只有行语句调用。

问题解答

 其实上述的过程已经完美的解决了if-else分支的问题,但由于我们使用了Spring Boot框架,基于这个框架,我们还可以再进一步优化去除策略工厂的手动创建过程

引入继承体系

我们对于消息处理器引入继承体系MessageHandler

SpringBoot使用策略模式彻底消除if-else_第8张图片

 上图中,并没有展示所有的子类体系,但这并不妨碍。首先MessageHandler是一个接口,而其子类在实现该接口的同时,同时为子类添加了注解**@Component**,这相当于省略了StrategyFactory类中的构造器的执行过程,因为IOC容器会自动为我们生成组件。

引入@Componet实现具体策略

package com.cetc52.situation.service;

import com.alibaba.fastjson.JSONObject;
import com.cetc52.situation.common.ZmqMessageType;
import com.cetc52.situation.domain.dto.Message;
import com.cetc52.situation.domain.entity.DeviceEntity;
import com.cetc52.situation.domain.entity.DeviceStatusEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Optional;

/**
 * 设备状态消息处理器
 *
 * @author songquanheng
 * 2021/12/2 16:08
 */
@Component(ZmqMessageType.DEVICE_STATE)
public class DeviceStateMessageHandler implements MessageHandler {
    @Autowired
    private DeviceService deviceService;

    @Autowired
    private WebSocketService webSocketService;

    @Override
    public Message toMessage(JSONObject message) {
        DeviceStatusEntity deviceMsg = getDeviceStatusEntity(message);
        return Message.newDeviceStatusMessage(deviceMsg);

    }

    /**
     * 处理设备状态报文消息
     * 1. 判断设备状态是否发生变化。
     * 2. 若变化则更新数据库中的设备状态信息,并使用websocket通知页面
     *
     * @param message
     */
    @Override
    public void handle(JSONObject message) {
        DeviceStatusEntity deviceMsg = getDeviceStatusEntity(message);
        Optional<DeviceEntity> deviceEntityOptional = getDeviceEntity(deviceMsg);
        if (!deviceEntityOptional.isPresent()) {
            return;
        }
        sendMessage(toMessage(message));
        DeviceEntity deviceEntity = deviceEntityOptional.get();
        updateDeviceStatus(deviceMsg, deviceEntity);
    }

    /**
     * 根据设备状态报文信息更新数据库中设备实体
     *
     * @param deviceMsg    最新上报的设备状态报文信息
     * @param deviceEntity 数据库中最新的设备实体
     */
    public void updateDeviceStatus(DeviceStatusEntity deviceMsg, DeviceEntity deviceEntity) {
        deviceEntity.setDeviceStatus(deviceMsg.getDeviceStatus());
        deviceService.updateDeviceStatusAndUpdateTime(deviceEntity);
    }

    @Override
    public void sendMessage(Message message) {
        webSocketService.sendMsg(message);
    }


    /**
     * 根据上传的设备状态信息获取设备状态实体类
     *
     * @param message 融合上报的设备状态消息
     * @return
     */
    private DeviceStatusEntity getDeviceStatusEntity(JSONObject message) {
        return message.toJavaObject(DeviceStatusEntity.class);
    }

    /**
     * 根据设备状态消息获取消息对应的设备实体
     *
     * @param deviceMsg 设备状态消息
     * @return
     */
    private Optional<DeviceEntity> getDeviceEntity(DeviceStatusEntity deviceMsg) {
        DeviceEntity deviceEntity = deviceService.findByDeviceId(deviceMsg.getDeviceId());
        return Optional.ofNullable(deviceEntity);
    }

}

笔者要为这个类进行如下的阐述:

  • 因为使用了@Component注解,因此该类的实例会自动生成
  • 因为IOC中会有该Bean,因此,我们可以使用**@Autowired**注入由ioc管理的其他Bean
  • @Component(ZmqMessageType.DEVICE_STATE)
@Component(ZmqMessageType.DEVICE_STATE)
public class DeviceStateMessageHandler implements MessageHandler {...}

 这是我们使用了一种简单的技巧,直接把ioc管理的这个Bean命名成其类型码了,这也是一种取巧的手段,并不华丽,但比较实用。

另一个具体策略类

package com.cetc52.situation.service;

import com.alibaba.fastjson.JSONObject;
import com.cetc52.situation.common.ZmqMessageType;
import com.cetc52.situation.domain.dto.Message;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * 一轮目标数据发送完成消息处理器
 *
 * @author songquanheng
 * 2021/12/2 18:27
 */
@Component(ZmqMessageType.END_ROUND_TRANSMIT)
public class EndRoundMessageHandler implements MessageHandler {
    @Autowired
    private WebSocketService webSocketService;

    @Override
    public Message toMessage(JSONObject message) {
        return Message.newCleanMessage(message.toJSONString());
    }

    @Override
    public void handle(JSONObject message) {
        sendMessage(toMessage(message));
    }

    @Override
    public void sendMessage(Message message) {
        webSocketService.sendMsg(message);
    }
}

其他具体策略类,不再赘述。

工厂类

 在这里,再次利用Spring Boot的自动注入,这个则是自动注入Map的一种巧妙的用法。

package com.cetc52.situation.service;

import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.util.Map;

/**
 * 数据融合上报数据处理类
 *
 * @author liquanming
 * 2021/9/16 19:52
 */
@Service
@Slf4j
public class MessageService {
    @Autowired
    private Map<String, MessageHandler> messageHandlers;

    /**
     * 消息处理
     *
     * @param message 消息体
     */
    @Async
    public void handelMessage(JSONObject message) {
        final MessageHandler handler = getMessageHandler(message);
        handler.handle(message.getJSONObject("data"));
    }

    /**
     * 获取消息对应的处理器
     *
     * @param message 消息
     * @return 处理器
     */
    private MessageHandler getMessageHandler(JSONObject message) {
        final String messageType = message.getString("type");
        return messageHandlers.get(messageType);
    }

}

    @Autowired
    private Map<String, MessageHandler> messageHandlers;

 在此,massageHandlers会由ioc自动注入,这样,我们并不需要显示的执行构造messageHandlers的过程,而系统会自动为我们把策略结合创建完成。另外的一个好处,就是,当约定了新的消息类型,连MessageService类型,我们并不需要做任何修改,只需要新创建一个具体策略类型即可优雅的完成工作,类的可扩展性拉满。这非常的优雅,而优雅就是程序员的最美好的品格

总结

 本篇文章意在通过使用工厂模式结合策略模式彻底取代if-else分支的出现。本文相较于之前的方法,如果要采用这样的方法,有几个点需要理解。

  • 策略继承体系的提炼,一般if-else两者是要对等的,如果不对等的话,请采用卫语句解决,对等的含义是代码相当,作用相当,类似门当户对。

  • @Component实现接口,并注入其他ioc所管理的bean,这是创造性的一步,因为可以直接把策略的类型由ioc生成和管理,这样也就省略了显式创造的过程,策略工厂虽然存在,但其实也就被MessageService类通过

    @autowired 
    private Map<String, MessageHandler> messageHandlers;
    

    给一键实现了。

  • 优秀的程序员发现重复,不容忍重复。因为重复是万恶之源。(其实就是瑞·达里欧所说的发现问题,不容忍问题)

    SpringBoot使用策略模式彻底消除if-else_第9张图片

    下载

    为防止使用图床失效,附上原文下载地址:

    使用策略模式消除if-else分支跳转

    好了,亲爱的读者,或者说亲爱的程序员,这便是本文的全部内容了,程序员是一个并不是多好的工作,虽然薪水比较高,但还是希望读者能够爱惜自己的身体,坚持运动和早睡早起,有一个良好的身体。能够不断的朝着自己的目标前进前进前进。

    最后,以笔者最近喜欢的一首李涉的诗歌来结束这个文章,爱好诗歌的人,也可以背诵下这首诗歌,万一可以卖弄呢?

    《题鹤林寺壁》
    终日昏昏醉梦间,忽闻春尽强登山;
    因过竹院逢僧话,偷得浮生半日闲。

你可能感兴趣的:(Java,编程思想,实践,策略模式,spring,boot,java)