将通用性的技术逻辑与差异性的业务逻辑相分离
背景
很多业务代码,将通用性的技术逻辑与差异性的业务逻辑混杂在一起,这样的做法导致:
- 业务意图不容易很快识别出来,或者要费力思考业务语义;
- 当要复用相同的处理逻辑时,则不得不复制一份。
开发人员常常要花更多的力气去理解业务代码,隐形之中增加了很多理解与维护成本。遗憾的是,很多开发人员还并没有充分意识到这一点,甚至觉得这无伤大雅。
实际上,这个细微的问题,反映的却是开发人员普遍缺乏设计性的思考。 如果一个开发人员重视设计思维,他就会发现,这不仅仅是编程细节,而是可以运用设计去掌控的事情。
那么,如何将通用性的技术逻辑与差异性的业务逻辑相分离呢? 首先,要明白什么是技术逻辑,什么是业务逻辑。
比如:遍历一个商品对象列表,取出所有商品的标题列表。
-
技术逻辑:通常是一些通用处理,比如遍历一个对象列表,取出对象中的某个属性。 我并不关心是什么对象列表,或对象的什么属性。 我只关心遍历列表及如何取出对象中的某个属性。 技术逻辑通常是需要对客户端屏蔽的底层细节。
-
业务逻辑: 通常是差异性的处理,比如商品列表与商品标题(可能是JSON中的某个字段)。 我并不关心如何遍历列表或取出对象属性的技术细节,我只关心商品及商品标题。业务逻辑通常是领域需要重点关注的领域知识,要清晰地凸显出来。
下面,将以一个示例来说明,如何将将通用性的技术逻辑与差异性的业务逻辑相分离。
示例
请看下面这段从主流程里抽出来的代码片段。代码清单一:
List<Integer> packIds = orderDeliveryResult.getData().stream()
.filter(x->x.getDeliveryState() == 1 || x.getDeliveryState() == 2 )
.map(x->x.getId()).collect(Collectors.toList());
你能一眼看出这段代码的含义吗? 哦,看上去是要拿到一个包裹ID 列表,可是 x.getDeliveryState() == 1 || x.getDeliveryState() == 2
是什么意思呢? 你得去找作者沟通一下了。
这段代码是将技术逻辑和业务逻辑混杂在一起的典型例子。对于业务语义来说,实际上并不关心 stream, filter, map 这种技术细节。下面看看改写后会是什么样:
List<Integer> packIds = getDeliveredPackIds(orderDeliveryResult.getData());
private List<Integer> getDeliveredPackIds(List<OrderDelivery> orderDelivery) {
return StreamUtil.filterAndMap(orderExpresses, oe -> isDelivered(oe), OrderDelivery::getId);
}
private boolean isDelivered(OrderDelivery oe) {
return oe.getDeliveryState() == 1 || oe.getDeliveryState() == 2;
}
public class StreamUtil {
private StreamUtil() {}
public static <T,R> List<R> map(List<T> dataList, Function<T,R> getData) {
if (CollectionUtils.isEmpty(dataList)) { return new ArrayList(); }
return dataList.stream().map(getData).collect(Collectors.toList());
}
public static<T,R> List<R> filterAndMap(List<T> dataList, Predicate<? super T> predicate , Function<T,R> getData) {
if (CollectionUtils.isEmpty(dataList)) { return new ArrayList(); }
return dataList.stream().filter(predicate).map(getData).collect(Collectors.toList());
}
}
改写后:
-
原来主流程里的代码变成了一行: getDeliveredPackIds(orderDeliveryList); 业务语义凸显出来了: 哦,原来是要拿到已发货的包裹列表。一目了然,在理解主流程时也不需要切换到理解这种细节层面的东西了。
-
原来的 stream, filter, map 被分离到 StreamUtil 工具类中,后面可以反复使用,并且在实现业务逻辑时,再也不需要关心如何遍历列表、过滤条件、拿到返回值列表这种技术细节了。
-
分离出了领域知识:isDelivered。 这个实际上应该写到 orderDelivery 类中。这样
oe -> isDelivered(oe)
写成更简洁的形式:OrderDelivery::isDelivered
。
新的实现方式,实现了一箭三雕:
- 凸显业务语义 ;
- 代码复用;
- 沉淀领域知识。
这就是将通用技术逻辑与业务逻辑分离的三大重要益处!
反思
语言特性
Java8 提供了 stream ,于是开发人员在应用系统里到处留下了 stream 的足迹; 可是你理解其初衷吗?
stream 的目的是能够以一种声明式的方式清晰地凸显意图,然而,开发人员很快就将意图扔到一边,只图写法的快感,根本没有体现出这种语言特性的初衷。 也许 Java8 的提供者,应该多提供一个 StreamUtil 的类,才会让开发人员意识到其根本目的所在。
应当深思语言特性提供的根本目的,并加以适当包装,以便让客户端用更清晰、易懂、便利的方式去表达现实流程和规则,而不是贪图品尝一下新鲜特性。
设计思维
为什么说体现了设计思维呢?
可扩展性设计,本质上是将变化与不变分离。 技术逻辑代表了通用不变的部分,而业务逻辑代表了复杂多变的部分。将通用性的技术逻辑与差异性的业务逻辑相分离,本质上体现了可扩展性设计思维。虽然微小的编程问题并不足以体现其优势,但确确实实能很好地锻炼可扩展性设计思维。这种思维可以通过每一次的“将通用性的技术逻辑与差异性的业务逻辑相分离”的思考和实践进行强化。
远方,即在足下。
领域思维
领域思维,实际上是一个附加效果。当把通用性的技术逻辑提炼出来后,就能更清晰地看到所要表达的内容,即:领域知识和领域规则。这能让开发人员更容易意识到真正的关注点所在,而不是沉溺于技术和编程细节。
同时,当清晰地看到领域知识和领域规则的时候,就会思考将它放置在合适的位置,比如实体能力或领域服务, 而不是淹没在流程代码里。
小结
将通用性的技术逻辑与差异性的业务逻辑分离,实现了一箭三雕:凸显业务语义 ;代码复用;沉淀领域知识。 编程表达的小小差异,不仅仅体现出设计思维,还体现了一个开发人员是否对领域知识有敏锐的感知。这种微小的差异是很难察觉出来的,但可以日积月累、积微知著,一旦面对大规模业务系统时,就会体现出它的可贵之处。