本篇文章关注于编程实践中的相关流程设计内容,内容来源自己过去的工作总结。
业务流程设计
越复杂流程,越容易出错。为了减少出错的情况,需要提取并且封装通用逻辑,用一个易于理解的名字来对外提供服务。在业务不同抽象层级上,干各自职责对应的事情。
前后台交互的流程越多,需要维护的状态就越多,出现问题的概率就越大,因此在不影响主要功能的前提下,流程能简化就尽量简化,那些被简化的路径,在某些异常场景下,会对业务有一定的影响,具体影响到什么程序,在上线前,谁也不好说。可通过数据埋点得到上线后的真实数据,以供权衡。
针对复杂业务时,不能一上手就开始写,要先设计好框架和整体流程。对于一些需求上面没有明确的交互细节,一般按照通用交互细节来就可以。
对于具体子功能的实现上,需要细致考虑,较好的实践方法是逐条列出来,在每种状态或情况下,什么样的输入得到什么样的输出。
在复杂程序执行过程中,需要维护多个状态,随着业务的改变,在处理于状态关联的事件时,思维负担会越来越重重,很容易顾此失彼。
为了更好的控制复杂度,简化逻辑,需要对状态进行分级管理,将每个级别的状态和对应处理分层化。对外统一状态操作接口,每个模块中,都通过统一状态接口来管理状态。
每一个常量数字,在业务上都要有依据来源,要么是经验值,要么是业务规则规定的。
失败流程
任何失败重试类操作,都要考虑对后台的影响。因为网关只会转发请求,大量冗余的请求会给后台造成巨大压力,可能会影响正常业务流程。对于失败重试,一定要设置定时间隔以及重试次数,这里的失败要区分类型,如果是业务操作类的失败,业务层直接提示出错即可。如果是其他类型的失败,底层需要做好重试策略,直到重试策略返回失败,才可认为是失败,此过程对上层是透明的。
针对开发回发的数据,要进行有效性校验才能继续往下走,在有些业务场景中,需要对不同类型的失败分配不同类的错误码,以便外部处理。比如读取配置信息这个功能,可能的出错就有以下几种情况:
- 配置文件不存在
- 配置文件存在,但文件格式不正确
- 配置文件存在,文件格式正确,属性Key缺失
- 配置文件存在,文件格式正确,属性Key正常,属性Value缺失
- 配置文件存在,文件格式正确,属性Key正常,属性Value是异常值(超大的数,或者是负数)
如果因为上述种种原因,导致读取配置文件失败,那要做好默认配置的处理,根据产品要求,对不同的异常情况,做出对应的处理,使用默认配置继续运行,还是弹框提示用户等。
在对后台返回来的格式化数据进行解析时,一定要考虑对应字段不存在的情况下该如何处理。不管后台返回什么类型的错误数据,都不能崩溃。
流程优化
仔细分析业务流程,避免不必要的依赖和操作,相关的业务逻辑,要聚合到一起。
分析问题的思路要不断在实践中打磨,反思,总结。自己第一时间想到的方案,往往还有很大的优化空间,那么,从什么角度去思考优化呢?在开发实践中,逐渐明确两个大的方向,在此记录一下
从底层往上层思考 : 底层是最基础的数据层,业务流程的数据是不是可以往下移动到底层,然后调用底层明确性语义接口函数来实现上层业务,而不是把一堆判断逻辑都交由业务层去做。
从上层往底层思考 : 在业务层和底层数据之间,要有一个间接层,对上提供较为高级的功能,对下封装最底层数据,提供更高层次的接口。
相似的业务处理采用相似的办法,不要一堆条件,范围A-C的用这样的处理方法,业务范围D-F的用那样的处理方法,其他类型业务用其他的处理方法。这样一来,业务和对应的错误处理分散在各处,后期维护很容易出错。
一个类,如需向其他类获得相关信息,不要依赖于类与类之间的继承组织关系,尝试通过this
来进行强制转换来获取,因为这种层级关系在未来可能会发生变动,而这种层级关系的变动,不会通知使用者,因此在实现类似需求时,需要将对外部的依赖关系尽可能的降低,建议采用消息的方式,每层规定好消息的职责,一层一层向上传递请求,直到某一层给与响应为止。
一致性
一致性,指的是对于UI元素和代码命名的一致性,同一逻辑元素的命名一致性,它在UI层、中间层、网络层以及后台接口中的核心词汇要保持一致,这样增加代码的可读性。
一致性体现在资源管理上,同一份资源,谁申请,谁就负责释放。谁增加引用记数,谁就负责减少。
在一些通用功能的设置上,保持代码一致性很重要。比如设置控件字体,原生系统会提供一套接口,自绘部分也会提供一套接口,那么外部在使用时,就会有疑惑,设置字体是用原生接口还是自绘接口呢?他们会不会相互影响呢?在前期设计时,同一类功能,只提供一套接口较好。
相似操作能够归集到一起的,一定要归集到一起,让逻辑聚合,而不是到处分散。
修改影响评估
比如,你修改了A函数中的A分支,A函数的B分支没有改动。外部O1模块使用了A函数的B分支,O2模块使用了A函数的A分支,那么你的本次修改涉及到的影响范围是哪些呢?O2模块肯定是要测的,O1模块需不需要提测?答案是需要的,从广义上来看,任何调用A函数的模块都要重测一片。即使本次修改没有涉及到分支,也要去测试。
在尝试修改后台程序时,要注意与之前系统的兼容性。
在修改一个涉及范围比较大的问题时,如果涉及的地方太多了,为了最终问题的解决和测试方面的平滑过渡,需要将问题分治,按照功能需求模块来统计汇总,然后,将问题列表按模块分配给各自负责人,由他们去进一步往下分散进行修改,这样可以保证大型问题的平滑过渡,为开发和测试提供缓冲时间段。
比如本次修改点可分为以下几个集合,
集合1:涉及到哪些方面
集合2:涉及到哪些方面
集合3:涉及到哪些方面
逻辑完备性
对于层级调用,资源的申请和释放,要特别注意一致。这一点,特别容易遗漏。
比如说,先压入缓存,再发出请求,那么,清理缓存的时机,应该要覆盖发出请求后的每一条执行路径。
7.1 发出请求失败
7.2 发出请求成功,响应成功
7.3 发出请求成功,响应失败
一段业务代码,正确的路径只有一条,但错误的路径有很多条,怎么处理在这个过程中有可能出现的错误,就很显的功力所在了。
如果遇到一些需要特殊处理的地方,优先把大部分情况放在主干逻辑上,在额外的分支中处理额外。
凡是涉及到网络请求的,如果中途关闭了页面,一定要注意请求资源的清理操作,这样在重新打开页面,不会收到上一次无效的响应。
优化前的准备
没有profile的优化都是瞎扯淡,一旦开始重构,就要指定好优化目标,所有的相关修改都聚焦在目标上,不能跑偏了。优化前要分析现状、制定方案和可量化的目标,完成优化后,需要提取相关的数据,总结对比可量化的分析优化成果。后台重构需要配置A/B方案,对于可能会有变化的模块或者业务,或者A、B方案不好取舍的,可采用后台配置采用哪个方案,当重构的版本上线时,先保留旧的模块,如果有问题,可以在后台中配置切回到旧的模块,这种思想同可以同灰度发布一起使用(5W,10W)
在进行组件的重构时,需要遵循最小影响范围来做,一处重构,尽量只影响到这个业务,不影响到其他业务,保证可控性和可测试性。
维护经验
对于接手旧项目,里面用到的第三方库,如果有新版本的接口库或者更好的第三方接口库,该如何抉择?
- 如果原有的库能够满足需求,则不要贸然替换或者升级
- 如果原有的库不能满足特定场景下的需求,优先自己封装在特定场景下的接口,以供使用。如果特定场景下的需求需要的新功能不能通过封装已有API来提供,而该功能在原有库的升级版中存在,那么需要和原有库维护者商量讨论后再进行升级。
- 用新的三方库来替换,该方法改动范围会很大,不到万不得已,不允许使用。
当老旧代码中有很多相互交缠的逻辑,需要改动多处才能改好词Bug,不好评估改动影响的范围,这个时候,为了最大限度的兼容老旧代码,不能继续在老旧代码的基础上继续填坑、挖坑,如果选取其中一个简单的界面,用清晰简单的逻辑去实现,然后在逐步替换老旧模块,小步慢跑的改进。
当需要在原有功能上面增加新的功能时,要特别注意一点,就是原有功能所使用的指令和流程不能更改。比如查询A产品的天数信息,原有的指令只支持一种产品的查询,现在其他界面有同时查询多种产品的情况,而原有指令只实现了查询单个产品信息, 那么,下面有两种解决方法:
优点 缺点
方法一:修改原有指令,使其支持多条信息的查询 增加指令的功能 需修改原有查询单条产品的页面,增加了测试成本
方法二:不改动原有指令,新增支持多条信息查询指令 不会影响已有功能 类似功能有多处实现,查多条包含查一条的功能
因为查询一条产品信息属于查询多条产品信息的特例,为了可维护性,采用方法一较好。
一个简答的权限判断功能设计
有A、B、C三个账号,有ID1,ID2,ID3,ID4四个功能,每个账号允许的功能如下:
A--> ID1, ID3
B--> ID2, ID3
C--> ID4
现在要设计一个权限判断的函数,给定功能ID和账号,返回是否允许执行此操作判断?
一种是从上往下,从入口ID和各个可操作ID段来判断,优点是直观,简单,缺点是对扩展性不好,未来要增加新的入口时,需要修改多出
方法一: 从上到下,逻辑简单直接,可扩展性差
bool IsAllow(Account acnt, int nFunId)
{
if (A == acnt)
{
if (nFunId == ID1 || nFunId == ID3)
{
return TRUE;
}
}
else if (B == acnt)
{
if (nFunId == ID2 || nFunId == ID3)
{
return TRUE;
}
}
else if (C == acnt)
{
if (nFunId == ID4)
{
return TRUE;
}
}
return FALSE;
}
另一种方法是从下往上,将每个交易ID和能够操作条件集合在一起,这种方式的扩展性和可读性都非常好,推荐使用。
struct TIDInfo
{
int nId;
vector<Account> vAllowAcntType; // 该ID要求的账号属性
bool IsAllow(Account acnt) // 给定账号是否存在特定属性
{
return vAllowAcntType.find(acnt) != vAllowAcntType.end();
}
}
vector<TIDInfo> allIdInfo; // 所有功能ID集合
bool IsAllow(Account acnt, int nFunId)
{
for (int i = 0 ; i < allIdInfo.size(); ++i)
{
if (allIdInfo[i].nId == nFunId)
{
return allIdInfo[i].IsAllow(acnt);
}
}
return FALSE;
}