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

注:阅读本文需要具备的知识: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编程,这篇文章权当抛砖引玉,希望更多码友指教,欢迎在评论留下您独特的理解和看法。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,490评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,581评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,830评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,957评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,974评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,754评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,464评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,847评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,995评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,137评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,819评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,482评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,023评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,149评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,409评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,086评论 2 355

推荐阅读更多精彩内容

  • 很实用的编程英语词库,共收录一千五百余条词汇。 第一部分: application 应用程式 应用、应用程序app...
    春天的蜜蜂阅读 1,362评论 0 22
  • spring官方文档:http://docs.spring.io/spring/docs/current/spri...
    牛马风情阅读 1,684评论 0 3
  • 第一章.面向对象与面向协议编程 本书是关于面向协议编程。当苹果2015年的开发者大会上发布了Swift2,他们也宣...
    酱油不爱醋阅读 1,395评论 0 7
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,105评论 1 32
  • 洞见SELENIUM自动化测试 写在最前面:目前自动化测试并不属于新鲜的事物,或者说自动化测试的各种方法论已经层出...
    厲铆兄阅读 6,730评论 3 47