如何優雅的使用枚舉

從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
就到這吧, 謝謝.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章