天天寫業務程式碼,我給擼了一個業務處理框架

北京劉先生 發表於 2022-05-16
框架

套用一個吸睛的說法“天天寫業務程式碼,如何成為技術大牛?”,分享一下自己在寫業務程式碼過程中,梳理出一個業務處理框架的過程。

 

說明

此框架是在處理業務過程中梳理出來的,並不具有通用性,這裡主要展示框架一步步產生的過程,可以通過其處理過程和思路,思考自己的處理方案。

 

經框架重構之後,原本一個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; }
}
 

總結

業務的複雜性是我們無法控制的,面對一個複雜的問題,如何通過對複雜的問題進行合理的劃分,拆解成多個相對簡單的問題,降低系統複雜性,從而減少對開發人員自身水平的依賴,減少開發人員工作強度,提升業務程式碼質量,是一個優秀的技術人能力的體現。

 

更高的程式碼質量和更快的開發效率,是我們一直追求的目標。

 

更好的複用,更簡單的維護,更清晰的結構,是我們應該遵循的原則。