領域模型設計為復雜問題的解決提供了一套方法,但其理論往往非常抽象,本系列文單旨在提供一些最佳實踐。您需要首先認識到,軟件的設計過程主觀性很強,我希望能夠提供一個設計思想讓您在入門中有一個感性的認識,莫要陷入到“教條主義”中。
領域驅動設計:強調的是戰略,是宏觀的,它為復雜業務的解決提供了指導思想。在實踐中,無論是“面向過程”還是“面向對象”的設計方式,都是領域驅動思路的一種實現方式,要根據不同的場景使用不同的方式,請您不要陷入自我懷疑,否認“面向過程”。
程序研發過程中,往往涉及到驗證。不論是采用哪種實現方式(面向過程的方式或者對象驅動),原則上,公有方法中的第一件事情是驗證參數。何時,何地,如何做驗證是一個開發者要面臨的挑戰。做一個合格的軟件設計師(在這里,我喜歡用設計師而非碼農或程序員,那是對於自己的不尊重),當您想要寫一份干凈、整潔及可讀性很強的代碼時,細節上的轉變會讓您的作品看起來更加賞心悅目、更有自信、代碼可維護性更強。
代碼是給人看的,不是機器
軟件設計不存在固定的規則,如果您在開發過程始終堅持某些原則就產生了規則
1、基於視圖模型的驗證
在展開書寫之前,我們需要假設一個非常簡單的業務場景:用戶的每一個操作,都需要為其增加一個操作日志。在此處,我們遵循如下設計原則:1)涉及新建、更新、復雜查詢業務時,Service層公有方法都接收視圖模型作為參數,而非拆成一個個獨立的參數;2)所有公有方法的要做的第一件事是驗證。視圖模型代碼如下。
public class OperationLogInfo { private String module; private String operatorId; private String operatorName; private String action; public String getModule() { return module; } public void setModule(String module) { this.module = module; } public String getOperatorId() { return operatorId; } public void setOperatorId(String operatorId) { this.operatorId = operatorId; } public String getOperatorName() { return operatorName; } public void setOperatorName(String operatorName) { this.operatorName = operatorName; } public String getAction() { return action; } public void setAction(String action) { this.action = action; } }
接下來為Service層代碼。
@Service public class OperationLogService { private static Logger logger = LoggerFactory.getLogger(OperationLogService.class); /** * 保存操作日志 * @param operationLogInfo 操作日志信息 */ public void save(OperationLogInfo operationLogInfo) { try { if (operationLogInfo == null) { throw new IllegalArgumentException(OperationMessages.NO_OPERATION_LOG); } if (StringUtils.isEmpty(operationLogInfo.getModule())) { throw new IllegalArgumentException(OperationMessages.NO_MODULE_INFO); } if (StringUtils.isEmpty(operationLogInfo.getOperatorId())) { throw new IllegalArgumentException(OperationMessages.NO_OPERATOR_ID_TYPE); } if (StringUtils.isEmpty(operationLogInfo.getOperatorName())) { throw new IllegalArgumentException(OperationMessages.NO_OPERATOR_NAME_TYPE); } if (StringUtils.isEmpty(operationLogInfo.getAction())) { throw new IllegalArgumentException(OperationMessages.NO_ACTION_TYPE); } OperationLogDataEntity entity = OperationLogDataEntity.of(operationLogInfo); this.operationLogDao.save(entity); } catch (IllegalArgumentException e) { logger.error(e.getMessage(), e); } catch (Exception e) { logger.error(OperationMessages.SAVE_LOG_FAILED, e); } } }
為了保持代碼的干凈,我引入了一個新的類”OperationMessages“,將所有的”報錯信息“或者”操作提示“以靜態常量的形式保存在一個統一的地方供后續代碼引用。如果您的服務中有多個包,建議為每一個包中都加入這樣一個包含常量的類,用於區分不同業務的操作提示。下面為這個常量的代碼片段。
/** * 操作提示 */ final public class OperationMessages { public static final String NO_OPERATION_LOG = "無操作日志信息"; }
返回到我的應用服務代碼,嗯……整體來看比較整潔,不過代碼讀起來不舒服,70%全是驗證,我的業務代碼已經淹沒在驗證的海洋里。還好只有4個字段的信息,否則……讓我們來優化一下,把驗證類代碼全部移到一個方法中。
@Service public class OperationLogService { private static Logger logger = LoggerFactory.getLogger(OperationLogService.class); /** * 保存操作日志 * @param operationLogInfo 操作日志信息 */ public void save(OperationLogInfo operationLogInfo) { try { this.validate(operationLogInfo); OperationLogDataEntity entity = OperationLogDataEntity.of(operationLogInfo); this.operationLogDao.save(entity); } catch (IllegalArgumentException e) { logger.error(e.getMessage(), e); } catch (Exception e) { logger.error(OperationMessages.SAVE_LOG_FAILED, e); } } private void validate(OperationLogInfo operationLogInfo) { if (operationLogInfo == null) { throw new IllegalArgumentException(OperationMessages.NO_OPERATION_LOG); } if (StringUtils.isEmpty(operationLogInfo.getModule())) { throw new IllegalArgumentException(OperationMessages.NO_MODULE_INFO); } if (StringUtils.isEmpty(operationLogInfo.getOperatorId())) { throw new IllegalArgumentException(OperationMessages.NO_OPERATOR_ID_TYPE); } if (StringUtils.isEmpty(operationLogInfo.getOperatorName())) { throw new IllegalArgumentException(OperationMessages.NO_OPERATOR_NAME_TYPE); } if (StringUtils.isEmpty(operationLogInfo.getAction())) { throw new IllegalArgumentException(OperationMessages.NO_ACTION_TYPE); } } }
假如我需要在類”OperationLogInfo“中再加入一個新的字段,對應的”validate“方法也需要變更。希望你的記性好一點不要忘了這處的變更,要不然可能要開始”事故報告“之旅,誰知道您的DB中是否設置了字段必填或有某個長度的限制。而外,從信息內聚的角度來看,把驗證代碼寫到服務層貌似差了一點,畢竟類”OperationLogInfo“中有什么字段,只有它自己最知道,所以我們希望它可以承擔“信息專家”的角色。因此,我們把驗證的方法上升到DTO模型“OperationLogInfo”中。 此外,考慮到參數驗證大多數時是必需的,所以我們做一個視圖模型的父類。
“信息專家”:給對象分配職責時,應該把職責分配給具有完成該職責所需要信息的那個類
“視圖模型”:對於視圖模型,相信每個人都會有自己的理解,比較通用的解釋是“承載用於在頁面上顯示的信息的模型”。但我個人對於視圖模型有另外的解釋。舉一些例子:“相親時,我想要呈現給對方一些關於自己的信息”,“買書的時候,封面上通常會有一些內容的介紹”。這些都可以被稱之為我的或書的視圖,是某個對象想要讓另外的對象了解自身情況的一種信息載體。假如,訂單模塊如果要獲取賬戶模塊的信息,最好可通過獲取對方的視圖模型來實現。這里面存在一個設計的技巧:涉及到兩個包之間的交互,建議都通過視圖模型來實現;在一個包內的,如果代碼內聚性很好,使用數據模型也很方便。這種通過視圖模型交互的方式,有利於后續項目的拆分。比如早期項目要求快速研發,訂單模型與賬戶模塊在一個單體項目中,后續如果想把這兩個模塊分離,由於視圖模型的存在,拆分工作會非常簡易。這里存在另外一個原則“不同的包之間,只能通過Service去訪問彼此”,不要為了圖省事直接調用對方的DAO。此外,有的工程師習慣稱呼“OperationLogInfo”為DTO,DTO其實是一種統稱,數據模型、視圖模型、命令、事件都可稱之為DTO,這樣的叫法比較泛泛。
新引入的父類叫“VOBase”,類“OperationLogInfo”繼承於它。原因很簡單,“OperationLogInfo”的來源可能是另外的包,也可能是通過REST傳進的參數,是一種信息的縮影,設計為視圖模型還是比較自然的。
public interface Validatable { /** * 驗證 * @return 驗證結果 */ ParameterValidationResult validate(); } public abstract class VOBase implements Validatable { @Override public ParameterValidationResult validate() { return ParameterValidationResult.success(); } public String toJson() { return JsonUtils.toJson(this); } }
變更后的“OperationLogInfo”和“OperationLogService”代碼如下。
public class OperationLogInfo extends VOBase { private String module; private String operatorId; private String operatorName; private String action; @Override public ParameterValidationResult validate() { if (StringUtils.isEmpty(operationLogInfo.getModule())) { return ParameterValidationResult.failed(OperationMessages.NO_MODULE_INFO); } if (StringUtils.isEmpty(operationLogInfo.getOperatorId())) { return ParameterValidationResult.failed(OperationMessages.NO_OPERATOR_ID_TYPE); } if (StringUtils.isEmpty(operationLogInfo.getOperatorName())) { return ParameterValidationResult.failed(OperationMessages.NO_OPERATOR_NAME_TYPE); } if (StringUtils.isEmpty(operationLogInfo.getAction())) { return ParameterValidationResult.failed(OperationMessages.NO_ACTION_TYPE); } return ParameterValidationResult.success(); } //省略其它get、set方法 }
@Service public class OperationLogService { private static Logger logger = LoggerFactory.getLogger(OperationLogService.class); /** * 保存操作日志 * @param operationLogInfo 操作日志信息 */ public void save(OperationLogInfo operationLogInfo) { try { if (operationLogInfo == null) { throw new IllegalArgumentException(OperationMessages.NO_OPERATION_LOG); } ParameterValidationResult validation = operationLogInfo.validate(); if (!validation.isSuccess()) { throw new IllegalArgumentException(validation.getMessage()); } OperationLogDataEntity entity = OperationLogDataEntity.of(operationLogInfo); this.operationLogDao.save(entity); } catch (IllegalArgumentException e) { logger.error(e.getMessage(), e); } catch (Exception e) { logger.error(OperationMessages.SAVE_LOG_FAILED, e); } } //其它代碼省略 }
如果您有“代碼強迫證”,可以將此處的驗證放到下面另起一個方法。不過此處的分離與前面設計的分離意義不同,我們引入了“信息專家”(OperationLogInfo)的概念,把驗證的責任進行了約束。
內驗:基於“信息專家”理論,將驗證的過程放到待驗證的對象中
基於視圖模型的內驗設計,建議:1)可使用Spring框架提供的驗證框架;2)不可以在驗證方法中引入其它的Service、DAO、遠程調用工具等,要保證視圖模型的純粹。
2、基於業務模型的驗證
基於領域模型的驗證,通過使用一些很小的設計技巧可以實現非常優雅的驗證。很多工程師喜歡使用如“AXON”這類框架,覺得使用起來非常的酷。實際上,應用模型驅動時我個人不是特別建議使用那類開源框架,一是依賴性太強;二是框架為了支撐各類模式,設計的非常復雜,造成您的代碼性能不是很高。此外,無論是EDA還是CARS模式,都屬於局部模式,不要在系統中全面應用。一些邏輯簡單的場景使用面向過程設計效果很好;復雜的業務則要根據其業務形態使用不同的設計模式。
引入模型驅動,說明您的業務比較復雜,那設計出的對象也不會很簡單。如何保證一個對象的合法性是您需要首先考慮的內容。對象的生成,一個是通過外部參數新創建,另外則通過查詢數據庫進行加載。無論是哪種方式,數據都是不可信的。所以,驗證規則一定是非常非常的多,那么是否有一種方式能讓我們專注於業務開發,而非為驗證頭痛呢?這里面引入了兩個問題:1)如何驗證;2)何時驗證。
2.1、業務模型內驗的實現
業務模型的內驗,可以通過引入一些小的設計技巧完成。如果把代碼的所有實現細節全部都展現出來,對於閱讀者來說也是一件比較痛苦的事情,所以在此進行一些簡化,僅貼一些核心代碼供參考。此處的業務場景為“服務部署審批”流程,簡單來說就是每一次的服務上線需要通過一輪輪的審核,只有都通過后方能進行實施。
//業務模型:部署審批單
public class DeploymentApprovalForm extends ApprovalFormBase { private LocalDateTime deploymentDate; private ProcessStatus status = ProcessStatus.DRAFTING; private PhaseType currentPhase = PhaseType.DRAFTING; DeploymentApprovalForm(Long id, String name, ApplierInfo applierInfo, LocalDateTime createdDate, LocalDateTime updatedDate, List<ApprovalNodeBase> nodes, LocalDateTime deploymentDate, ProcessStatus status, PhaseType currentPhase) { //代碼省略 } @Override protected void addRule(RuleManager ruleManager) { super.addRule(ruleManager); ruleManager.addRule(new ObjectNotNullRule("status", this.status, OperationMessages.INVALID_STATUS)); ruleManager.addRule(new NotEqualsRule("status", this.status, ProcessStatus.UNKNOWN, OperationMessages.INVALID_STATUS)); ruleManager.addRule(new ObjectNotNullRule("currentPhase", this.currentPhase, OperationMessages.INVALID_PHASE)); ruleManager.addRule(new NotEqualsRule("currentPhase", this.currentPhase, PhaseType.UNKNOWN, OperationMessages.INVALID_PHASE)); ruleManager.addRule(new ObjectNotNullRule("deploymentDate", this.deploymentDate, OperationMessages.INVALID_DEPLOYMENT_DATE)); } //代碼省略 }
上面的“部署審批單”領域模型中,方法“addRule”為內驗的具體實現,定義在父類中。
public abstract class ApprovalFormBase extends EntityModel<Long> { private String name; private ApplierInfo applierInfo; //代碼省略 } public abstract class EntityModel<TID extends Comparable> extends DomainModel implements Versionable, Deletable { //ID private TID id; //代碼省略 } public abstract class DomainModel extends ValidatableBase { /** * 初始化當前狀態 */ public void initializeToNewCreation() { } //代碼省略 } public abstract class ValidatableBase implements Validatable { /** * 驗證當前領域模型 * @return 驗證r的結果 */ final public ParameterValidationResult validate() { RuleManager ruleManager = new RuleManager(this); this.addRule(ruleManager); return ruleManager.validate(); } /** * 增加驗證規則 * @param ruleManager 驗證規則管理器 */ protected void addRule(RuleManager ruleManager){ } }
“ObjectNotNullRule”為驗證規則,定義在框架中,代碼如下所示。通過定義不同類型的驗證規則如“NotEqualsRule”、“RegexRule”等,可以讓驗證的實現變得非常靈活。如果您願意,甚至可以定義如“and”,"or"這類的邏輯表達式。這里的驗證邏輯使用了設計模式中的“規約模式”,建議找相關文章進行了解。
final public class ObjectNotNullRule extends RuleBase<Object> { /** * 規則基類 * * @param nameOfTarget 驗證目標的名稱 * @param target 驗證的目標 * @param errorMessage 當規驗證失敗時的錯誤提示信息 */ public ObjectNotNullRule(String nameOfTarget, Object target, String errorMessage) { super(nameOfTarget, target, errorMessage); } /** * 執行驗證 * @return 驗證是否成功 */ @Override public ParameterValidationResult validate() { if(this.getTarget() == null){ return ParameterValidationResult.failed(null); } return ParameterValidationResult.success(); } }
通過引入一些簡單的設計模式,可以將一些通用的功能進行封裝,這樣您的代碼就會變得更加純粹。開發過程中,我很喜歡用一個詞來描述自己的代碼,包括“整潔”,“純粹”,“高可讀性”,希望您也有類似的規則,讓編程工作視為一項藝術。
2.1、業務模型內驗的實現
此處有了驗證,您也許會問:“那我什么時候調用呢,每一次保存的時候?每一次操作對象的時候?”,如果真的這樣,您會發現代碼中有許多重復的東西。在開發過程中如果發現了重復代碼通常有兩類處理方式:1)封裝為類中單獨的方法;2)將方法提升至父類中。而驗證方法的調用時機,用這兩類方式都不太適合。領域模型的驗證通常是在應用服務中,不太好為所有的應用服務都設計出通用的可繼承的驗證方法。而如果我們仔細分析一下,到底何時真的需要進行驗證,您會發現調用時機其實是可枚舉的:1)從數據庫中加載已有模型;2)模型新創建后;3)經過一系列的模型操作后,模型被最終持久化時。您也許會問,我在業務模型的方法中可能要判斷一些類字段是否為空或者是否有合適的值等,是不是每次都要調用驗證方法?答案是否定的,因為在上面的三個環節中您已經保證了整體對象的合法性,這是任何業務操作的前提,在后續的業務執行過程中就不必做額外的驗證。當然,如果是參數的驗證則需要您在代碼中實現,因為參數不是對象本身的屬性,不屬於內驗范圍。
關於對象的創建,模型驅動的設計方式通過有兩類方法:1)通過對象的構造函數,適合參數很少的場景;2)通過對象工廠。實踐中發現,“實體類型”模型的創建都比較復雜,要求的內嵌對象和參數通常比較多,所以工廠的方式非常常見。
我們繼續分析上面三個驗證時機。在此,我們為項目加入限制:業務模型的創建必須通過工廠。這樣的話,1)和2)兩個場景中都可以將驗證方法放至對象工廠中,保證我們創建對象的過程或者返回一個合法的對象或者直接拋創建異常。下面代碼展示了實現細節。
final public class DeploymentApprovalFormFactory { public final static DeploymentApprovalFormFactory INSTANCE = new DeploymentApprovalFormFactory(); private DeploymentApprovalFormFactory() { } public DeploymentApprovalForm create(DeploymentApprovalFormInfo deploymentApprovalFormInfo) throws DeploymentApprovalFormCreationException { if (deploymentApprovalFormInfo == null) { throw new DeploymentApprovalFormCreationException(OperationMessages.INVALID_APPROVAL_FROM_INFO); } //代碼省略 PhaseType currentPhase = PhaseType.getPhaseType(deploymentApprovalFormInfo.getCurrentPhase()); if (currentPhase == PhaseType.UNKNOWN) { currentPhase = PhaseType.DRAFTING; } //代碼省略 DeploymentApprovalForm deploymentApprovalForm = new DeploymentApprovalForm(deploymentApprovalFormInfo.getId(), deploymentApprovalFormInfo.getName(), applier, createdDate, updatedDate, nodes, deploymentDate, status, currentPhase); ParameterValidationResult validationResult = deploymentApprovalForm.validate(); if (!validationResult.isSuccess()) { throw new DeploymentApprovalFormCreationException(validationResult.getMessage()); } return deploymentApprovalForm; } }
“DeploymentApprovalFormInfo”這個對象是一個視圖模型,其值可以來自前端,也可以在“Repository”中通過調用“DAO”從數據庫中查詢信息后構建。
針對場景3),如果我們每次持久化時都顯示的調用一次驗證,會出現大量的重復的代碼。所以,我們引入了一個新的模式“工作單元”,工作單元是在使用面向對象設計時非常實用的一種模式,下面代碼給出片段,建議在網上找一些文章進行詳細學習。
public abstract class UnitOfWorkBase implements UnitOfWork { private static final Logger logger = LoggerFactory.getLogger(UnitOfWorkBase.class); /** * 提交所有改變的對象至事務 * */ @Override public CommitHandlingResult commit() { CommitHandlingResult result = new CommitHandlingResult(); try { this.validate(); this.persist(); } catch(ValidationException e) { logger.error(e.getMessage(), e); result = new CommitHandlingResult(false, e.getMessage()); } catch(Exception e) { logger.error(e.getMessage(), e); result = new CommitHandlingResult(false, OperationMessages.COMMIT_FAILED); } finally { this.clear(); } return result; } //驗證對象 protected void validate() throws ValidationException { CompositeParameterValidateResult result = new CompositeParameterValidateResult(); for (EntityModel entityModel : this.entityModels) { ParameterValidationResult validationResult = entityModel.validate(); if (!validationResult.isSuccess()){ result.addValidationResult(validationResult); result.fail(); } } if (!result.isSuccess()) { throw new ValidationException(result.getMessage(), result); } } //代碼省略 }
基於業務模型的內驗設計,建議:1)不可以使用框架的驗證框架,會產生強依賴;2)不可以在驗證方法中引入其它的Service、DAO、遠程調用工具等,會破壞您的架構完整性。通常情況下,您在使用模型驅動的設計方式時,應用的是一個“洋蔥”架構,業務模型居於架構的核心中,其它的組件依賴於模型而非反向的依賴。
本文介紹了驗證模式中的“內驗證”,后續會針對“外驗證”進行說明。