Skip to content

本文由 简悦 SimpRead 转码, 原文地址 mp.weixin.qq.com

前言

前段时间面试了一个小伙子, 在问答环节他说 “我不要写业务代码, 太没意思。我只写基础代码或者框架代码”。
        不愿写业务代码几乎是程序员的共性。乏味、不易复用、大概率没完没了的需求变更, 还没太大成就感。
        但程序员是个非常自由的职业, 自由的体现在于:你可以把大量的精力和时间用在写自己感兴趣的代码 (可能它并不紧急), 用少量的时间去应付真实需求。你让我搬砖, 没问题, 但我得先造个轮子。

业务需求

现在你接到这样一个需求:给你一个字符串, 格式为_编号 | 省份 | 地市 | 公司名称_, 有如下约定:

  1. 字符串格式不正确, 视为异常数据;

  2. 省份名称错误, 视为异常数据;

  3. 公司名称少于 3 个字的, 视为干扰数据;

  4. 编号重复或少于 8 位的, 视为干扰数据;

  5. 地市名称错误的, 修改为 “其他”, 视为有效数据;

  6. 其他情况为有效数据;

对于异常数据, 要保存到异常表, 并记录详细的异常原因; 对于有效数据, 要保存到业务表; 对于干扰数据, 直接过滤。

清晰的业务, 简单的代码:

/**
 * 处理字符串-分析并保存 V1.0
 * @param line 格式化的字符串
 * @return
 */
public void handle(String line) {
    try {
        EntityData data = Util.convert(line);
        Assert.isTrue(Util.isCorrectProvince(data.getProvince()), "省份错误");
        if(Util.length(data.getCompany()) < 3) {
            return;
        }
        if(!Util.isCorrectCity(data.getCity())){
            data.setCity("其他");
        }
        Util.saveEntity(data);
    } catch (FormatException e) {
        Util.saveException(line, e);
    } catch (LackIdException | DuplicateIdException ignored) {

    }
}   

你说这代码有问题吗?完全没问题!分分钟完成工作。但这就是程序员最不喜欢写的业务代码, if-else、try-catch, 非常无趣。
        所以我们要搞点事情, 让工作有点乐趣。比如说分析一下流程, 提取一些和具体业务无关的东西, 并尝试能否复用到其他的场景。

提取逻辑

其实工作中有大量的类似需求:对一个对象做转换、修改、过滤, 根据结果做不同处理。

  1. 转换的定义
    输入一个对象, 输出另一个对象; 结果状态是成功 or 失败;

  2. 修改的定义
    输入一个对象, 输出同一个对象但可以对它做一些赋值; 结果状态是成功 or 失败;

  3. 过滤的定义
    输入一个对象, 输出对它的业务判断; 结果状态是符合 or 不符合;

符合可以视为成功, 不符合可以视为忽略, 所以对一个对象的处理, 我们一共有三种结果状态:成功、失败、忽略。对了, 如果是失败的话我们还要有一个失败原因, 我们称之为异常。
        好了, 我们现在需要一个新的数据结构来装载我们的业务诉求, 首先它有一个对象引用; 其次它有一个异常引用; 最后它有一个结果状态; 就像一个盒子
ValueBox.java

/**
 * 数据包装
 * @author lucifer.chan
 **/
@Getter @Slf4j
public class ValueBox<T> {
    //数据
    private T value;
    //结果状态枚举 SUCCESS, FAILED, IGNORED
    private ResultType resultType;
    //异常
    private Exception exception;

    public static <T> ValueBox<T> newInstance(T value) {
        return new ValueBox<>(value, ResultType.SUCCESS, null);
    }

    private ValueBox(T value, ResultType resultType, Exception exception) {
        this.value = value;
        this.resultType = resultType;
        this.exception = exception;
    }
}

它还需要一些简单的方法来阐述我们的业务操作:转换、修改、过滤。以及几个快速达成目的的方法。

/**
 * 是否失败
 */
public boolean isFailed() {
    return resultType == ResultType.FAILED;
}

/**
 * 是否忽略
 */
public boolean isIgnored() {
    return resultType == ResultType.IGNORED;
}

/**
 * 转换
 */
<U> ValueBox<U> map(LcFunction<? super T, ? extends U> mapper) throws Exception {
    return new ValueBox<>(mapper.apply(value), resultType, exception);
}

/**
 * 修改
 */
ValueBox<T> peek(LcConsumer<? super T> consumer) throws Exception {
    consumer.accept(value);
    return this;
}

/**
 * 过滤
 */
ValueBox<T> filter(Predicate<? super T> predicate) {
    if (!predicate.test(value)) {
        this.resultType = ResultType.IGNORED;
    }
    return this;
}

/**
 * 快速忽略
 */
<U> ValueBox<U> ignored() {
    //noinspection unchecked
    return (ValueBox<U>)filter(t -> false);
}

/**
 * 快速异常
 */
<U> ValueBox<U> failed(Exception e) {
    return new ValueBox<>(null, original, ResultType.FAILED, e);
}

/**
 * 快速异常
 */
<U> ValueBox<U> failed() {
    return new ValueBox<>(null, original, ResultType.FAILED, exception);
}

