switch的替代方案——面向Operation编程

2019-05-01  本文已影响0人  sprainkle

注:阅读本文需要具备的知识:lambda表达式。

痛点

先举个例子:
用户的登录账号有手机号码和邮箱等, 所以现在有 AccountTypeEnum 枚举,在注册过程的最后一步 -- 插入数据, 我们需要将注册账号 set 到对应的字段, 即: 手机 → setPhone(phone), 邮箱 → setEmail(email);

一般情况, 我们可以通过 switch(account) 的方式, 根据不同的账号类型把账号 set 到对应字段. 这样做没错, 但很麻烦, 后续的维护成本也很高, 比如:

  1. 所有跟账号类型有关的逻辑, 都会有一个 switch(account) 代码块;
  2. 以后再加一种 account type, 所有 switch(account) 代码块都必须修改;

解决方案

面向操作编程可以屏蔽不同类型的差异, 只要在业务逻辑开始之前, 根据 account type 路由到对应的 Operator, 比如: phone → PhoneOperator, 接下来的所有与 account type 有关的操作, 只需要跟 PhoneOperator 打交道即可.

面向操作编程其实是本人根据上述的解决方案起的名字,不具权威性,若不喜可以忽略,这里主要是介绍一种解决思路,或有更好的欢迎在评论留言。

实现

Operator

public interface Operator<K> {

    /**
     * Operator的名称, 同一类型的Operator的路由器{@link OperatorRouter}能够根据该值路由到当前的Operator
     * @return route key
     */
    K getName();

}

该接口很简单,但确实最核心的一个,之后的所有扩展方法都会在该接口的实现类中定义。该只有一个方法getName(),该方法的返回结果就是实现类的名称,但其实是用于路由器OperatorRouter的路由。

OperatorRouter

public abstract class OperatorRouter<K, O extends Operator> {

    /**
     * 存放同一类型的{@link Operator}
     */
    private Map<K, O> operatorMap = Collections.emptyMap();

    /**
     * 根据 route key 路由到目标{@link Operator}
     * @param routeKey
     * @return
     */
    public O route(K routeKey) {
        O o = operatorMap.get(routeKey);
        if (o == null) {
            handleBadRoute(routeKey);
        }
        return o;
    }

    /**
     * 处理路由结果为空的情况. {@link #route(Object)}
     * @param routeKey
     */
    protected abstract void handleBadRoute(K routeKey);

    /**
     * 返回{@link Operator}的子类的{@link Class}
     * @return {@link O#getClass()}
     */
    protected abstract Class<O> getOperatorClass();

    /**
     * 在初始化{@link OperatorRouter}时, 会对所有 {@link Operator}进行校验, 确保初始化完成后的{@link OperatorRouter},
     * 其管理的 {@link Operator} 都是可用的, 校验逻辑默认为直接放行, 当子类需要对其管理的 {@link Operator} 进行校验时, 可重写该方法.
     *
     * @param operator
     */
    protected void checkOperator(O operator) {}

    void setOperatorMap(Map<K, O> operatorMap) {
        this.operatorMap = operatorMap;
    }

}

OperatorRouter为同一类型的Operator实现类的路由器,其中主要有一个属性,和两个抽象方法。属性operatorMap用于存放所有的Operator实现类;第一个抽象方法handleBadRoute(K routeKey)很好理解,就是处理路由结果为空的情况,可以看到带一个参数routeKey,可以在处理的时候知道哪个路由出现问题,比如在日志打印的时候会用到;第二个方法Class<O> getOperatorClass()下文会介绍。

到这里主要的类就介绍完了,这两个类一般会放在公共包中,然后在项目中实现或继承。但是还差一步,就是必须要在项目初始化的时候收集同类型的Operator然后通过OperatorRouter#setOperatorMap注入路由器中。如果项目使用的是Spring系的框架,实现类都会交由容器进行管理,即注入容器成为一个Bean,这里给出一种思路:OperatorAutoConfiguration

OperatorAutoConfiguration

@Configuration
public class OperatorAutoConfiguration {

