在springboot中使用策略模式

在springboot中使用策略模式

Scroll Down

简介

设计模式

设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。项目中合理地运用设计模式可以完美地解决很多问题,每种模式在现实中都有相应的原理来与之对应,每种模式都描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是设计模式能被广泛应用的原因。

使用原因

在使用的酷Q的http模式以后,我在后台建立了一个Springboot的服务项目作为消息接收器和回复功能,但是由于回复消息就需要很多种消息回复功能,所以,堆在一个类实现显然是不可能的,所以,策略模式应召而来

image-20200814203308619

注:酷Q已亡,江山已尽,该文档仅作代码展示

CQHTTP消息体

接受参数

import lombok.Data;

/**
 * 接受到的消息对象
 * @Author: lsq
 */
@Data
public class RequestMessage {
    private String postType; //上报类型 message
    private String messageType; // 消息类型 private group
    // 消息子类型,如果是好友则是 friend,如果从群或讨论组来的临时会话则分别是 group、discuss
    private String subType;
    private Integer messageId; // 消息 ID
    private Integer groupId; // 群ID
    private Integer userId; // 发送人ID
    // 消息内容
    private String message;
    private String rawMessage; // 原始消息
    private Integer font; // 字体大小
    private Sender sender; // 发送人信息
    @Data
    public static class Sender{
        private Integer userId;
        private String nickname;
        private String card;
        private String sex;
        private Integer age;
        private String area;
        private String level;
        private String role;
        private String title;
    }
}

传出参数

import lombok.Data;

/**
 * 回复的消息
 */
@Data
public class ResponseMessage {
    // 要回复的内容
    String reply;
    // 消息内容是否作为纯文本发送(即不解析 CQ 码),只在 reply 字段是字符串时有效
    Boolean autoEscape;
    // 是否要在回复开头 at 发送者(自动添加),发送者是匿名用户时无效
    Boolean atSender;
    // 撤回该条消息
    Boolean delete;
    // 	把发送者踢出群组(需要登录号权限足够),不拒绝此人后续加群请求,发送者是匿名用户时无效
    Boolean kick;
    // 	把发送者禁言 ban_duration 指定时长,对匿名用户也有效
    Boolean ban;
    // 禁言时长
    Long banDuration;
    // 邀请时是否通用请求
    Boolean approve;
}

策略模式

在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。

在策略模式中,我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的 context 对象。策略对象改变 context 对象的执行算法。

策略模式把对象本身和运算规则区分开来,模式分为三个部分。

  1. 环境类(Context):用来操作策略的上下文环境 ,也就是说传入消息然后获取消息的控制层
  2. 抽象策略类(Strategy):策略的抽象,策略模式的接口,
  3. 具体策略类(ConcreteStrategy):具体的策略实现,每一种回复模式的具体实现。下面我们代码去实现一遍就能很清楚的理解了,

抽象策略类

无论是任何消息回复模式,其核心都只有一个功能,那就是接受消息内容,然后根据消息内容回复,因此,策略类核心的功能就出来了

于是,我们定义一个接口用于抽象所有的消息回复方法

public interface CoolqService {
    /**
     * 返回消息体
     * @param news 接受的消息内容
     * @return 返回的消息体
     */
    ResponseMessage respose(RequestMessage news);
}

respose方法用于返回数据这就是一个抽象出来的方法

环境类

由于消息接受和返回使用的是http协议,所以我们要监听某个端口并返回数据,那便需要一个控制层获取请求回应消息

@RestController
public class CoolqController {
    @Autowired
    CoolqService coolqImpl;
    @PostMapping("/news")
    public ResponseMessage postNews(@RequestBody RequestMessage news){
        return coolqImpl.respose(news);
    }
}

@Autowired 是Spring MVC的一个自动注入类,用于从Spring IOC 容器中自动获取Bean,从而实现业务类解耦

具体策略类

既然抽象策略类有了,环境类也有了,那便轮到了具体业务类例如:以下的阴阳怪气模式

