业务思考-异步业务组织
在最近的开发过程中,遇到这样一个问题,细想蛮有意思的,在此记录一下。
在进行具体讨论前,先将业务抽象,方便后续讨论。比方说,有一个业务Business
,它依赖三个子命令A
,B
和C
,每个命令都是异步收发且有先后顺序要求,暂且以A->B->C
为依赖关系,每个指令都可能出错(响应超时或者响应错误),并且只要有一个出错,后续请求不必再发送,直接返回错误。
以上是业务背景,下面来看下如何组织上述逻辑关系。
代码组织方式
按照异步收发框架以及需求,这块逻辑,可以这样写:
void Business()
{ // 第一步,发送命令A
SendCommandA();
}
// 底层异步响应回调函数
void OnReply(CReq* pReq)
{
switch(pReq)
{
case CommandA:
{ // 收到A响应后,正确则继续发送B命令,错误则回复错误。
pReq->isRespRight() ? SendCommandB() : OnFinish(ECheckResult_Error, "指令A的错误信息");
}
break;
case CommandB:
{
pReq->isRespRight() ? SendCommandC() : OnFinish(ECheckResult_Error, "指令B的错误信息");
}
break;
case CommandC:
{
pReq->isRespRight() ? OnFinish(ECheckResult_OK, "正确结果") : OnFinish(ECheckResult_Error, "指令B的错误信息");
}
break;
default:
assert(0); // 未知情况
break;
}
}
上述写法,直观明了,前一个指令响应成功,再发下一个指令,否则提示错误。这种写法不好之处在于指令与指令之间耦合太紧。改进方案是利用消息来拆分调用关系。例如收到CommandA
的正确响应,发出一个类似OnReceiveCmdA
的异步消息。在消息中,带上CommandA
指令响应的相关信息。业务自身订阅该消息并进行后续处理。这样一来,可降低命令之间的依赖,增加灵活性。
这里,我想提到的是另外一点,在工程中,看到其他同事有写这样的响应处理:
// 其他的省略
case CommandA:
{
if (pReq->isRespRight())
{
OnFinish(EQryResult_CommandA_OK, pReq->GetRightRet());
}
else
{
OnFinish(EQryResult_CommandA_SysError, pReq->GetErrorMsg());
}
}
break;
}
enum EQryResult
{
EQryResult_CommandA_OK,
EQryResult_CommandB_OK,
// ...
EQryResult_CommandA_SysError
}
void OnFinish(EQryResult eRet, string& strMsg)
{
switch(eRet)
{
case EQryResult_CommandA_OK:
{
SendCommandB();
}
break;
case EQryResult_CommandA_SysError:
{
// tip user
}
breakl
}
}
上面这种组织逻辑的思路,是针对每种情况,无论是正确的还是错误的,都定义对应的类型码,提供一个统一的OnFinish
处理函数入口。通过入参,配合类型码来完成后续的操作。
这种组织逻辑的方法,笔者感觉不好,思考了下,有以下几点不好的地方:
- 混淆职责。这种将正确处理和错误处理放在同一个函数中,混淆不同的职责。笔者认为,在这种异步处理,错误可以统一处理,但正确情况,各个指令有各自的应用场景,不适合统一处理。
- 可扩展性差。后续随着业务的变动,可能会修改已有分支或者新增分支,不管如何,这样的修改,对于测试、运维或者产品来说,属于黑盒修改,即使在修改说明中明确了改动方法,这也只稍微缓解了测试人员的测试焦虑,增加后续的测试成本。
- 可读性差。在异步消息处理时,上下文信息很重要,上面这种做法,在响应时又封装一层,看似方便了调用方,实则苦了实现方和阅读方。这种只简化了调用方的做法,隐藏后续紧密关联的业务处理,让阅读者要跳入到
OnFinish
函数,再根据具体入参,找到对应的处理分支,才能检查正确性,这种封装,降低代码可读性,不推荐。
小结
这种异步处理,建议错误情况统一处理,正确情况,通过消息来隔离各个具体的处理流程,每一个具体的处理,用类似OnRespCommandA
这样的函数包裹起来,明确表明职责,这是较为稳妥的组织方法。