本文由 简悦 SimpRead 转码, 原文地址 mp.weixin.qq.com
前言
前段时间面试了一个小伙子, 在问答环节他说 “我不要写业务代码, 太没意思。我只写基础代码或者框架代码”。
不愿写业务代码几乎是程序员的共性。乏味、不易复用、大概率没完没了的需求变更, 还没太大成就感。
但程序员是个非常自由的职业, 自由的体现在于:你可以把大量的精力和时间用在写自己感兴趣的代码 (可能它并不紧急), 用少量的时间去应付真实需求。你让我搬砖, 没问题, 但我得先造个轮子。
业务需求
现在你接到这样一个需求:给你一个字符串, 格式为_编号 | 省份 | 地市 | 公司名称_, 有如下约定:
字符串格式不正确, 视为异常数据;
省份名称错误, 视为异常数据;
公司名称少于 3 个字的, 视为干扰数据;
编号重复或少于 8 位的, 视为干扰数据;
地市名称错误的, 修改为 “其他”, 视为有效数据;
其他情况为有效数据;
对于异常数据, 要保存到异常表, 并记录详细的异常原因; 对于有效数据, 要保存到业务表; 对于干扰数据, 直接过滤。
清晰的业务, 简单的代码:
/**
* 处理字符串-分析并保存 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, 非常无趣。
所以我们要搞点事情, 让工作有点乐趣。比如说分析一下流程, 提取一些和具体业务无关的东西, 并尝试能否复用到其他的场景。
提取逻辑
其实工作中有大量的类似需求:对一个对象做转换、修改、过滤, 根据结果做不同处理。
转换的定义
输入一个对象, 输出另一个对象; 结果状态是成功 or 失败;修改的定义
输入一个对象, 输出同一个对象但可以对它做一些赋值; 结果状态是成功 or 失败;过滤的定义
输入一个对象, 输出对它的业务判断; 结果状态是符合 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);
}
这里有几个细节需要注意:
LcFunction、LcConsumer 接口的定义跟 jdk 的没啥区别, 就是方法签名上多了一个 throw Exception, 就不再单独给出源码了。之所以没有使用 jdk 原生的, 这是因为真实的业务方法是可以抛出 Exception, 而我们会处理。
filter 方法的参数是用的是原生的 Predicate, 原因也很直观, 我们不希望业务的过滤方法里有异常抛出, 如果真抛出了, 那就不在业务规则范围里了。自己处理去吧~
对 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 自身, 这意味着我们可以做链式调用。
具体的实现也很简单:
结果状态不对就啥也不干;
有异常就吃掉;
定义一个 IgnoredException 来做兼容;
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
现实场景下, 我们是不可能只接入这么一条数据的, 我们要处理的是一堆的数据文件。怎么玩?下期推文见。
全文 (完)