对错误码的设计思考
本文从工作中的具体实践出发,介绍自己对错误码的一些设计思考。下面将从问题背景、需求分析、设计实践这三个方面来分别阐述。
问题背景
抛开具体的业务处理逻辑,这个问题可以抽象为两种模式:
报文头和报文体一致模式
这种模式,是由前端往后台发送请求得到响应,由前端负责封包解包。这里的报头和报体具有相同的数据组织格式和逻辑层级,其逻辑一致性由业务服务来保证,具体体现如下:
- 如果报头中的响应码出错,那报头的响应信息包含错误描述信息,报文体为空
- 如果报头中的响应码正确,那报头的响应信息为空,报文体是正确的业务数据,且数据格式与报头一致。
报文头和报文体不一致的模式
这种模式,常见于在现有服务中新增一个中转服务,专门用来对接第三方服务。由前端负责对接数据的拆包和解包,中转后台服务只处理网络层的收发逻辑,不关心具体数据内容。
由于第三方服务和中转服务都存在错误可能,因此,前端如何解析响应就需要好好设计。
需求分析
从使用者的角度来看,期望能够从请求响应获取以下三个接口:
- 该响应是成功还是失败?
- 如果失败,失败的错误码是多少?
- 如果失败,失败的错误描述信息是什么?
使用者使用接口1
来进行业务分支判断。一般来说,成功则显示响应数据,失败则通过接口2
和接口3
来提示错误信息。其中,错误码是便于开发定位原因,错误信息是便于最终用户知晓错误情况。
设计实践
这里以接入第三方服务返回json格式的响应数据来举例:一般格式如下:
{ // 响应成功
code: 0,
msg: "",
data: [XXX]
}
{ // 响应失败
code : 1000,
msg: "XXX"
data:{}
}
通过前面的分析以及具体实践,在与同事讨论中,对于接口2
存在的必要性上有分歧。为什么呢?因为在现有系统中,报文头和报文体一致模式和不一致模式都存在,且都存在错误的情况,在提示用户时,在是否需要显示错误码这一点上,产品和开发意见有差异。
- 对于报文和报体数据一致的场景,
接口2
的数据来源唯一,且可以保证准确一致。 - 对于不一致的场景,
接口2
的数据来源有多种,具体有以下三种情况:- 中转层的错误码和错误信息
- 第三方服务返回正常,但业务错误
- 第三方服务返回异常,这里可能有:格式错误、无期望的字段等
对于后者来说,错误信息来源也存在上述问题。按照单一职责的原则来设计,会是这样:
struct ErrorInfo
{
int nErrCode;
string nErrMsg;
}
struct ErrorInfo m_ProxyLayer; // 中转层错误信息
struct ErrorInfo m_ThirdLayer; // 第三方服务层的错误信息
bool bParseFlag; // 解析json是否成功
bool bExpectField; // 期望字段是否存在
由于在不同层级的错误不会同时发生,且错误码有优先级别,照此设计可使得职责清晰,缺点在于有多种设置错误方式,且在获取错误码和错误信息时,要依据错误码的优先级来处理。
个人看法,在该场景下,有两种妥协方案,在违反单一职责原则的前提下,提供更好的封装和使用。
方案一:在前端响应时,使用如下数据接口来保存错误信息:
struct ErrorInfo
{
int nErrCode;
string nErrMsg;
void setErrorInfo(int nErrorCode, const string& strErrMsg)
{
this->nErrCode = nErrorCode;
this->strErrMsg = strErrMsg;
}
bool IsRspRight(){
return 0 != nErrCode;
}
int GetErrCode(){return nErrCode;}
const string& GetErrMsg(){return nErrMsg;}const
}
使用单一错误码以及错误信息来保存不同层级的错误信息,与上述职责分离的方案相比,设置和获取错误使用统一接口,简单直接。因为不同层级的错误不会同时发生,即使出现相同的错误码,也只会返回优先级最高的错误码以及错误信息。该方案的缺点在于复用错误码以及错误信息,上层不能依据错误信息做后续的业务判断,因为它的含义已经不再单一。
方案二:在中转层处理,针对第三方服务的指令,提供指令适配器,将其转换为前端可统一解包的格式。好处在前端可按照之前封包解包逻辑进行处理,无需关系后端对接类型,按照中转层提供的接口传参即可。缺点在于中转层承担对接第三方服务的全部工作以及增加指令适配工作,一旦出现异常,所有依赖该中转层的指令都会失败
小结
针对以上场景,提出了职责分离和职责复用两种处理方式,在此抛砖引玉,大家有什么其他想法,可以在评论区一起讨论。