记一次使用策略模式优化代码的经历

一、背景

之前接手了一个 springboot 项目。在我负责的模块中,有一块用户注册的功能,但是比较特别的是这个注册并不是重新注册,而是从以前的旧系统的数据库中同步旧数据到新系统的数据库中。由于这些用户角色来自于不同的系统,所以我需要在注册的时候先判断类型(这个类型由一个专门的枚举类提供),再去调用已经写好的同步方法同步数据。

伪代码大概是这样的:

public void register(String type,String userId,String projectId,String declareId){
    // 判断用户类型
    if (UserSynchronizeTyeEnum.A.type.equals(type)) {
        // 同步A类型的数据
    } else if (UserSynchronizeTyeEnum.A.type.equals(type)) {
        // 同步B类型的数据
    } else {
		throw new RuntimeException("不存在的用户类型");
    }
    ... ...
}

由于用户的类型比较多,所以当我接手的时候已经有8个 if-esle 了,由于这个项目会逐步的跟其他平台对接,要同步的用户类型会越来越多,而且也不能排除什么时候不新增,反而要取消一部分类型的同步情况。

就这个情况来说,一方面每一次新增类型都会让 if-else 串越来越长,取消一些类型的同步还要直接删除 if-else 里的对应代码;另一方面,这个业务的需求相对稳定,同步方法会不一样,但是一定会根据类型来判断。出于以上考虑,我决定趁现在牵扯范围不大的时候重构一下。

二、思路

1.抽取策略接口和策略类

首先,由于每种用户类型的同步方法是由各模块自己提供的,其实已经抽出了策略,只是没有实现一个统一的策略接口。

但是我在这一步遇上了问题:

  • 各模块的同步方法的名称不全部一样;
  • 由于年代久远,旧代码是不允许改的。

代码不让改,就没法通过为旧实现类新增接口实现多态,方法名不一样,那么反射这条路子也走不通。我想到了装饰器,为每个实现类新增一个装饰器类,注册的时候通过装饰器去调用同步方法,但是这样缺点很明显,会引入一个装饰器接口+n多个装饰器类,为了优化这一个方法,反而要引入十几个类,实在是脱裤子放屁。

但是好在天无绝人之路,他们并不是完全没有相同点:

  • 虽然参数名不一样,但是每个同步方法都需要的参数数量和类型都是一样的;
  • 他们都返回一个布尔值

这让我想起了 JDK8 的函数式接口,将策略接口改造为函数式接口,由于同步方法的参数和返回值类型都是一样的,就可以直接以 Lambda 表达式的形式将各个模块的同步方法放进去,这样就不需要改动模块的代码了。

新增的接口如下:

@FunctionalInterface
public interface IUserSynchronizeSerivice {

    /**
     * 同步方法
     */
    public boolean sync(String userId,String declareId);

}

2.策略池的实现

接着,为了实现原本 if-else 的逻辑,我需要一个策略池,能够建立起一个用户类型跟对应的同步策略的映射关系,一开始,我打算直接写在 register()方法所在的类中加入以下代码:

@Autowired
private AUserService aUserService;
@Autowired
private BUserService bUserService;

private static final Map<String,UserSynchronizeTyeEnum.IUserSynchronizeService> synchronizeServiceStrategy = new HashMap<>();
@PostConstruct
private void strategyInit(){
    // spring容器启动后将策略装入策略池
    synchronizeServiceStrategy.put(UserSynchronizeTyeEnum.A.type,aUserService::synchronization);
    synchronizeServiceStrategy.put(UserSynchronizeTyeEnum.B.type,bUserService::sync);
}

但是这样在添加新的用户类型时,需要先去枚举类添加新枚举,然后再回到register()所在的类为策略池添加策略,这个两个逻辑上相连的过程被分散到了两个地方,而且仍然要修改register()所在类的代码。所以决定不用上述的代码,而是去对枚举类下手。

原本的枚举类是这样的:

/**
 * 老系统用户注册,用户类型与同步方法的枚举类
 */
public enum UserSynchronizeTyeEnum {
    /**
     * 类型A的用户
     */
    A("a"),/**
     * 类型B的用户
     */
    B("b");

    /**
     * 用户类型
     */
    private final String type;

    UserSynchronizeTyeEnum(String type) {
        this.type = type;
    }

    public String getType() {
        return type;
    }
}

为了保证逻辑能够集中,我决定将添加策略这一过程一起放到到枚举类里,在添加枚举的时候就把策略一起放进去:

注:下文的 SpringUtils 实现了 BeanFactoryPostProcessor 接口,是一个用于从 ConfigurableListableBeanFactory 获取对象的工具类。

/**
 * 老系统用户注册,用户类型与同步方法的枚举类
 * 添加新类型时,需要将模块对应的同步方法一并放入
 */
public enum UserSynchronizeTyeEnum {

    /**
     * 类型A的用户
     */
    A("a",(userId,projectId,declareId) -> {
        return SpringUtils.getBean(AUserService.class).synchronization(userId,declareId);
    }),/**
     * 类型B的用户
     */
    B("b",declareId) -> {
        return SpringUtils.getBean(BUserService.class).sync(userId,declareId);
    });

    /**
     * 用户类型
     */
    private final String type;

    /**
     * 同步方法
     */
    private final IUserSynchronizeService synchronizeService;

    UserSynchronizeTyeEnum(String type,IUserSynchronizeService synchronizeService) {
        this.type = type;
        this.synchronizeService = synchronizeService;
    }
}

由于由于枚举类已经相当于之前策略池的 Map 集合了,所以我们直接在里面添加一个 getSynchronizeService()方法,用于直接获取同步方法:

/**
 * 根据枚举值获取对应同步方法
 */
