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


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

    先说结果

    框架效果:

    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; }
    }
     

    总结

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

     

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

     

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

     

  • 相关阅读:
    poj 1141 Brackets Sequence (区间DP)
    算法:冒泡排序
    Learning English From Android Source Code:2 Ampersand
    泛泰A870刷4.4专用英文版非触摸CWM Recovery 6.0.4.8(三版通刷)
    Android Studio 那些事|Activity文件前标识图标显示为 j 而是 c
    HDU-4930 Fighting the Landlords 多校训练赛斗地主
    POJ 3211 Washing Clothes(01背包)
    C# winform ListView 的右键菜单的下级菜单的选项视情况禁用方法
    Android系统开发(4)——Autotools
    Android系统开发(3)——Makefile的编写
  • 原文地址:https://www.cnblogs.com/flame7/p/16266510.html
Copyright © 2020-2023  润新知