    @Autowired(required = false)
    public void initOperatorRouter(Map<String, OperatorRouter> routerMap, ApplicationContext applicationContext) {
        if (null != routerMap && false == routerMap.isEmpty()) {

            routerMap.values().forEach(router -> {
                Class<Operator> operatorClass = router.getOperatorClass();
                Map<String, Operator> beans = applicationContext.getBeansOfType(operatorClass);

                Map<Object, Operator> tmpMap = new HashMap(8);

                beans.forEach((beanName, operator) -> {
                    router.checkOperator(operator);
                    tmpMap.put(operator.getName(), operator);
                });

                router.setOperatorMap(Collections.unmodifiableMap(tmpMap));
            });

        }
    }

}

然后在公共包的resource包下新建目录META-INF,再新建文件spring.factories,文件内容为(包路径需要替换为OperatorAutoConfiguration的包路径):

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  包路径.OperatorAutoConfiguration

注:spring.factories的作用,如果不知道的可以自行百度或Google,springboot的实现原理很大程度就是基于这个。

实战

定义AccountTypeEnum

public enum AccountTypeEnum {
    /** 邮件 */
    EMAIL("email", "邮箱"),
    /** 手机 */
    PHONE("phone", "手机"),
    ;

    private String code;

    private String name;

    /**
     * 允许返回空
     *
     * @param code code
     * @return {@link AccountTypeEnum}
     */
    public static AccountTypeEnum parseOfNullable(String code) {
        if (code != null) {
            for (AccountTypeEnum e : values()) {
                if (e.code.equals(code)) {
                    return e;
                }
            }
        }
        return null;
    }

    /**
     * 允许返回空
     *
     * @param code code
     * @return name
     */
    public static String getNameNullable(String code) {
        AccountTypeEnum e = parseOfNullable(code);
        if (e != null) {
            return e.name;
        }
        return null;
    }
    // 省略getter、构造方法,如果使用lombok插件,可以使用注解@Getter、@AllArgsConstructor
}

定义用户类

public class SysUser implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主键ID
     */
    private Long userId;
    /**
     * 邮箱
     */
    private String email;
    /**
     * 手机
     */
    private String phone;
    // 省略其他属性、getter/setter
}

定义AccountTypeOperator

public interface AccountTypeOperator extends Operator<AccountTypeEnum> {

    /**
     *
     * @return
     */
    BiConsumer<SysUser, String> getAccountSetter();

    /**
     *
     * @return
     */
    Function<SysUser, String> getAccountGetter();

    /**
     * 校验账号不存在. 若存在, 则抛异常
     * @param sysUser
     * @param account
     */
    void checkAccountNotExists(SysUser sysUser, String account);

    /**
     * 校验账号存在. 若不存在, 则抛异常
     * @param sysUser
     * @param account
     */
    void checkAccountExists(SysUser sysUser, String account);

    default String getAccount(SysUser sysUser) {
        return getAccountGetter().apply(sysUser);
    }

    default void setAccount(SysUser sysUser, String account) {
        getAccountSetter().accept(sysUser, account);
    }

}

EmailAccountTypeOperator

@Component
public class EmailAccountTypeOperator implements AccountTypeOperator {

    @Override
    public AccountTypeEnum getName() {
        return AccountTypeEnum.EMAIL;
    }

    @Override
    public BiConsumer<SysUser, String> getAccountSetter() {
        return (SysUser::setEmail);
    }

    @Override
    public Function<SysUser, String> getAccountGetter() {
        return SysUser::getEmail;
    }

    @Override
    public void checkAccountNotExists(SysUser sysUser, String account) {
        // 校验邮箱是否已被使用. 若被使用, 则抛异常
    }

    @Override
    public void checkAccountExists(SysUser sysUser, String account) {
        // 校验邮箱是否已被使用. 若没找到对应记录, 则抛异常
    }

}

PhoneAccountTypeOperator

@Component
public class PhoneAccountTypeOperator implements AccountTypeOperator {

    @Override
    public AccountTypeEnum getName() {
        return AccountTypeEnum.PHONE;
    }

    @Override
    public BiConsumer<SysUser, String> getAccountSetter() {
        return (SysUser::setPhone);
    }

    @Override
    public Function<SysUser, String> getAccountGetter() {
        return SysUser::getPhone;
    }

    @Override
    public void checkAccountNotExists(String account) {
        // 校验手机号码是否已被使用. 若被使用, 则抛异常
    }

    @Override
    public void checkAccountExists(String account) {
        // 校验手机号码是否已被使用. 若没找到对应记录, 则抛异常
    }

}

AccountTypeOperatorRouter

@Component
public class AccountTypeOperatorRouter extends OperatorRouter<AccountTypeEnum, AccountTypeOperator> {