@Service("阴阳怪气")
public class YYGQImplCoolq implements CoolqService {
    double FORCE_UNIVERSAL_ANSWER_RATE = 0.13;
    String[] UNIVERSAL_ANSWERS = {
            "你说你马呢?",
            "那没事了。",
            "真别逗我笑啊。",
            "那可真是有趣呢。",
            "就这?就这?",
            "你品,你细品。",
            "不会真有人觉得是这样的吧,不会吧不会吧?",
            "你在教我做事?",
            "是的呢",
            "是的呢",
            "所以呢?",
            "然后呢?",
            "确实是这样呢",
            "真是有够可笑的呢",
            "那你厉害咯~",
            "就你知道咯~",
            "那你很棒棒哦",
            "那你很厉害哦",
            "真就oo嗷",
            "您可真聪明呢",
            "切,也就这样吧~",
            "切,也就这样吧~",
            "切,无聊~",
            "切,就你?",
            "我最佩服北京双十节的情形。早晨,员警到门,吩咐道:‘挂旗!’‘是,挂旗!’”各家大半懒洋洋的踱出一个国民来,撅起一块斑驳陆离的洋布。这样一直到夜,——收了旗关门;几家偶然忘却的,便挂到第二天的上午。",
            "一见短袖子,立刻想到白臂膊,立刻想到全裸体,立刻想到生殖器,立刻想到性交,立刻想到杂交,立刻想到私生子。中国人的想像惟在这一层能够如此跃进。"

    };
    String[] STRONG_EMOTION_ANSWERS = {
            "你急了急了急了?",
            "他急了,他急了!",
            "切,真是玻璃心~",
            "就你最厉害",
            "这就是oo吗,i了i了・这就是oo吗,真是有够可笑的呢",
            "老达尔文了:适者生存,爱干不干,不干就走。"
    };

    String[] QUESTION_ANSWERS = {
            "不会真有人还不知道吧?",
            "你都不知道,那你说你马呢?",
            "你不是大学生吗?这都不知道?",
            "我养条狗倒粮食它都会自己吃,你家孩子这么大连自己吃饭都不会,倒不如问问他以后该怎么办。",
            "懂的都懂,不懂也不解释"
    };

    String[] STRONG_EMOTION_PATTERNS = {
            "!",
            "???",
            "???",
            "气抖冷",
            "傻逼",
            "垃圾",
            "2b",
    };

    String[] QUESTION_PATTERNS = {
            "?",
            "怎么",
            "什么",
            "咋",
            "?",
            "你是"
    };
    @Override
    public ResponseMessage respose(RequestMessage news) {
        String key = news.getMessage();
        ResponseMessage message = new ResponseMessage();
        message.setReply(getLikeValue(key));
        return message;
    }

    public String getLikeValue(String key) {
        Random random = new Random();
        double doubles = random.nextDouble();
        if (FORCE_UNIVERSAL_ANSWER_RATE > doubles) {
            return UNIVERSAL_ANSWERS[random.nextInt(UNIVERSAL_ANSWERS.length)];
        }
        if (ifin(key, STRONG_EMOTION_PATTERNS)) {
            return STRONG_EMOTION_ANSWERS[random.nextInt(STRONG_EMOTION_ANSWERS.length)];
        }
        if (ifin(key, QUESTION_PATTERNS)) {
            return QUESTION_ANSWERS[random.nextInt(QUESTION_ANSWERS.length)];
        }
        return UNIVERSAL_ANSWERS[random.nextInt(UNIVERSAL_ANSWERS.length)];
    }

    private boolean ifin(String a, String[] patterns) {
        for (String i : patterns) {
            if (a.contains(i)) {
                return true;
            }
        }
        return false;
    }
}

这个阴阳怪气类实现了CoolqService 通过并通过@Service注入到IOC容器中,用于控制层去自动获取

效果如下

img

那么问题来了,@Autowired自动获取只能获取一个类,那我要怎么切换模式来达到使用其他策略模式的方式?

if(???){
    XXX xxx = new XXX();
    xxx.y();
}else if(???){
    XXX xxx = new XXX();
    xxx.y();
}else .......

用这种方法?那也太麻烦了吧,而且违反了开闭原则,耦合度极高,每一次新增都要改代码,所有,这里便要使用第二种模式,工厂模式

工厂模式

工厂模式(Factory Pattern)是 Java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

换句话说,使用了工厂模式,使用者便不需要去想如何创建类,只要使用get方法便可以从工厂获取一类想要类,简单,无脑

@Service
public class FactoryForCoolqService {
    @Autowired
    private Map<String, CoolqService> coolqServices = new ConcurrentHashMap();

    public CoolqService getCoolqService(String component){
        CoolqService coolqService = CoolqServices.get(component);
        if(coolqService == null) {
            throw new RuntimeException("并没有找到这个类呦");
        }
        return coolqService;
    }
}

ConcurrentHashMap 是加了锁的HashMap,用于防止并发问题

@Autowired 从IOC容器中获取所有的被注入到容器中的CoolqService对象,key 为@Service注解中指定的value属性,未指定的默认为类名,

getCoolqService方法便是通过模式名去获取 CoolqService 对象

于是,我们的控制层便可以改造为:

