过程驱动的代码范式
在面向对象大张旗鼓的今天,大多数人对面向过程编程嗤之以鼻,但有些场景使用过程驱动的编程思路,反而能更好地描述业务规则以及业务流程,比如前台表达的渲染链路,或是章节一中 比较重的领域服务,使用过程驱动能更好地描述数据处理的过程以及产品用例流程。
上图能力库中的能力点是我们过程驱动最核心的部分,是我们对能力的抽象,一般一个能力只沉淀一个具体的原子方法,并决策流程是否能执行下去;往上是阶段划分,不同的能力在具体的业务流程中,是处于不同阶段的,这里的阶段划分是指从流程阶段的维度对能力点进行分类并放在不同的包中,让我们的流程更加清晰;再往上是不同场景下我们对能力点的执行链路编排,并对外做统一输出。
2.1 能力点
能力点通常由一个接口定义,入参是执行上下文,出参是流程是否需要继续。
public interface AbilityNode<C extends Context> {
/**
* 执行一个渲染节点
* @param context 执行上下文
* @return 是否还需要继续往下执行
*/
boolean execute(C context);
基于这个接口,我们可以实现非常多原子能力节点。java中,这些能力节点作为bean由spring容器统一管理,运行时取出即用。
在一个业务场景下,我们的能力点往往会非常多,那么我们就需要对他们进行基于业务场景的阶段划分,并分门管理;比如我们在前台投放场的实践中,按照召回、补全、过滤、排序、渲染,划分了五个阶段,每一个能力点被归类到其中一个阶段中进行管理。
2.2 能力编排
有了能力点,我们需要基于编排引擎将这些能力串联起来,用以描述业务规则或是业务流程;这些能力执行的过程有些仅仅是可串行执行的,有些是也可以并行执行的,下面给出一套通用的流程协议以及实现过程:
public abstract class AbilityChain<C extends Context<?>> {
@Resource
private ThreadPool threadPool;
public abstract C initContext(Request request);
public Response execute(Request request) {
//获取渲染上下文
C ctx;
try {
ctx = this.initContext(request);
if (ctx == null || ctx.getOrder() == null || CollectionUtils.isEmpty(ctx.getOrder().getNodeNames())) {
return null;
}
} catch (Throwable t) {
log.error("{} catch an exception when getRenderContext, request={}, e="
, this.getClass().getName(), JSON.toJSONString(requestItem), t);
return null;
}
try {
//执行所有节点
for (List<String> nodes : ctx.getOrder().getNodeNames()) {
List<AbilityNode<C>> renderNodes = renderNodeContainer.getAbilityNodes(nodes);
boolean isContinue = true;
if (renderNodes.size() > 1) {
//并发执行多个节点
isContinue = this.concurrentExecute(renderNodes, ctx);
} else if (renderNodes.size() == 1){
isContinue = this.serialExecute(renderNodes, ctx);
}
if (!isContinue) {
break;
}
}
return ctx.getResponse();
} catch (Throwable t) {
log.error("RenderChain.execute catch an exception, e=", t);
return null;
}
}
/**
* 并发执行多个node,如果不需要继续进行了则返回false,否则返回true
*/
private boolean concurrentExecute(List<AbilityNode<C>> nodes, C ctx) {
if (CollectionUtils.isEmpty(nodes)) {
return true;
}
long start = System.currentTimeMillis();
Set<Boolean> isContinue = Sets.newConcurrentHashSet();
List<Runnable> nodeFuncList = nodes.stream()
.filter(Objects::nonNull)
.map(node -> (Runnable)() -> isContinue.add(this.executePerNode(node, ctx)))
.collect(Collectors.toList());
linkThreadPool.concurrentExecuteWaitFinish(nodeFuncList);
//没有node认为不继续,就是要继续
return !isContinue.contains(false);
}
/**
* 串行执行多个node,如果不需要继续进行了则返回false,否则返回true
*/
private boolean serialExecute(List<AbilityNode<C>> nodes, C ctx) {
if (CollectionUtils.isEmpty(nodes)) {
return true;
}
for (AbilityNode<C> node : nodes) {
if (node == null) {
continue;
}
boolean isContinue = this.executePerNode(node, ctx);
if (!isContinue) {
//不再继续执行了
return false;
}
}
return true;
}
/**
* 执行单个渲染节点
*/
private boolean executePerNode(AbilityNode<C> node, C context) {
if (node == null || context == null) {
return false;
}
try {
boolean isContinue = node.execute(context);
if (!isContinue) {
context.track("return false, stop render!");
}
return isContinue;
} catch (Throwable t) {
log.error("{} catch an exception, e=", nodeClazz, t);
throw t;
}
}
}
其中流程协议为:
[
[
"Ability1"
],
[
"Ability3",
"Ability4",
"Ability6"
],
[
"Ability5"
]
]
其中Ablitiy3 4 6表示需要并发执行,Ability1、3/4/6、5表示需要串行执行。
2.3 切面
有了能力点和流程编排引擎,基于过程编码的代码骨架就已经有了;但是往往我们还需要一些无法沉淀为能力的逻辑,需要在每个节点(或指定节点)执行前/后进行,这就需要切面的能力;
如图,比如流程埋点、预校验就非常适合放到切面这一层实现。下图是我们在实践中,基于切面实现的能力点追踪,可以清晰地看到每个能力点执行的过程以及数据变更情况。
2.4 包结构实践
该包结构即描述了上述的所有模块,chain目录下为多个业务场景流程,围绕着外层的AbilityNode和AbilityChain进行实现,编排出符合业务场景逻辑的流程。
完成代码示例
package com.xxx.wlop.xxx;
import com.alibaba.fastjson.JSON;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import lombok.Data;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Slf4j
public class ProcessTest {
@Data
public static class Context{
Order order;
Response response;
public void track(String msg){
log.error(msg);
}
}
@Data
public static class Order {
List<List<String>> nodeNames;
}
public static class Request{}
public static class Response{}
public static interface AbilityNode<C extends Context>{
/**
* 执行一个渲染节点
* @param context 执行上下文
* @return 是否还需要继续往下执行
*/
boolean execute(C context);
}
/**
* 渲染节点
*/
public static class RenderAbilityNode implements AbilityNode<Context>{
/**
* 执行一个渲染节点
*
* @param context 执行上下文
* @return 是否还需要继续往下执行
*/
@Override
public boolean execute(Context context) {
return true;
}
}
public static class FilterAbilityNode implements AbilityNode<Context>{
/**
* 执行一个过滤节点
*
* @param context 执行上下文
* @return 是否还需要继续往下执行
*/
@Override
public boolean execute(Context context) {
return true;
}
}
public abstract static class AbilityChain<C extends Context>{
public abstract C initContext(Request request);
private ExecutorService executorService = Executors.newFixedThreadPool(10);
public Response execute(Request request){
C ctx;
try{
ctx = initContext(request);
if (ctx == null || ctx.getOrder() == null || CollectionUtils.isEmpty(ctx.getOrder().getNodeNames())) {
return null;
}
}catch (Throwable t){
log.error("{} catch an exception when getContext, request={}, e="
, this.getClass().getName(), JSON.toJSONString(request), t);
return null;
}
try{
for (List<String> nodes : ctx.getOrder().getNodeNames()) {
List<AbilityNode<C>> abilityNodes = getAbilityNodes(nodes);
boolean isContinue = true;
if (abilityNodes.size() == 1) {
isContinue = this.serialExecute(abilityNodes, ctx);
}else{
// 多线程并发处理
isContinue = this.concurrentExecute(abilityNodes,ctx);
}
if (!isContinue) {
break;
}
}
return ctx.getResponse();
}catch (Throwable t){
log.error("RenderChain.execute catch an exception, e=", t);
return null;
}
}
@SneakyThrows
public List<AbilityNode<C>> getAbilityNodes(List<String> nodes){
List<AbilityNode<C>> nodeList = Lists.newArrayList();
for (String node : nodes) {
ClassLoader classLoader = getClass().getClassLoader();
Class<?> aClass = Class.forName(node, true, classLoader);
AbilityNode<C> nodeInstance = (AbilityNode<C>) aClass.getDeclaredConstructor().newInstance();
nodeList.add(nodeInstance);
}
return nodeList;
}
private boolean concurrentExecute(List<AbilityNode<C>> nodes,C ctx){
if (CollectionUtils.isEmpty(nodes)) {
return true;
}
long start = System.currentTimeMillis();
Set<Boolean> isContinue = Sets.newConcurrentHashSet();
CountDownLatch latch = new CountDownLatch(nodes.size());
for (AbilityNode<C> node : nodes) {
executorService.submit(() -> {
try {
isContinue.add(this.executePerNode(node, ctx));
} finally {
latch.countDown();
}
});
}
try {
latch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
long end = System.currentTimeMillis();
log.info("Concurrent execution took " + (end - start) + " ms");
//没有node认为不继续,就是要继续
return !isContinue.contains(false);
}
private boolean serialExecute(List<AbilityNode<C>> nodes,C ctx){
if (CollectionUtils.isEmpty(nodes)) {
return true;
}
for (AbilityNode<C> node : nodes) {
if (node == null) {
continue;
}
boolean isContinue = this.executePerNode(node, ctx);
if (!isContinue) {
//不再继续执行了
return false;
}
}
return true;
}
private boolean executePerNode(AbilityNode<C> node, C context) {
if (node == null || context == null) {
return false;
}
try {
boolean isContinue = node.execute(context);
if (!isContinue) {
context.track("return false, stop render!");
}
return isContinue;
} catch (Throwable t) {
log.error("{} catch an exception, e=", node.getClass().getSimpleName(), t);
throw t;
}
}
}
public static class DefaultAbilityChain extends AbilityChain<Context>{
@Override
public Context initContext(Request request) {
Order order = new Order();
order.setNodeNames(Lists.newArrayList(
Lists.newArrayList("com.jd.wlop.alpha.ProcessTest$FilterAbilityNode"),
Lists.newArrayList("com.jd.wlop.alpha.ProcessTest$RenderAbilityNode")
));
Context context = new Context();
context.setOrder(order);
return context;
}
}
public static void main(String[] args) {
Request request = new Request();
DefaultAbilityChain defaultAbilityChain = new DefaultAbilityChain();
Response response = defaultAbilityChain.execute(request);
}
}