public Optional<IUserSynchronizeService> getSynchronizeService(String type) {
    for (UserSynchronizeTyeEnum tyeEnum : UserSynchronizeTyeEnum.values()) {
        if (tyeEnum.type.equals(type)) {
            return Optional.of(tyeEnum.synchronizeService);
        }
    }
    return Optional.empty();
}

到目前为止,策略池已经基本完成了,但是我们不难发现,现在为策略接口添加实现的地方也变成了枚举类中,策略接口 IUserSynchronizeService 一般也不会被用在其他地方,因此不妨把策略接口也一并引入枚举类中,让他成为一个枚举类的内部接口

现在,枚举类是这样的:

策略模式的枚举类

枚举类堆外只暴露根据类型获取方法的IUserSynchronizeService() 方法,以及 A 和 B 两个枚举。

完整的 UserSynchronizeTyeEnum枚举类代码如下:

/**
 * 老系统用户注册,用户类型与同步方法的枚举类
 * 添加新类型时,需要将模块对应的同步方法一并放入。待用户注册时,会遍历枚举对象并根据类型获取对应的同步方法执行。
 */
public enum UserSynchronizeTyeEnum {

    /**
     * 类型A的用户
     */
    A("a",declareId);
    });

    /**
     * 用户类型
     */
    public String type;

    /**
     * 同步方法
     */
    public IUserSynchronizeService synchronizeService;

    UserSynchronizeTyeEnum(String type,IUserSynchronizeService synchronizeService) {
        this.type = type;
        this.synchronizeService = synchronizeService;
    }

    /**
     * 根据枚举值获取对应同步方法
     */
    public static Optional<IUserSynchronizeService> getSynchronizeService(String type) {
        for (UserSynchronizeTyeEnum tyeEnum : UserSynchronizeTyeEnum.values()) {
            if (tyeEnum.type.equals(type)) {
                return Optional.of(tyeEnum.synchronizeService);
            }
        }
        return Optional.empty();
    }

    /**
     * 同步方法需要符合函数式接口
     */
    @FunctionalInterface
    public interface IUserSynchronizeService {
        boolean sync(List<Map<String,Object>> dateList,String declareId);
    }
}

三、使用

现在,改造完毕,可以开始使用了,对于原先的 register()方法,现在改为:

public void register(String type,String declareId){
    // 获取同步方法,没有就抛异常
    UserSynchronizeTyeEnum.IUserSynchronizeService synchronizeService = UserSynchronizeTyeEnum.getSynchronizeService(type)
        .orElseThrow(() -> new RuntimeException("类型不存在"));
    // 同步用户数据
    synchronizeService.sync(userId,declareId);
}

当我们需要再添加一个 C 类用户的同步注册的时候,只需要前往枚举类添加:

/**
 * 类型C的用户
 */
C("c",declareId) -> {
    return SpringUtils.getBean(CUserService.class).sync(userId,declareId);
});

即可,register()方法就不需要再做修改了。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


什么是设计模式一套被反复使用、多数人知晓的、经过分类编目的、代码 设计经验 的总结;使用设计模式是为了 可重用 代码、让代码 更容易 被他人理解、保证代码 可靠性;设计模式使代码编制  真正工程化;设计模式使软件工程的 基石脉络, 如同大厦的结构一样;并不直接用来完成代码的编写,而是 描述 在各种不同情况下,要怎么解决问题的一种方案;能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免引
单一职责原则定义(Single Responsibility Principle,SRP)一个对象应该只包含 单一的职责,并且该职责被完整地封装在一个类中。Every  Object should have  a single responsibility, and that responsibility should be entirely encapsulated by t
动态代理和CGLib代理分不清吗,看看这篇文章,写的非常好,强烈推荐。原文截图*************************************************************************************************************************原文文本************
适配器模式将一个类的接口转换成客户期望的另一个接口,使得原本接口不兼容的类可以相互合作。
策略模式定义了一系列算法族,并封装在类中,它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
设计模式讲的是如何编写可扩展、可维护、可读的高质量代码,它是针对软件开发中经常遇到的一些设计问题,总结出来的一套通用的解决方案。
模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中,使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
迭代器模式提供了一种方法,用于遍历集合对象中的元素,而又不暴露其内部的细节。
外观模式又叫门面模式,它提供了一个统一的(高层)接口,用来访问子系统中的一群接口,使得子系统更容易使用。
单例模式(Singleton Design Pattern)保证一个类只能有一个实例,并提供一个全局访问点。
组合模式可以将对象组合成树形结构来表示“整体-部分”的层次结构,使得客户可以用一致的方式处理个别对象和对象组合。
装饰者模式能够更灵活的,动态的给对象添加其它功能,而不需要修改任何现有的底层代码。
观察者模式(Observer Design Pattern)定义了对象之间的一对多依赖,当对象状态改变的时候,所有依赖者都会自动收到通知。
代理模式为对象提供一个代理,来控制对该对象的访问。代理模式在不改变原始类代码的情况下,通过引入代理类来给原始类附加功能。
工厂模式(Factory Design Pattern)可细分为三种,分别是简单工厂,工厂方法和抽象工厂,它们都是为了更好的创建对象。
状态模式允许对象在内部状态改变时,改变它的行为,对象看起来好像改变了它的类。
命令模式将请求封装为对象,能够支持请求的排队执行、记录日志、撤销等功能。
备忘录模式(Memento Pattern)保存一个对象的某个状态,以便在适当的时候恢复对象。备忘录模式属于行为型模式。 基本介绍 **意图:**在不破坏封装性的前提下,捕获一个对象的内部状态,并在该
顾名思义,责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为
享元模式(Flyweight Pattern)(轻量级)(共享元素)主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结