你真的正確實現了領域模型嗎?
你的代碼真的正確實現領域模型了嗎?這個題目從領域驅動設計實踐者的角度來看,多少有些模糊不清了。代碼?領域模型?根據Eric Evans的《Domain-Driven Design》一書,代碼本身不也是一種領域模型嗎?在開始本篇正題之前,有必要先對相關概念做簡單梳理。
Eric Evans認為,領域模型本身并不是某種特殊的圖,而是這種圖所要傳達的知識,并且這還特指針對領域專家頭腦中的知識的一種嚴格的組織且有選擇的抽象。
根據這種理解,領域模型可以被自由地表示為圖、文字甚至是代碼!也就是說,領域模型和代碼在Evans看來本質上是同一種東西。
盡管如此,本文還是使用“領域模型”作為一個與“代碼”相對的概念,這是為什么呢?
這是因為,這種理解常常更符合開發人員的認知。從大多數開發人員的角度來看,“模型”這個概念,不管是否是面向“領域”的,常常區別于代碼并且以圖的形式存在(比如,典型的UML圖)。實際上,這兩種觀點并不沖突。筆者在這里將前者Evans對領域模型的理解稱為一種“廣義的領域模型”,將后者特指圖形式的領域模型稱為一種“狹義的領域模型”。廣義的與狹義的領域模型的關系如下圖所示。
細心的讀者將會發現,上圖還傳遞了另外兩個觀點。
第一:圖形式的領域模型通常對應于設計階段,而代碼形式的領域模型則通常對應于實現階段。這種觀點其實與張逸老師的《我對領域模型的理解》一文不謀而合。他認為,設計對領域模型的反映,就是設計模型;代碼對領域模型的表達,就是實現模型。在設計階段,軟件設計人員需要基于對領域的理解建立對領域問題的解決方案;在實現階段,開發人員則根據設計模型進行編碼實現,使領域模型躍然于代碼上。當然,在設計與實現之前還需要有分析階段,領域專家與開發團隊圍繞領域梳理對業務知識的理解。不過,分析階段關注的又是另外一些問題了,本文主要討論設計與實現階段及其軟件制品之間的關系。
第二:狹義的領域模型與代碼,同時作為廣義的領域模型的特定表現形式,二者之間需要有明確的對應關系。也就是說,代碼應該忠實地反映領域模型,(狹義的)領域模型也應該及時地表現出代碼的變化。正是領域模型與代碼實現之間的緊密聯系才使模型變得有用,并確保我們所作出的深層次的設計能夠最終轉化為可運行的軟件。當然,如果團隊本身能力很強,完全只使用代碼來表示領域模型,那就可以從根本上避免代碼與領域模型不一致的問題。
言歸正傳,那么,所謂的領域模型和代碼之間“明確的對應關系”具體指的是什么呢?筆者認為,這具體包含三個方面的對應關系:
術語方面,也就是說代碼中使用的領域術語應與(狹義的)領域模型中的一致,這與Evans強調的在代碼中貫徹使用通用語言(Ubiquitous Language)是一致的。
領域概念的分解,也就是說結構方面的設計,包括領域對象及其關系,在代碼與領域模型中應是一致的。比如,UML類圖與代碼結構之間應該存在明確的對應。
領域概念之間的交互,也就是說行為方面的設計,包括領域對象之間的動作,在代碼與領域模型中應是一致的。比如,UML時序圖與代碼行為之間應該存在明確的對應。
為了實現設計階段的領域模型與實現階段的代碼之間的一致,從模型驅動工程(Model-Driven Engineering)的角度來看讀者可以應用以下幾種策略。
1、從模型到代碼的轉換
從模型到代碼的轉換(Model-to-Code Transformation)是模型驅動工程的一種典型技術,尤其活躍于2010年前后。將這種技術應用于領域驅動設計,可以幫助從領域模型自動化地生成代碼,從而減少開發人員手動編寫代碼的工作量,一定程度上能夠降低由于開發人員的疏忽造成的與領域模型的不一致。另一方面,由于其自動化的代碼生成過程,該技術還被期望能夠提高軟件開發效率。
領域驅動設計社區推崇的「模型到代碼轉換策略的工具」包括SCULPTOR (http://sculptorgenerator.org/), LEMMA(https://github.com/SeelabFhdo/lemma)等。
然而,從模型到代碼的轉換方法存在兩個固有問題。
其一,由于模型本身相比于代碼總是不夠具體的,因此從模型到代碼的轉換通常只能生成代碼框架,其代碼實現細節仍需由開發人員手動實現。這一過程仍然可能造成代碼與領域模型的偏離。
其二,在開發人員基于生成的代碼框架實現代碼細節時,領域模型本身可能也在演化,那么,演化后的領域模型所生成的代碼應該如何與開發人員正在修改的代碼進行合并呢?二者之間的合并沖突問題又應該如何解決?
2、代碼到模型的轉換
從代碼到模型的轉換(Code-to-Model Transformation),是從模型到代碼轉換的逆向過程,可以被認為是模型驅動的逆向工程(Model-Driven Reverse Engineering)的典型技術。
將這種技術應用于領域驅動設計,可以幫助從代碼中恢復出可視化的領域模型,所恢復的領域模型本身代表了“在代碼中被實現的領域模型”,同時它也是對代碼進行的一種抽象與可視化。利用恢復得到的圖形式的領域模型,開發人員能夠進一步對比設計的領域模型與實現的領域模型,從而快速發現和報告兩者之間的分歧。
更進一步地,還可以在設計的領域模型與實現的領域模型之間進行反射建模(Reflexion Modeling),即通過可視化的映射模型表現出實現的領域模型相比于設計階段的差異(包括新增、刪除與修改元素),從而更直觀地幫助開發人員識別代碼是否偏離了領域模型。
此外,抽象與可視化后的代碼視圖還可以幫助開發人員進行知識消化(Knowledge Crunching),減少該過程對代碼實現細節的依賴,從而降低該過程的認知復雜度。該策略的具體實施方法可以參考筆者最近在軟件學報上發表的《一種面向領域驅動設計的逆向建模支持方法》一文(http://www.jos.org.cn/jos/article/abstract/6278?st=search)。
3、模型組合
模型組合(Model Composition)基于關注點分離的思想將軟件系統劃分為模型單元并通過合成處理技術來組裝這些單元以解決系統的復雜性。
將這種技術應用于領域驅動設計,可以通過組合領域模型元素(及其代碼)得到開發人員預期的領域模型與代碼,從而降低開發人員手動實現代碼的工作量,減小代碼偏離領域模型。
這種策略,在筆者看來與目前大行其道的低代碼類似,其本質上都通過組件化以及模型驅動工程等思想來減少開發人員的代碼編寫。目前,根據最新的學術文獻綜述(https://www.sciencedirect.com/science/article/pii/S0950584920300689),這種策略仍存在許多挑戰,比如如何自動化地匹配要組合的模型元素,缺少工業規模的解決方案等。
4、模型與代碼的可追溯性管理
模型驅動工程被廣泛地用于多個軟件制品之間的可追溯性管理,比如設計文檔、源代碼以及測試用例等。將這種技術應用于領域驅動設計,可以在領域模型和代碼之間建立鏈接,并通過跟蹤這些鏈接可視化地展示與維護領域模型和代碼之間的一致性。
此外,如果輔以自動化的模型轉換技術(比如從模型到代碼轉換),利用這種機制還可以自動化地維護領域模型和代碼之間的鏈接。也就是說,當源模型(比如領域模型)被修改時,這種修改可以被自動化地傳播到目標模型(比如代碼)上。
缺乏適當的可追溯性信息的存儲、處理與查詢技術被學術界(https://www.sciencedirect.com/science/article/pii/S0950584912001346)認為是這種策略目前存在的局限性。筆者尚未看到這種策略在領域驅動設計社區的應用。
更重要的是,以上基于模型驅動工程的方法實際上都還依賴于某種領域特定語言(Domain Specific Language)。
比如,要想實現自動化的模型到代碼的轉換,期望的領域特定語言需要回答三個問題,即如何表示領域模型,如何表示代碼,以及如何建立領域模型元素到代碼元素之間的映射。
比如,如何在圖形式的領域模型中表示聚合?如何在代碼中表示聚合?如何在這兩種形式的聚合之間建立關聯?
既然如此,使用這些領域特定語言就很可能會對開發團隊的建模語言與編程語言等做出一定限制,要想在工業界落地這些模型驅動工程方法仍然有很多問題值得被討論。筆者在此列出的一二三不過是班門弄斧罷了。
本文作者是南京大學鐘陳星博士,目前正從事領域驅動設計與微服務相關的學術研究。此研究對于領域驅動設計的工程實踐而言,具有較高價值。作為國內目前唯一開展領域驅動設計方向學術研究的團隊,鐘博士在南京大學張賀教授的指導下,研讀了大量領域驅動設計的書籍與論文,對這一領域具有頗深造詣。感謝鐘博士的信任,邀請我加入該項目,使我能附驥參與此項研究,以貢獻我在工程實踐方面的一隅之見。