套用一個吸睛的說法“天天寫業務程式碼,如何成為技術大牛?”,分享一下自己在寫業務程式碼過程中,梳理出一個業務處理框架的過程。
說明
此框架是在處理業務過程中梳理出來的,並不具有通用性,這裡主要展示框架一步步產生的過程,可以通過其處理過程和思路,思考自己的處理方案。
經框架重構之後,原本一個500多行的處理邏輯,像一碗麵條,各種穿插呼叫,最終可簡化到只有300多行程式碼,而且各部分彼此分離,結構清晰。
背景
系統簡述:
這是一個養殖行業的生產管理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; } }
總結
業務的複雜性是我們無法控制的,面對一個複雜的問題,如何通過對複雜的問題進行合理的劃分,拆解成多個相對簡單的問題,降低系統複雜性,從而減少對開發人員自身水平的依賴,減少開發人員工作強度,提升業務程式碼質量,是一個優秀的技術人能力的體現。
更高的程式碼質量和更快的開發效率,是我們一直追求的目標。
更好的複用,更簡單的維護,更清晰的結構,是我們應該遵循的原則。