    @Override
    protected void handleBadRoute(AccountTypeEnum routeKey) {
        // 提前抛异常,避免空指针异常
    }

    @Override
    public Class<AccountTypeOperator> getOperatorClass() {
        return AccountTypeOperator.class;
    }

}

继承OperatorRouter并实现2个抽象方法,其中getOperatorClass()的作用就是返回当前路由器管理的Operator子类的Class,这里为AccountTypeOperator,方便从Spring容器中获取所有AccountTypeOperator实现类实例,然后放到OperatorRouter#operatorMap中。

AccountUtil

public class AccountUtil {
    /**
     * 邮件
     */
    public static final String EMAIL_PATTERN = "^[\\w!#$%&'*+/=?`{|}~^-]+(?:\\.[\\w!#$%&'*+/=?`{|}~^-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}$";
    private static final Pattern emailPattern = Pattern.compile(EMAIL_PATTERN);

    /**
     * 手机
     */
    public static final String PHONE_PATTERN = "^1[3|4|5|7|8][0-9]{9}$";
    private static final Pattern phonePattern = Pattern.compile(PHONE_PATTERN);


    /**
     * 检测账号类型
     *
     * @param account
     * @return
     */
    public static AccountTypeEnum detectAccountType(String account) {
        if (StrUtil.isBlank(account)) {
            return null;
        }

        if (checkEmail(account)) {
            return AccountTypeEnum.EMAIL;
        }

        if (checkPhone(account)) {
            return AccountTypeEnum.PHONE;
        }

        return null;
    }

    public static boolean checkEmail(String account) {

        return emailPattern.matcher(account).matches();
    }

    public static boolean checkPhone(String account) {

        return phonePattern.matcher(account).matches();
    }
}

UserService

@Service
public class UserService {

    @Autowired
    private AccountTypeOperatorRouter accountTypeOperatorRouter;

    // 省略其他依赖

    /**
     * 使用面向Operation编程实现
     * @param request
     * @return
     */
    public SysUser register(RegisterRequest request) {
        AccountTypeEnum accountType = AccountUtil.detectAccountType(request.getAccount());
        AccountTypeOperator operator = accountTypeOperatorRouter.route(accountType);

        // 检验邮箱或手机号码未被注册
        operator.checkAccountNotExists(request.getAccount());

        // 其他校验

        SysUser newUser = new SysUser();
        operator.getAccountSetter().accept(newUser, request.getAccount());

        // 借助 java8 的接口默认方法进一步封装
        // operator.setAccount(newUser, request.getAccount());

        newUser.setPassword(request.getPassword());

        // 插入数据, 进行注册

        return newUser;
    }

    /**
     * 使用switch实现
     * @param request
     * @return
     */
    public SysUser registerThroughSwitch(RegisterRequest request) {
        AccountTypeEnum accountType = AccountUtil.detectAccountType(request.getAccount());

        // 检验邮箱或手机号码未被注册
        switch (accountType) {
            case EMAIL:
                // 校验邮箱未被使用
                break;
            case PHONE: 
                // 校验手机号码未被使用
                break;
            default:
                // do something
        }
        
        // 其他校验

        SysUser newUser = new SysUser();
        switch (accountType) {
            case EMAIL:
                newUser.setEmail(request.getAccount());
                break;
            case PHONE:
                newUser.setPhone(request.getAccount());
                break;
            default:
                // do something
        }
        newUser.setPassword(request.getPassword());

        // 插入数据, 进行注册

        return newUser;
    }

    @Data
    public static class RegisterRequest {
        /**
         * 账号. 邮箱或手机号码
         */
        private String account;
        /**
         * 密码
         */
        private String password;

        // 省略其他属性和getter/setter
    }

    // 省略其他方法

}

可以看出,如果使用switch,代码十分臃肿,而且每一个switch都有一个default分支需要处理,而且以后如果再加一种账号类型,那么必须要改所有相关的switch代码块,而这明显违背开闭原则 (OCP)。相反,面向Operation编程,则很好的遵守了该原则,只需要再添加一个对应的AccountTypeOperator实现类即可。其他的好处,各位可以自行体会。

对于面向Operation编程,这篇文章权当抛砖引玉,希望更多码友指教,欢迎在评论留下您独特的理解和看法。

上一篇 下一篇

猜你喜欢

热点阅读