套用一个吸睛的说法“天天写业务代码,如何成为技术大牛?”,分享一下自己在写业务代码过程中,梳理出一个业务处理框架的过程。
先说结果
框架效果:
- 规范了业务逻辑与校验逻辑的编写规则,实现了业务逻辑与校验逻辑的分离解耦
- 通过一组自定义特性,取代了原先大量的低价值代码
- 实现了校验逻辑的插件化,提高代码复用性,可维护性,可测试性
框架构成:
- 一个封装了校验记录构造器的校验基类
- 一组自定义校验特性,及其对应的处理类
- 一个校验接口,及一组配套的负责校验实现类插件化执行的类
说明
此框架是在处理业务过程中梳理出来的,并不具有通用性,这里主要展示框架一步步产生的过程,可以通过其处理过程和思路,思考自己的处理方案。
经框架重构之后,原本一个500多行的处理逻辑,像一碗面条,各种穿插调用,最终可简化到只有300多行代码,而且各部分彼此分离,结构清晰。
需要肯定的是:在满足业务及性能要求的前提下,代码量越少越好,过多的代码会降低代码可读性,增加维护成本。
另外,web框架集成了模型校验,为什么我不使用框架的模型校验,而要自定义一组校验特性呢, 是因为系统有一套结构化的校验信息,自定义特性是为了兼容现有的功能。
背景
系统简述:
这是一个养殖行业的生产管理SaaS系统,可简单理解为:猪场管理系统
几个概念:
简单提一下这三个概念,因为后续所有操作都围绕这3个概念展开。
业务逻辑:处理核心业务的逻辑
校验逻辑:处理数据校验的逻辑
校验记录构造器:用于承载校验结果的结构化的校验结果对象集合(在代码中为:ErrorRecordBuilder,ErrorRowBuilder)
类图1
最初的样子
业务逻辑和校验逻辑穿插调用,不分彼此,高度耦合,代码特点是:
- 业务逻辑处理类负责“校验记录构造器”(ErrorRecordBuilder,ErrorRowBuilder)创建和维护。
- 存在大量重复的,低价值的校验代码,如:必填项校验,数值范围校验,数据格式校验,等。代码中充满了if...else...判断
- 多个业务模块,共用一个校验逻辑处理类,而不是根据领域模型划分校验逻辑。校验逻辑臃肿,职责不清。
- 业务逻辑处理类中包含校验逻辑,职责划分不清晰。
下图是此时业务逻辑与校验逻辑的交互流程
流程图1
第一次改进:提取校验基类(VerificationBase)
创建一个校验记录构造器需要7行代码,而且校验记录构造器放在业务处理类中,是那么格格不入(最小知识原则),于是,第一步,便从这里下手。
提取校验基类(VerificationBase),明确业务逻辑处理类与校验逻辑处理类的职责,
校验基类隐藏了校验记录构造器的创建细节,具体模块的校验处理类不用关心校验记录构造器的具体构建过程,简化“校验记录构造器”对象的创建及使用,校验逻辑只需要通过几个简单的属性和方法操作校验记录构造器。
类图2
下图是此时业务逻辑与校验逻辑的交互流程(可与“流程图1”对比)
流程图2
第二次改进:通过自定义特性(Attribute)处理通用校验
针对于系统中存在的大量的重复的,低价值的if...else...校验判断的情况,采用了自定义特性进行校验的方案。
提取非业务型通用校验逻辑,通过自定义特性(Attribute)处理通用校验,简化通用校验代码,规范校验逻辑。
目前已经完成的自定义特性包括:
必填项校验:Required
浮点数格式及范围校验:Number
整数格式及范围校验:Integer
时间类型数据校验:DateTime
集合数据某字段数值唯一性校验:Unique
集合数据某字段数值重复校验:Repetition
字符串校验(包括最大字符长度校验,正则校验):String
举两个例子:
1.必填项校验
之前判断必填的代码是:
if (productID.IsNullOrEmpty()) { errorRowBuilder.AddColumn(importLine.ToErrorColumn(o => o.RequisitionObject, ErrorCode.NoContent.GetIntValue())); }
使用自定义特性校验必填项,只需在对应属性上添加[Required]即可
[Required] public string ProductId { get; set; }
简单提一下这三个概念,因为后续所有操作都围绕着3个概念展开。
之前数值校验逻辑的通常的写法是: (数据以字符串形式提交)
//数量 if (!decimal.TryParse(importLine.Quantity.SafeString(), out decimal quantity) || !(quantity > 0 && quantity <= 999999.99M) ) { errorRowBuilder.AddColumn(importLine.ToErrorColumn(o => o.Quantity, CommonErrorCode.OutRangeNumber.GetIntValue()) .AddFormat("$ENTITY.Quantity", quantity.ToString()) .AddFormat("$MinValue", "0") .AddFormat("$MaxValue", "999999.99")); } else { line.Quantity = quantity; }
使用自定义特性校验数值,只需在对应属性上添加[Number]即可
[Number] public string Quantity { get; set; }
由以上的示例可以看到,使用自定义特性对通用逻辑进行校验,可以极大的减少代码量,并且保证校验逻辑的一致性,避免由于不同开发人员的不同开发习惯,造成逻辑判断上的差异。
在某些场景下,使用自定义特性进行校验判断,能够减少50%以上的校验代码量。
需要强调一下:在满足业务及性能要求的前提下,代码量越少越好,过多的代码会降低代码可读性,增加维护成本。
第三次改进:提取通用业务校验逻辑,业务校验逻辑插件化
这一步提取通用业务校验逻辑,实现通用业务校验逻辑插件化。
在第二次改进工作中,解决的是非业务型校验的问题(数值范围,必填项等),实际代码中还有许多与业务相关的通用校验逻辑,比如人员校验,单据操作权限校验,等,这些校验几乎每个单据都会用到,将其提取为通用处理逻辑,并实现插件化,是这次改进的目标。
以人员校验(Person)为例,展示业务型校验插件化的实现细节。
主要有以下4点:
- IVerificationProvider是自定义校验接口,是实现校验处理插件化的基础。
- 定义特性[Person],标记字段为需要进行人员校验。
- QlwPersonVerificationProvider为实际校验处理逻辑,基本逻辑为:读取标记了[Person]的属性值,判断其是否符合当前操作要求。
- 将QlwPersonVerificationProvider注册到公共校验逻辑处理类中,在执行校验时,会自动调用,完成校验处理。
QlwPersonVerificationProvider同时实现了IVerificationProvider, IPersonVerificationProvider,是由于业务上需要处理除校验之外的其他逻辑,这里不做讨论。
类图3
第四次改进:彻底分离业务逻辑与校验逻辑,实现校验逻辑插件化
第三次改进是实现通用校验逻辑的插件化,每个业务中业务逻辑与校验逻辑还是存在相互调用,这一次,彻底分离业务逻辑与校验逻辑,实现校验逻辑插件化。
到这里,我们提出一个问题:业务逻辑与校验逻辑之间,需要相互关联吗?
显然是不需要的,校验逻辑仅需要Command就可以完成校验处理,而业务逻辑处理本身也不需关心具体校验逻辑。
为此,将代码结构进一步改造,遵循AOP的处理思想,基于MediatR管道处理方式,将校验类以插件的形式,注册到Command中,在调用Handler之前,自动执行校验逻辑,校验通过之后,再执行Handler,否则抛出校验异常信息,中断程序执行。
从代码结构角度来看,业务处理类和校验类之间彻底解耦,代码复杂度降低。
从开发人员角度来看,只需要独立编写校验代码和业务处理代码,而不需要关心校验代码是在何时被调用。
下图是此时业务逻辑与校验逻辑的交互流程(可与最开始的“类图1”对比)
类图4
下图是此时业务逻辑与校验逻辑的交互流程(可与“流程图2”对比)
流程图3
这种逻辑拆分,从表面上看,是将代码从一个地方转移到了另一个地方,深层的意义在于解耦,降低业务代码复杂度,提高代码可读性和可维护性。
拆分之后,在编写校验逻辑代码时,不需要关心具体业务逻辑如何实现,同样在编写业务逻辑代码时,也不需要关心校验逻辑如何处理,从而让开发人员的关注点更集中,业务处理更简单。
Command注册校验类的代码示例:
public class MaterialReceiveUpdateCommand : AutoVerificationCommandBase, IRequest<Result> { /// <summary> /// 构造函数中,注册需要的校验类 /// </summary> public MaterialReceiveUpdateCommand() { base.Clear(); base.Register<AuditVerificationProvider>(); base.Register<MaterialReceiveUpdateValidate>(); } /// <summary> /// 主流水号 /// </summary> [Required] [NumericalOrder] public string NumericalOrder { get; set; } }
总结
业务的复杂性是我们无法控制的,面对一个复杂的问题,如何通过对复杂的问题进行合理的划分,拆解成多个相对简单的问题,降低系统复杂性,从而减少对开发人员自身水平的依赖,减少开发人员工作强度,提升业务代码质量,是一个优秀的技术人能力的体现。
更高的代码质量和更快的开发效率,是我们一直追求的目标。
更好的复用,更简单的维护,更清晰的结构,是我们应该遵循的原则。