天天写业务代码,我给撸了一个业务处理框架

套用一个吸睛的说法“天天写业务代码,如何成为技术大牛?”,分享一下自己在写业务代码过程中,梳理出一个业务处理框架的过程。

 

说明

此框架是在处理业务过程中梳理出来的,并不具有通用性,这里主要展示框架一步步产生的过程,可以通过其处理过程和思路,思考自己的处理方案。

 

经框架重构之后,原本一个500多行的处理逻辑,像一碗面条,各种穿插调用,最终可简化到只有300多行代码,而且各部分彼此分离,结构清晰。

 

背景

系统简述:

这是一个养殖行业的生产管理SaaS系统,可简单理解为:猪场管理系统

 

几个概念:

简单提一下这三个概念,因为后续所有操作都围绕这3个概念展开。

业务逻辑:处理核心业务的逻辑

校验逻辑:处理数据校验的逻辑

校验记录构造器:用于承载校验结果的结构化的校验结果对象集合(在代码中为:ErrorRecordBuilder,ErrorRowBuilder)

 

天天写业务代码,我给撸了一个业务处理框架

类图1

 

最初的样子

业务逻辑和校验逻辑穿插调用,不分彼此,高度耦合,代码特点是:

  1. 业务逻辑处理类负责“校验记录构造器”(ErrorRecordBuilder,ErrorRowBuilder)创建和维护。
  2. 存在大量重复的,低价值的校验代码,如:必填项校验,数值范围校验,数据格式校验,等。代码中充满了if...else...判断
  3. 多个业务模块,共用一个校验逻辑处理类,而不是根据领域模型划分校验逻辑。校验逻辑臃肿,职责不清。
  4. 业务逻辑处理类中包含校验逻辑,职责划分不清晰。

 

下图是此时业务逻辑与校验逻辑的交互流程

天天写业务代码,我给撸了一个业务处理框架

流程图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点:

  1. IVerificationProvider是自定义校验接口,是实现校验处理插件化的基础。
  2. 定义特性[Person],标记字段为需要进行人员校验。
  3. QlwPersonVerificationProvider为实际校验处理逻辑,基本逻辑为:读取标记了[Person]的属性值,判断其是否符合当前操作要求。
  4. 将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; } }

 

总结

业务的复杂性是我们无法控制的,面对一个复杂的问题,如何通过对复杂的问题进行合理的划分,拆解成多个相对简单的问题,降低系统复杂性,从而减少对开发人员自身水平的依赖,减少开发人员工作强度,提升业务代码质量,是一个优秀的技术人能力的体现。

 

更高的代码质量和更快的开发效率,是我们一直追求的目标。

 

更好的复用,更简单的维护,更清晰的结构,是我们应该遵循的原则。

 

发表评论

相关文章