One of the primary reasons why businesses chose to use workflow engines is that they get pretty pictures that explain what is going on and look like they are easy to deal with. The truth is anything but that, but pretty sell.
My recommended solution for workflow has a lot going for it, if you are a developer. But if you’ll try to show a business analyst this code, they are likely to just throw their hands up in the air and give up. Where are the pretty pictures?
One of the main advantages of this kind of approach is that it is very rigid. You are handling things in the event handlers, registering the next step in the workflow, etc. All of which is very regimented. This is so for a reason. First, it make it very easy to look at the code and understand what is going on. Second, it allow us to process the code in additional ways.
Consider the following AST visitor, which operate over the same code.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
public class WorkflowGraphGenerator : Esprima.Utils.AstVisitor { StringBuilder _sb = new StringBuilder(); Stack<string> _current = new Stack<string>(); Stack<string> _conditionals = new Stack<string>(); private string _code; public string Graph => _sb.ToString() + "\r\n}"; public WorkflowGraphGenerator(string code) { _code = code; _sb.AppendLine("digraph G {"); } protected override void VisitIfStatement(Esprima.Ast.IfStatement ifStatement) { var text = _code.Substring(ifStatement.Test.Range.Start, ifStatement.Test.Range.End - ifStatement.Test.Range.Start); _conditionals.Push( text.Replace("\"", "\\\"") ); base.VisitIfStatement(ifStatement); _conditionals.Pop(); } protected override void VisitReturnStatement(Esprima.Ast.ReturnStatement returnStatement) { if(returnStatement.Argument is Esprima.Ast.Literal explain) { _sb.Append($"\"{_current.Peek()}\" -> \"{explain.StringValue}\" "); if (_conditionals.Count > 0) { _sb.Append("[ label=\"").AppendJoin(" AND ", _conditionals).Append("\" ]"); } _sb.AppendLine(";"); } base.VisitReturnStatement(returnStatement); } protected override void VisitCallExpression(Esprima.Ast.CallExpression callExpression) { var depth = _current.Count; if (callExpression.Callee is Esprima.Ast.Identifier id) { switch (id.Name) { case "on": if(callExpression.Arguments.Count > 0 && callExpression.Arguments[0] is Esprima.Ast.Literal onLabel) { _current.Push(onLabel.StringValue); } break; case "next": if (callExpression.Arguments.Count > 0 && callExpression.Arguments[0] is Esprima.Ast.Literal nextLabel) { _sb.Append($"\"{_current.Peek()}\" -> \"{nextLabel.StringValue}\" "); if(_conditionals.Count > 0) { _sb.Append("[ label=\"").AppendJoin(" AND ", _conditionals).Append("\" ]"); } _sb.AppendLine(";"); } break; default: break; } } base.VisitCallExpression(callExpression); if (depth != _current.Count) _current.Pop(); } }
This took me about twenty minutes to write, mostly to figure out the Graphviz notation. It take advantage of the fact that the structure of the code is predictable to generate the actual flow of actions from the code.
You get to use readable code and maintainable practices and show pretty pictures to the business people.