如何优雅的使用枚举

从mybatis和逻辑判断两个方面介绍枚举的使用, 并且顺带讲了一下函数式编程是怎么回事

mybatis中使用枚举

关于这部分内容网上有很多介绍, 本节也是基于网上的教程写的, 不过本节还是很详细的.

表结构

数据库表中, 像状态,类型这样含义的字段通常都是可以用枚举进行替代的. 如下面的表结构中artical_type

CREATE TABLE `blog_artical` (
    `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '文章id',
    `author_id` INT(11) UNSIGNED NOT NULL COMMENT '作者id',
    `title` VARCHAR(50) NULL DEFAULT NULL COMMENT '文章标题',
    `content` TEXT NULL COMMENT '文章内容',
    `description` VARCHAR(200) NULL DEFAULT NULL COMMENT '描述',
    `artical_type` TINYINT(4) UNSIGNED NOT NULL DEFAULT '3' COMMENT '文章类型 1.技术2.生活3.草稿4.其他',
    `is_delete` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' COMMENT '是否删除',
    `create_time` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `update_time` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    PRIMARY KEY (`id`),
    INDEX `idx_author_id` (`author_id`)
)
COLLATE='utf8mb4_0900_ai_ci'
ENGINE=InnoDB
;

创建枚举类

为了和数据库中artical_type对应, 需要创建一个枚举类. 注意这里我们继承并重写了getCode方法, 其目的在于统一获取枚举code的方式. ArticalTypeEnum使用getCode, 如果其他枚举使用getValue,getInt等等, 将十分混乱.

public enum  ArticalTypeEnum implements BaseCode {
    /** 技术 */
    TECH(1, "技术"),
    /** 生活 */
    LIFE(2, "生活"),
    /** 草稿 */
    DRAFT(3, "草稿"),
    /** 其他 */
    OTHER(4, "其他");

    private Integer code;
    private String description;

    ArticalTypeEnum(Integer code, String description) {
        this.code = code;
        this.description = description;
    }

    @Override
    public Integer getCode() {
        return this.code;
    }

    public String getDescription() {
        return description;
    }
}


/**
 * 为了统一数据库和对应枚举值的对应关系, 接口中的getCode方法为获取枚举值的方式
 */
public interface BaseCode {

    Integer getCode();

    /**
     * 根据枚举类型和code获取对应的枚举
     * @param c
     * @param code
     * @param <T>
     * @return
     */
    static <T extends BaseCode> T valueOfEnum(Class<T> c, int code) {
        BaseCode[] enums = c.getEnumConstants();
        Optional<BaseCode> optional = Arrays.asList(enums).stream()
                .filter(baseEnum -> baseEnum.getCode().equals(code)).findAny();
        if (optional.isPresent()) {
            return (T) optional.get();
        }
        throw new IllegalArgumentException(String.format("[%s]没有对应枚举值: [%s]", c.getName(), code));
    }

}

定义枚举转换规则

实现自定义的转换规则只需要继承BaseTypeHandler重写方法即可

public class BaseCodeTypeHandler<E extends BaseCode> extends BaseTypeHandler<E> {

    private final Class<E> type;

    public BaseCodeTypeHandler(Class<E> type) {

        if (type == null) {
            throw new IllegalArgumentException("Type argument cannot be null");
        } else {
            this.type = type;
        }
    }

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
        ps.setInt(i, parameter.getCode());
    }

    @Override
    public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
        int code = rs.getInt(columnName);
        return rs.wasNull() ? null : BaseCode.valueOfEnum(this.type, code);
    }

    @Override
    public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        int code = rs.getInt(columnIndex);
        return rs.wasNull() ? null : BaseCode.valueOfEnum(this.type, code);
    }

    @Override
    public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        int code = cs.getInt(columnIndex);
        return cs.wasNull() ? null : BaseCode.valueOfEnum(this.type, code);
    }

}

我们可以继续封装一个自动转换枚举类型的转换器, 其作用为如果实现了自定义枚举类通过自定义枚举类进行转换, 否则使用默认的EnumTypeHandler进行转换. 注意这里面的复写方法只是使用对应的转换器去调用相应的方法. 因此注册转换器的时候只要注册AutoEnumTypeHandler就可以.

public class AutoEnumTypeHandler<E extends Enum<E>> extends BaseTypeHandler<E> {

    private BaseTypeHandler typeHandler = null;

    public AutoEnumTypeHandler(Class<E> type) {
        if (type == null) {
            throw new IllegalArgumentException("Type argument cannot be null");
        }
        if(BaseCode.class.isAssignableFrom(type)){
            typeHandler = new BaseCodeTypeHandler(type);
        }else {
            typeHandler = new EnumTypeHandler<>(type);
        }
    }

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
        typeHandler.setNonNullParameter(ps,i, parameter,jdbcType);
    }

    @Override
    public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return (E) typeHandler.getNullableResult(rs,columnName);
    }

    @Override
    public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return (E) typeHandler.getNullableResult(rs,columnIndex);
    }

    @Override
    public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return (E) typeHandler.getNullableResult(cs,columnIndex);
    }
}

注册枚举转换器

SqlSessionFactory sqlSessionFactory = bean.getObject();
TypeHandlerRegistry typeHandlerRegistry = sqlSessionFactory.getConfiguration().getTypeHandlerRegistry();
typeHandlerRegistry.setDefaultEnumTypeHandler(AutoEnumTypeHandler.class);

mybatisGenerator

注意mybatisGenerator.xml在逆向工程中可以直接帮我们生成数据库对应的实体类, 因为我们需要类型字段为枚举因此需要在生成的时候指定字段类型以及转换器.

 <table tableName="blog_artical">
            <ignoreColumn column="create_time"/>
            <ignoreColumn column="update_time"/>
            <columnOverride column="artical_type"
                                javaType="com.community.constant.ArticalTypeEnum"
                                jdbcType="TINYINT"
                                typeHandler="com.community.util.handler.AutoEnumTypeHandler"/>
            <columnOverride column="is_delete"
                            javaType="java.lang.Boolean"
                            jdbcType="TINYINT"
                            typeHandler="com.community.util.handler.AutoEnumTypeHandler"/>

生成的实体类中的类型字段:

2020-01-30-10-22-47

测试

首先向数据库插入一条信息, 之后进行查询, 并获取文章类型.

@Test
public void getValue() {
    BlogArtical artical = new BlogArtical();
    artical.setAuthorId(1);
    artical.setTitle("标题");
    artical.setDescription("描述");
    artical.setArticalType(ArticalTypeEnum.LIFE);
    artical.setIsDelete(false);
    artical.setContent("文本内容");
    blogArticalMapper.insertUseGeneratedKeys(artical);

    BlogArtical articalQueried = blogArticalMapper.selectByPrimaryKey(artical.getId());
    log.debug("输出文章类型信息: {}", articalQueried.getArticalType());
}

运行结果:

2020-01-30-10-14-46

通过这种方式, 我们的代码里将减少大量和数据库中类型有关的魔法值.

简化逻辑判断

不好的代码

注意下面打印日志的地方我们可以做很多的业务处理.

private static void dealWorkByAge(Integer age) {
    if (Objects.isNull(age) || age <= 0) {
        throw new IllegalArgumentException(String.format("[dealWorkByAge]年龄不能为空或小于等于0, age: [%s]", age));
    }

    if (age <= 20) {
        log.debug("人生得意马蹄急");
    } else if (age <= 50) {
        log.debug("人到中年不得已");
    } else if(age<=100) {
        log.debug("一寸光阴一寸金");
    } else {
        log.debug("阅尽尘世经风雨");
    }

}

转换为枚举方式处理

定义枚举类

@Slf4j
public enum AgeEnum {

    TEEN(20, t -> {
        log.debug("人生得意马蹄急");
    }),
    ADULT(50, t -> {
        log.debug("人到中年不得已");
    }),
    OLD(100, t -> {
        log.debug("一寸光阴一寸金");
    }),
    GOD(Integer.MAX_VALUE, t -> {
        log.debug("阅尽尘世经风雨");
    });


    public Integer age;
    public Consumer consumer;

    AgeEnum(Integer age, Consumer consumer) {
        this.age = age;
        this.consumer = consumer;
    }
}

使用枚举转换if逻辑处理:

private static void dealWorkByAgeEnum(Integer age) {
    if (Objects.isNull(age) || age <= 0) {
        throw new IllegalArgumentException(String.format("[dealWorkByAge]年龄不能为空或小于等于0, age: [%s]", age));
    }

    for (AgeEnum level : AgeEnum.values()) {
        if (age <= level.age) {
            level.consumer.accept(age);
            break;
        }
    }
}

这个例子不是很好, 需要注意的是: for循环中不会有if的那种优先级次序. 因此可能会导致业务处理出现问题. 比如输入age为10, 该参数在任何一个枚举类型中都是满足处理条件的. 因此最好将范围限制到为闭区间内.

另外再说明一点. 前端下拉框中的值如果传对应枚举类型的名字. 是可以自动转化成相应枚举类型的. 因此后端直接调用枚举相应的函数方法即可完成业务处理. 😃 是不是有点恍然大悟

函数式编程

2020-01-30-14-06-24
打开java.util.function包我们可以看到jdk提供的一些函数式接口. 主要有以下几种类型:

  1. Consumer 消费型. 输入一个参数, 没有返回值

    Consumer<Integer> consumer1 = t -> log.debug("consumer1====> " + t);
    Consumer<Integer> consumer2 = consumer1.andThen(t -> log.debug("consumer2====> " + t));
    
    consumer1.accept(1);
    consumer2.accept(2);
    
    

    2020-01-30-13-11-47

  2. Function 给定一个输入返回一个输出

    Function<Integer, String> function1 = t -> "======> " + t;
    Function<Integer, String> andThen = function1.andThen(t -> " =====> andThen: " + t);
    
    
    Function<Integer, String> compose = function1.compose(t -> t * 2);
    
    log.debug("compose 返回结果: [{}]", compose.apply(4));
    log.debug("andThen 返回结果: [{}]", andThen.apply(4));
    
    

    2020-01-30-13-26-36

    这里提及一下compose, 可以看一下下面的源码:

    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }
    

    很明显, 首先执行输入的函数并将其返回值作为输入. 执行过程是先执行输入, 后执行调用者.
    这和andThen正好是相反的. 如下是andThen的源码:

    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }
    

    该方法是将当前的结果作为输入函数的输入参数. 执行过程是先执行调用者, 后执行输入.

  3. UnaryOperator 输入并返回该类型, 继承Function<T, T>

    UnaryOperator<Integer> unary1 = t -> t * 2;
    log.debug("UnaryOperator 返回结果: [{}]", unary1.apply(2));
    
  4. Predicate 输入一个参数, 返回判断结果

    Predicate<Integer> predicate = t -> t > 10;
    log.debug("是否大于10: [{}]", predicate.test(20));
    Predicate<Integer> and = predicate.and(t -> t < 30);
    log.debug("是否大于10且小于30: [{}]", and.test(20));
    Predicate<Integer> or = predicate.or(t -> t > 50);
    log.debug("是否大于10或50: [{}]", or.test(20));
    Predicate<Integer> negate = predicate.negate();
    log.debug("negate: [{}]" , negate.test(20));
    

    2020-01-30-13-56-22

  5. Supplier 没有输入参数, 获取程序运行返回结果

    Supplier<Integer> supplier = () -> 10;
    log.debug("Supplier: [{}]", supplier.get());
    

自定义函数接口

如果我们的业务依赖的不止这些参数要如何处理呢?(多个参数输入见Bi****看名字也能猜到这种接口支持两个参数输入)

一种方法是使用compose, andThen进行组合处理, 上面我们在Function接口的测试代码中已经分析了, 不过这种适合于下一步与上一步的相依赖的情况. 另外一种方法就是定义自己的函数接口支持多参数输入.

定义函数

@FunctionalInterface
public interface MineFunction<A, B, C, D, R> {
    R exe(A a, B b, C c, D d);
}

@FunctionalInterface表示这是一个函数接口

测试

MineFunction<Integer, Integer, Integer, String, String> mine =
        (a, b, c, d) -> String.format("%d,%d,%d,%s", a,b,c,d);
log.debug("mine: [{}]" , mine.exe(1,2,3,"哈哈哈"));

结果

2020-01-30-14-24-46

是不是很简单. 😃
注意如果你的多个参数之间可以封装成一个对象的话为什么不进行封装呢?

end

2020-01-30-14-28-43
就到这吧, 谢谢.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值