这里有几个细节需要注意:

  1. LcFunction、LcConsumer 接口的定义跟 jdk 的没啥区别, 就是方法签名上多了一个 throw Exception, 就不再单独给出源码了。之所以没有使用 jdk 原生的, 这是因为真实的业务方法是可以抛出 Exception, 而我们会处理。

  2. filter 方法的参数是用的是原生的 Predicate, 原因也很直观, 我们不希望业务的过滤方法里有异常抛出, 如果真抛出了, 那就不在业务规则范围里了。自己处理去吧~

  3. 对 value 有实际操作的方法 map、peek、filter 我们定义成 “包可见”, 这样可以对其它类非可见, 因为实际上, 我们也不希望被不受信者乱操作。另外, 显然我们还需要再定一个操作类:ValueOperator

ValueOperator

/**
 * 数据操作
 * @author lucifer.chan
 **/
@Slf4j
public class ValueOperator<T> {

    private ValueBox<T> box;

    private ValueOperator(T value) {
        this.box = ValueBox.newInstance(value);
    }

    private ValueOperator(ValueBox<T> box){
        this.box = box;
    }

    /**
     * 开始
     */
    public static <T> ValueOperator<T> start(T value){
        return new ValueOperator<>(value);
    }
    
    /**
     * 结束
     */
    public ValueBox<T> end(){
        return box;
    }
    
    //...
}

ValueOperator#map

/**
 * 转换
 * 1、失败的返回一个新的无值的box,并添加原先的异常信息
 * 2、忽略的返回一个新的忽略
 * 3、转换失败的看异常类型
 *      若是IgnoredException,则返回一个状态为ignored的空箱
 *      若是其他异常,则返回一个状态为failed的空箱,并添加转换失败的异常信息
 *
 * @param mapper 执行方法
 * @return
 */
public <U> ValueOperator<U> map(LcFunction<? super T, ? extends U> mapper){

    if(box.isFailed()) {
        return new ValueOperator<>(box.failed());
    }

    if(box.isIgnored()){
        return new ValueOperator<>(box.ignored());
    }

    try {
        return new ValueOperator<>(box.map(mapper));
    } catch (Exception e){
        log.error("错误类型[{}],错误内容:[{}]", e.getClass().getName(), e.getMessage());
        if(e instanceof IgnoredException){
            return new ValueOperator<>(box.ignored());
        }
        return new ValueOperator<>(box.failed(e));
    }
}

ValueOperator#peek

/**
 * 修改
 * 1、失败的不执行
 * 2、忽略的不执行
 *
 * @param consumer 执行方法
 * @return
 */
public ValueOperator<T> peek(LcConsumer<T> consumer){
    if(box.isFailed() || box.isIgnored()) {
        return this;
    }

    try {
        box = box.peek(consumer);
    } catch (Exception e){
        log.error("错误类型[{}],错误内容:[{}}", e.getClass().getName(), e.getMessage());
        if(e instanceof IgnoredException){
            box = box.ignored();
        } else {
            box = box.failed(e);
        }
    }
    return this;
}

ValueOperator#filter

/**
 * 过滤
 * 1、失败的不能过滤
 * 2、忽略的不能过滤
 *
 * @param predicate 执行方法
 * @return
 */
public ValueOperator<T> filter(Predicate<? super T> predicate) {
    if(!box.isFailed() && !box.isIgnored()){
        box = box.filter(predicate);
    }
    return this;
}

start 方法为静态方法, 也是唯一入口, 从这里我们就可以开始对数据做各种处理了, map、peek、filter 的返回类型都是 ValueOperator 自身, 这意味着我们可以做链式调用。

具体的实现也很简单:

  1. 结果状态不对就啥也不干;

  2. 有异常就吃掉;

  3. 定义一个 IgnoredException 来做兼容;

  4. end 返回最终的 ValueBox;

让我们回到最初的需求, 来看看使用它写出来的代码是怎样的:

public void handle(String line) {
    ValueBox<EntityData> box = ValueOperator.start(line)
            .map(Util::convert)
            .peek(data -> Assert.isTrue(Util.isCorrectProvince(data.getProvince()), "省份名称错误"))
            .filter(data -> Util.length(data.getCompany()) >= 3)
            .peek(data -> {
                if(!Util.isCorrectCity(data.getCity())){
                    data.setCity("其他");
                }
            })
            .peek(Util::saveEntity)
            .end();

    if(box.isFailed()){
        Util.saveException(line, box.getException());
    }
}

‍        这代码看起来是不是很有趣?有点像 Stream, 又有点像 Apache Camel 的 DSL, 但似乎除了骚气一点, 并没什么卵用, 你说咱费这劲整它有啥用?

Why?

首先, 它好玩。工作除了糊口, 最重要的是乐趣, 我们定义了一个和业务完全无关的处理流程, 然后将具体业务套了进去。折腾了半天, 虽然没有提高半点的执行效率, 但代码看起来就很拉风!
        第二, 暗藏玄机, 用 1 分钟时间思考一下如果升级一下 ValueOperator 的那三个方法, 我们可以做些什么?没错, 可以在执行业务方法前做一些事情, 业务方法执行之后再做一些事情, 这不就是拦截器吗?如果再插入一些监听, 你叫它 AOP 都没太大问题啊。
        第三, 如果我们改写那三个方法的实现, 将参数对象塞到队列里, 再在 end 方法里做优化调用, 这里面的想象空间就大了。
        而最关键的是, 做这些事情的同时, 真实的业务代码完全不需要调整, 这不就是框架要干的事情吗?

And then

现实场景下, 我们是不可能只接入这么一条数据的, 我们要处理的是一堆的数据文件。怎么玩?下期推文见。

全文 (完)

Released under the MIT License.