需求
这一年下来,写两次工作流流转,总结下经验。
第一次写的时候,只找到用模版设计模式包裹一下,每个方法都做隔离,但是在具体分支实现的时候,if/else 满屏分,而且因为要针对不同情况,重复代码很多,但是if/else的条件又不一样,搞得我没办法用设计模式修改,想过用工厂模式重构。
一是没时间,二是工厂模式和策略模式基本上都用不来,
首先,工厂模式一定是if else分支较多,并且入参明确、固定。
策略模式也是不同的方法,实现不同的业务,入参明确、固定。
它们两者都不适合参数多一个少一个的情况,用起来只能说恶心自己。
并且由于设计模式的方法过多,时常debug需要嵌套跳转好几轮代码,就比较恶心。
这一年,闲下来我都会重构部分重复的代码,比如if else过多用设计模式优化,优化下来的感受是,没感觉可读性有多提高,反而感觉代码可读性变差了,有些案例的设计模式,很多情况没考虑到,比如较多重复代码,直接复用interface default里的方法,给我直观的体验是其他人来看这个代码,不太好理解。
还不如if else来的直接。
反正只要在if else上注释写清楚,管什么可读性。
设计模式做不到事
举个例子,当有个非常恶心的业务,需要在两层for循环里写if else,continue关键字是你贴心侍卫,常伴汝身。
你必须用continue它,艹,这个东西用设计模式就不合理,只能复用一些代码,放到一个方法里面去,什么两层for循环里,写个四五百行if else,调试都要好几天,我不知道要是业务出现变动,这个代码后面还怎么改。
新奇的思路
再次遇到工作流,吃过一次亏,不能走老路。
我选择网上冲浪,翻阅资料,最终找到一篇好用例子。
什么都没说,直接上项目,擦,一用才知道里面有坑。
原案例
- 定义流程节点
首先定义一个抽象类ProcessNode,表示工作流中的一个节点:
public abstract class ProcessNode {
private String nodeName; // 节点名称
private List<ProcessNode> nextNodes; // 后继节点
private boolean isEndNode; // 是否为结束节点
public ProcessNode(String nodeName) {
this.nodeName = nodeName;
this.nextNodes = new ArrayList<>();
this.isEndNode = false;
}
public String getNodeName() {
return nodeName;
}
public void setNodeName(String nodeName) {
this.nodeName = nodeName;
}
public List<ProcessNode> getNextNodes() {
return nextNodes;
}
public void setNextNodes(List<ProcessNode> nextNodes) {
this.nextNodes = nextNodes;
}
public boolean isEndNode() {
return isEndNode;
}
public void setEndNode(boolean endNode) {
isEndNode = endNode;
}
}
其中,nodeName表示节点名称,nextNodes表示后继节点,isEndNode表示是否为结束节点。
接着,定义两个子类StartNode和EndNode,分别表示工作流的起始节点和结束节点:
public class StartNode extends ProcessNode {
public StartNode() {
super("Start");
}
}
public class EndNode extends ProcessNode {
public EndNode() {
super("End");
setEndNode(true);
}
}
- 定义流程实例
定义一个ProcessInstance类,表示一次工作流的执行实例:
public class ProcessInstance {
private ProcessNode currentNode; // 当前节点
public ProcessInstance(ProcessNode startNode) {
this.currentNode = startNode;
}
public ProcessNode getCurrentNode() {
return currentNode;
}
public void setCurrentNode(ProcessNode currentNode) {
this.currentNode = currentNode;
}
}
其中,currentNode表示当前执行到的节点。
执行工作流
定义一个ProcessEngine类,表示工作流引擎。该类包括以下方法:
addNodes:添加节点
run:执行工作流
代码如下:
public class ProcessEngine {
private Map<String, ProcessNode> nodes; // 节点列表
public ProcessEngine() {
this.nodes = new HashMap<>();
}
/**
* 添加节点
*/
public void addNodes(ProcessNode... processNodes) {
for (ProcessNode node : processNodes) {
nodes.put(node.getNodeName(), node);
}
}
/**
* 执行工作流
*/
public void run(ProcessInstance instance) {
while (!instance.getCurrentNode().isEndNode()) {
ProcessNode currentNode = instance.getCurrentNode();
List<ProcessNode> nextNodes = currentNode.getNextNodes();
if (nextNodes.isEmpty()) {
throw new RuntimeException("No next node found.");
} else if (nextNodes.size() == 1) {
instance.setCurrentNode(nextNodes.get(0));
} else {
throw new RuntimeException("Multiple next nodes found.");
}
}
}
}
测试
使用以下代码测试上述工作流引擎的功能:
public static void main(String[] args) {
ProcessNode startNode = new StartNode();
ProcessNode approveNode = new ProcessNode("Approve");
ProcessNode endNode = new EndNode();
startNode.setNextNodes(Arrays.asList(approveNode));
approveNode.setNextNodes(Arrays.asList(endNode));
ProcessEngine engine = new ProcessEngine();
engine.addNodes(startNode, approveNode, endNode);
ProcessInstance instance = new ProcessInstance(startNode);
engine.run(instance);
System.out.println("流程执行完成。");
}
运行结果为:
流程执行完成。
填坑
这个案例没考虑到每个Node都是一个function,它需要一个执行function,处理业务逻辑。
怎么玩呢?
使用Function<T, R>特性
public class EndNode extends ProcessNode {
public EndNode() {
super("End");
setEndNode(true);
System.out.println("执行end的任务");
}
public Object executeMethod(Integer languageId, Function<Integer, Object> function) {
return function.apply(languageId);
}
}
用这种方式把参数传递进去,并业务流转。
然后结合模版模式,把每个abstract的function看做Node,这样就能按照工作流一个方法执行完,执行下一个方法。
public abstract class TestTemplate {
abstract Object handler(Integer languageId);
// ...
}
第二点,这里缺少一个上一个方法流转结束,返回结果参数作为下一个方法的入参,这里没处理好,这样就会导致某个业务节点失败,回退到上一个节点,取不到入参的问题。
两种解决思路
第一种就是这些返回结果参数,一定要做数据库保存,到进入下一个节点,那这个流转入参就可以删除;
第二种
使用全局Map,并且不使用单例模式,bean注入,而是new Object。(这个不建议)
我们把ProcessEngine的Run方法改到template里面来,
public abstract class TestTemplate {
abstract Object handler(Integer languageId);
// ...
public void init(Integer languageId) {
ProcessNode startNode = new StartNode();
ProcessNode endNode = new EndNode();
startNode.setNextNodes(Arrays.asList(endNode));
ProcessEngine engine = new ProcessEngine();
engine.addNodes(startNode, endNode);
ProcessInstance instance = new ProcessInstance(startNode);
run(languageId, instance);
}
/**
* 执行工作流
*/
private void run(Integer languageId, ProcessInstance instance) {
String queryJson = StringConstant.Symbol.BLANK;
while (!instance.getCurrentNode().isEndNode()) {
ProcessNode currentNode = instance.getCurrentNode();
List<ProcessNode> nextNodes = currentNode.getNextNodes();
if (nextNodes.isEmpty()) {
throw new RuntimeException("No next node found.");
}
if (nextNodes.size() != 1) {
throw new RuntimeException("Multiple next nodes found.");
}
instance.setCurrentNode(nextNodes.get(0));
if (currentNode instanceof StartNode) {
StartNode startNode = (StartNode) currentNode;
Object obj = startNode.executeMethod(languageId, this::handler);
queryJson = JacksonUtil.writeJson(obj);
} else if (currentNode instanceof EndNode) {
EndNode endNode = (EndNode) currentNode;
Object params = new Object();
if(StringUtils.isNotBlank(queryJson)) {
params = JacksonUtil.readJson(queryJson, Object.class);
} else {
// 从数据库读取上次function的结果参数
}
Object obj = endNode.executeMethod(params, this::handler);
queryJson = JacksonUtil.writeJson(obj);
}
// 以此类推 ...
}
}
}
追加内容 回退节点
噢,忘了写这个内容了,其实也简单,只需要在Node上加上节点。
@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class ProcessNode implements Serializable {
/**
* 节点名称
*/
private String nodeName;
/**
* 上继节点
*/
private List<ProcessNode> upNodes;
/**
* 后继节点
*/
private List<ProcessNode> nextNodes;
/**
* 是否为结束节点
*/
private boolean isEndNode;
public ProcessNode(String nodeName) {
this.nodeName = nodeName;
this.nextNodes = new ArrayList<>();
this.isEndNode = false;
}
}
至于在那个业务回退,就需要在对应的run调用设置上继节点就行,如有问题及时沟通。
关于优化
可以把那Node初始化放在static里,不过要做好toString重写,否则很容易触发栈溢出。
以上,就是今天的内容。