@RestController
public class CoolqController {
    @Autowired
    CoolqService coolqImpl;
    @Autowired
    FactoryForCoolqService factoryForCoolqService;

    @PostMapping("/news")
    public ResponseMessage postNews(@RequestBody RequestMessage news) {
        String key = news.getMessage();
        String[] words = key.split(" ", 2);
        news.setMessage(words[1]);
        message = factoryForCoolqService.getCoolqServices(words[0]).respose(news);
        return coolqImpl.respose(news);
    }
}

这里通过空格将传入信息分离 并利用空格之前的内容去获取CoolqService对象,并将消息内容替换为空格之后的内容

于是我们再注入一个点歌模块试试?

@Service("点歌")
@Log4j2
public class Music163ImplCoolq  implements CoolqService {
    @Autowired
    private RestTemplate restTemplate;
    @Value("${url.网易云}")
    private String url;
    
    @Override
    public ResponseMessage respose(RequestMessage news) {
        String key = news.getMessage();
        ResponseMessage message = new ResponseMessage();
        message.setReply(getLikeValue(key));
        return message;
    }

    public String getLikeValue(String key) {
        HashMap map = restTemplate.getForObject(url + "/search?keywords="+key,HashMap.class);
        assert map != null;
         List<Map<String, Integer>> musics = ((Map<String, List<Map<String, Integer>>>) map.get("result")).get("songs");
        Integer integer = musics.get(0).get("id");
        return "[CQ:music,type=163,id="+integer.toString()+"]";
    }
}

这里去请求网易云音乐的查询接口例子如下:

网易云

很棒,那这样我们就实现了多策略模式切换,但是,respose这个方法就很难受,每个策略要读取信息和返回信息,都要去获取内容,并构造返回内容,这样导致了每个策略类中都有固定的重复代码,大有我只想要一颗树,你却给我一片森林的感觉。

如果我只想获取你发送的消息的字符串并返回一个字符串回复给你,而且获取我还有可能想要news的信息,但是我却不想要去构造news呢?

有的,那便是模板模式

模板模式

在模板模式(Template Pattern)中,一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。这种类型的设计模式属于行为型模式。

我可以使用一个抽象类实现CoolqService

public abstract class AbstractCoolqTemplate implements CoolqService {
    protected volatile ResponseMessage  message = new ResponseMessage();
    protected volatile RequestMessage news;
    @Override
    public ResponseMessage respose(RequestMessage news) {
        this.news = news;
        String key = news.getMessage();
        message.setReply(getLikeValue(key));
        return message;
    }
    /**
     * 通过模糊查询获取答案
     * @param key 问题
     * @return 答案
     */
    public abstract String getLikeValue(String key);
}

volatile 关键字修饰响应内容和返回值防止并发修改值

并新定义一个抽象方法getLikeValue给子类实现

那这样,子类在继承AbstractCoolqTemplate 时候,只需要去实现getLikeValue方法即可,并返回一条字符串,也能通过news全局变量得到具体消息内容

所以,我们的点歌功能便可以修改为

@Service("点歌")
@Log4j2
public class Music163ImplCoolq extends AbstractCoolqTemplate {
    @Autowired
    private RestTemplate restTemplate;
    @Value("${url.网抑云}")
    private String url;

    @Override
    public String getLikeValue(String key) {
        HashMap map = restTemplate.getForObject(url + "/search?keywords="+key,HashMap.class);
        assert map != null;
        List<Map<String, Integer>> musics = ((Map<String, List<Map<String, Integer>>>) map.get("result")).get("songs");
        Integer integer = musics.get(0).get("id");
        return "[CQ:music,type=163,id="+integer.toString()+"]";
    }
}

因此,我们在创造新模式的时候,只要继承AbstractCoolqTemplate 类并实现 getLikeValue,并在类上加上@Service即可

模板如下:

@Service("xxx模式")
public class Xxxxxxxx extends AbstractCoolqTemplate {
    @Override
    public String getLikeValue(String key) {
        return "回复什么消息呢";
    }
}

啊,这该死的简洁,完全不用管其他类的内容,完全实现了业务的解耦

如我们可以创建一个测试类:

@Service("测试")
public class 测试用 extends AbstractCoolqTemplate {
    @Override
    public String getLikeValue(String key) {
        return key + "呢";
    }
}

结果如下图

结语

面向对象结合设计模式,才能真正体会到程序变得可维护、可复用、可扩展、灵活性好。设计模式对于程序员而言并不陌生,每个程序员在编程时都会或多或少地接触到设计模式。无论是在大型程序的架构中,亦或是在源码的学习中,设计模式都扮演着非常重要的角色。