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

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

 

先说结果

框架效果:

  1. 规范了业务逻辑与校验逻辑的编写规则,实现了业务逻辑与校验逻辑的分离解耦
  2. 通过一组自定义特性,取代了原先大量的低价值代码
  3. 实现了校验逻辑的插件化,提高代码复用性,可维护性,可测试性

框架构成:

  1. 一个封装了校验记录构造器的校验基类
  2. 一组自定义校验特性,及其对应的处理类
  3. 一个校验接口,及一组配套的负责校验实现类插件化执行的类

 

说明

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

 

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

需要肯定的是:在满足业务及性能要求的前提下,代码量越少越好,过多的代码会降低代码可读性,增加维护成本。

 

另外,web框架集成了模型校验,为什么我不使用框架的模型校验,而要自定义一组校验特性呢, 是因为系统有一套结构化的校验信息,自定义特性是为了兼容现有的功能。

 

背景

系统简述:

这是一个养殖行业的生产管理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; }
}
 

总结

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

 

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

 

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

 

posted @ 2022-05-16 18:56  北京刘先生  阅读(2864)  评论(16编辑  收藏  举报