領域驅動設計

領域驅動設計能非常容易地應用於穩定領域,其中的關鍵活動適合開發人員對用戶腦海中的內容進行記錄和建模。但在領域本身不斷變化和發展的情況下,領域驅動 設計變得更具有挑戰性。這在敏捷項目中很普遍,在業務本身試圖演進的時候也會發生。

我們提供了模型中重要項 目過程、具體演進步驟的細節。

 

 

計劃的動機遠不止是要一個新外觀。多年的經驗告訴我們有更好的辦法來組織我們的內容,有更好的方式將我們的內容商業化,以及在此背後,還有更先進的開發方法——這纔是關鍵之所在。

其實,我們考慮工作的方式已超越了我們軟件可以處理的內容。這就是爲什麼DDD對我們來說如此有價值。

遺留軟件中阻礙我們的概念不匹配有兩個方面,我們先簡單看一下這兩方面問題,首先是我們的內部使用者,其次是我們的開發人員。這些問題都需要藉助DDD予以解決。

 

內部使用者的問題

新聞業是一個古老的行業,有既定的培訓、資格和職制,但對新聞方面訓練有素的新編輯來說,加入我們的行列並使用Web工具有效地工作是不可能的,尤其在剛來的幾個月裏。要成爲一個高效的使用者,瞭解我們CMS和網站的關鍵概念還遠遠不夠,還需要了解它們是如何實現的。

比如說,將緩存內容的概念(這應該完全是系統內部的技術優化)暴露給編輯人員;編輯們要將內容放置在緩存中以確保內容已準備好,還需要理解緩存工作流以用CMS工具診斷和解決問題。這顯然是對編輯人員不合理的要求。

 

開發人員的問題

概念上的不匹配也體現在技術方面。舉例來說,CMS有一個概念是“製品”,這完全是所有開發人員每天工作的核心。以前團隊中的一個人坦言,在足足九個月之後他才認識到這些“製品”其實就是網頁。圍繞“製品”形成的含義模糊的語言和軟件越來越多,這些東西讓他工作的真正性質變得晦澀難懂。

再舉一個例子,生成我們內容的RSS訂閱特別耗時。儘管各個版塊的主頁都包含一個清晰的列表,裏面有主要內容和附加內容,但底層軟件並不能對兩者予以區分。因此,從頁面抽取RSS訂閱的邏輯是混亂的,比如“在頁面上獲取每個條目,如果它的佈局寬大約一半兒、長度比平均長度長一些,那它可能是主要內容,這樣我們就能抽取鏈接、將其作爲一個訂閱”。

很顯然,對我們來說,人們對他們工作(開始、網頁和RSS訂閱)及其如何實現(緩存工作流、“製品”、混亂邏輯)的認識之間的分歧給我們的效益造成了明顯而慘重的影響。

 

從DDD開始

本部分闡述了我們使用DDD的場景:爲什麼選擇它,它在系統架構中所處的位置,還有最初的領域模型。在後面的章節中,我們會看一下如何把最初的領域知識傳播給擴充的團隊,如何演進模型,以及如何以此爲中心來演進我們的編碼技術。

 

選擇DDD

DDD所倡導的首要方面就是統一一致的語言,以及在代碼中直接體現用戶自己的概念。這能有效地解決前面提及的概念上的不匹配問題。單獨看來,這是一個有價值的見解,但其本身價值或許並不比“正確使用面向對象技術”多很多。

使其深入的是DDD引入的技術語言和概念:實體、值對象、服務、資源庫等。這確保了在處理非常大的項目時,我們的大型開發團隊有可能一致地進行開發——長遠來看,這對維護質量是必不可少的。甚至在廢棄我們更底層的代碼時(我們稍後會進行演示),統一的技術語言能讓我們恢復到過去、改進代碼質量。

 

系統中嵌入領域模型

本節顯示了DDD在整個系統架構中的地位。

我們的系統逐漸建立了三個主要的組件:渲染應用的用戶界面網站;面向編輯、用於創建和管理內容的應用程序;跟系統交互數據的供稿系統。這些應用都是基於Spring和Hibernate構建的Java Web應用,並使用Velocity作爲我們的模板語言。

我們可以看下這些應用的佈局:

 

Hibernate層提供數據訪問,使用EHCache作爲Hibernate的二級緩存。模型層包含領域對象和資源庫,服務則處於其上一層。在此之上,我們有Velocity模板層,它提供頁面渲染邏輯。最頂層則包含控制器,是應用的入口點。

看一下這個應用的分層架構,僅僅將模型當做是應用的一個自包含的層是很有意思的。這個想法大致正確,但模型層和其它層之間還是有一些細微的差別:由於我們使用領域驅動設計,所以我們需要統一語言,不僅要在我們談論領域時使用,還要在應用的任何地方使用。模型層的存在不僅是爲了從渲染邏輯中分離業務邏輯,還要爲使用的其它層提供詞彙表。

另外,模型層可作爲代碼的獨立單元進行構建,而且可以作爲JAR包導入到依賴於它的許多應用中。其它任何層則不是這樣。對構建和發佈應用來說,這意味着:在我們基礎設施的模型層改變代碼一定是跨所有應用的全局變化。我們可以在前端的網站中改變Velocity模板,只用部署前端應用就可以,管理系統或供稿系統則不用。如果我們在領域模型中改變了一個對象的邏輯,我們必須更新所有依賴於模型的應用,因爲我們只有(而且期望只有)一個領域視圖。

這有一種危害,就是領域建模的這種方法可能會導致單一模型,如果業務領域非常龐大,改變起來的代價會非常昂貴。我們認識到了這一點,因此隨着領域不斷增長,我們必須確保該層沒有變得太過笨重。目前,雖然領域層也是相當龐大和複雜的,但是這還沒有帶來什麼問題。在敏捷環境中工作,我們希望無論如何每兩週都要推出所有系統的最新變化。但我們持續關注着該層代碼改變的成本。如果成本上升到不能接受的程度,我們可能會考慮將單一模型細分成多個更小的模型,並在每個子模型之間給出適配層。但是我們在項目開始時沒有這樣做,我們更偏向於單一模型的簡單性,而不是用多個模型時必須解決的更爲複雜的依賴管理問題。

 

早期的領域建模

在項目初期,大家在鍵盤上動手開始編寫代碼之前,我們就決定讓開發人員、QA、BA、業務人員在同一個房間裏一起工作,以便項目能夠持續。在這個階段我們有一個由業務人員和技術人員組成的小型團隊,而且我們只要求有一個穩妥的初次發佈。這確保了我們的模型和過程都相當簡單。

我們的首要目標是讓編輯(我們業務代表的關鍵組成部分)就項目最初迭代的期望有一個清楚的認識。我們跟編輯們坐在一起,就像一個整體團隊一樣,用英語與他們交流想法,允許各個功能的代表對這些想法提出質疑和澄清,直到他們認爲我們正確理解了編輯需要的內容。

我們的編輯認爲,項目初期優先級最高的功能是系統能生成網頁,這些網頁能顯示文章和文章分類系統。

他們最初的需求可歸納爲:

  • 我們應該能將一篇文章和任何給定的URL關聯起來。
  • 我們要能改變選定的不同模板渲染結果頁面的方式。
  • 爲了管理,我們要將內容納入寬泛的版面,也就是新聞、體育、旅遊。
  • 系統必須能顯示一個頁面,該頁面包含指向特定分類中所有文章的鏈接。

我們的編輯需要一種非常靈活的方式來對文章進行分類。他們採用了基於關鍵字的方法。每個關鍵字定義了一個與內容相關的主題。每篇文章可以和很多關鍵字關聯,因爲一篇文章可以有很多主題。

我們網站有很多編輯,每個人負責不同版塊的內容。每個版塊都要求有自己導航和特有關鍵字的所有權。

從編輯使用的語言來看,我們似乎在領域中引入了一些關鍵實體:

  • 頁面 URL的擁有者。負責選擇模板來渲染內容。
  • 模板 頁面佈局,任何時候都有可能改變。技術人員將每個模板實現爲磁盤上的一個Velocity文件。
  • 版塊 頁面更寬泛的分類。每個版塊有一個編輯,還有對其中頁面共同的感官。新聞、旅遊和商業都是版塊的例子。
  • 關鍵字 描述存在於版塊中主題的方式。關鍵字過去用於文章分類,現在則驅動自動導航。這樣它們將與某個頁面關聯,關於給定主題所有文章的自動頁面也能生成。
  • 文章 我們能發佈給用戶的一段文本內容。

提取這些信息後,我們開始對領域建模。項目早期做出的一個決定:編輯擁有領域模型並負責設計,技術團隊則提供協助。對過去不習慣這種技術設計的編輯來說,這是相當大的轉變。我們發現,通過召開由編輯、開發人員、技術架構師組成的研習會,我們能用簡單、技術含量較低的方法勾畫出領域模型,並對它持續演進。討論模型的範圍,使用鋼筆、檔案卡和Blu-Tak繪製備選解決方案。每個候選模型都會進行討論,技術團隊把設計中的每個細節含義告訴給編輯。

儘管過程在最初相當緩慢,但很有趣。編輯發現這非常容易上手;他們能信手拈來、提出對象,然後及時從開發人員那裏獲得反饋,以知道生成的模型是否滿足了他們的需求。對於編輯們能在過程中迅速掌握技術的要領,技術人員都感到很驚喜,而且所有人都對生成的系統是否能滿足客戶需求很有信心。

觀察領域語言的演變也很有趣。有時文章對象會被說成是“故事”。顯然對於同一實體的多個名稱,我們並沒有一種統一語言,這是一個問題。是我們的編輯發現他們描述事物時沒有使用統一的語言,也是他們決心稱該對象爲文章。後來,任何時間有人說“故事”,就會有人說:“你的意思不是文章嗎?”在設計統一語言時,持續、公共的改進過程是種很強大的力量。

我們的編輯最初設計生成的模型是這樣的:

由於並非所有的團隊成員都參與了該過程的所有階段,所以我們需要向他們介紹工作進展,並將這些全部展現在了牆上。然後開發人員開始了敏捷開發之旅,而且由於編輯和開發人員、BA、QA都在一起工作,任何有關模型及其意圖的問題都能在開發過程的任何時候獲得第一手的可靠信息。

經過幾次迭代之後系統初具形態,我們還建立工具來創建和管理關鍵字、文章和頁面。隨着這些內容的創建,編輯們很快掌握了它們,並且給出了一些修改建議。大家普遍認爲這一簡單的關鍵模型能正常運行,而且可以繼續下去、形成網站初次發佈的基礎。

 

首次發佈之後,我們的項目團隊伴隨着技術人員和業務代表們的成長,一起取得了進步,打算演進領域模型。很顯然,我們需要一種有組織的方式來爲領域模型引入新的內容、進行系統的演進。

 

新員工入門

DDD是入門過程中的核心部分。非技術人員在整個項目生命週期內都會加入項目,因爲工作計劃是橫跨各個編輯領域的,反過來,在恰當的時機我們也會引入版面編輯。技術人員很容易就能加入項目,因爲我們持續不斷地僱用新員工。

我們的入門過程包括針對這兩類人的DDD講習,儘管細節不同,但高層次的議題涵蓋兩個相同的部分:DDD是什麼,它爲什麼重要;領域模型本身的特定範圍。

描述DDD時我們強調的最重要的內容有:

  • 領域模型歸業務代表所有。這就要從業務代表的頭腦裏抽象概念,並將這些概念嵌入到軟件中,而不能從軟件的角度思考,並試圖影響業務代表。
  • 技術團隊是關鍵的利益相關者。我們將圍繞具體細節據理力爭。

覆蓋領域模型的特定範圍本身就很重要,因爲它予以就任者處理項目中特定問題的真正工具。我們的領域模型中有幾十個對象,所以我們只關注於高級別和更爲明顯的少數幾個,在我們的情況中就是本文所討論的各種內容和關鍵字概念。在這裏我們做了三件事:

  • 我們在白板上畫出了概念及其關係,因此我們能給出系統如何工作的一個有形的表示。
  • 我們確保每位編輯當場解釋大量的領域模型,以強調領域模型不屬於技術團隊所有這一事實。
  • 我們解釋一些爲了達到這一點而做出的歷史變遷,所以就任者可以理解(a)這不是一成不變的,而是多變的,(b)爲了進一步開發模型,他們可以在即將進行的對話中扮演什麼樣的角色。

規劃中的DDD

入門是必不可少的,不過在開始計劃每次迭代的時候,知識才真正得以實踐。

 

使用統一語言

DDD強制使用的統一語言使得業務人員、技術人員和設計師能圍坐在一起規劃並確定具體任務的優先次序。這意味着有很多會議與業務人員有關,他們更接近技術人員,也更加了解技術過程。有一位同事,她先擔任項目的編輯助理,然後成長爲關鍵的決策者;她解釋說,在迭代啓動會議上她親自去看技術人員怎樣判斷和(激烈地)評估任務,開始更多地意識到功能和努力之間的平衡。如果她不和技術團隊共用一種語言,她就不會一直出席會議,也不會收穫那些認識。

在規劃階段利用DDD時我們使用的兩個重要原則是:

  1. 領域模型歸屬於業務;
  2. 領域模型需要一個權威的業務源。

領域模型的業務所有權在入門中就進行了解釋,但在這裏才發揮作用。這意味着技術團隊的關鍵角色是聆聽並理解,而不是解釋什麼可能、什麼不可能。需求抽象要求將概念性的領域模型映射到具體的功能需求上,並在存在不匹配的地方對業務代表提出異議或進行詢問。接着,存在不匹配的地方要麼改變領域模型,要麼在更高層次上解決功能需求(“你想用此功能達成什麼效果?”)。

對領域模型來說,權威的業務源是我們組織的性質所明確需要的。我們正在構建一個獨立的軟件平臺,它需要滿足很多編輯團隊的需求,編輯團隊不一定以同樣的方式來看世界。Guardian不實行許多公司實行的“命令和控制”結構;編輯臺則有很多自由,也能以他們認爲合適的方式去開發自己的網站版面,並設定預期的讀者。因此,不同的編輯對領域模型會有略微不同的理解和觀點,這有可能會破壞單一的統一語言。我們的解決辦法是確定並加入業務代表,他們在整個編輯臺都有責任。對我們來說,這是生產團隊,是那些處理日常構建版面、指定佈局等技術細節的人。他們是文字編輯依賴的超級用戶,作爲專家工具建議,因此技術團隊認爲他們是領域模型的持有者,而且他們保證軟件中大部分的一致性。他們當然不是唯一的業務代表,但他們是與技術人員保持一致的人員。

 

與DDD一起計劃的問題

不幸的是,我們發現了在計劃過程中應用DDD特有的挑戰,尤其是在持續計劃的敏捷環境中。這些問題是:

  1. 本質上講,我們正在將軟件寫入新的、不確定商業模式中;
  2. 綁定到一箇舊模型;
  3. 業務人員“入鄉隨俗”。

我們反過來討論下這些問題……

Eric Evans撰寫關於創建領域模型的文章時,觀點是業務代表的腦海中存在着一個模型,該模型需要提取出來;即便他們的模型不明確,他們也明白核心概念,而且這些概念基本上能解釋給技術人員。然而在我們的情況中,我們正在改變我們的模型——事實上是在改變我們的業務——但並不知道我們目標的確切細節。(馬上我們就會看到這一點的具體例子。)某些想法顯而易見,也很早就建立起來了(比如我們會有文章和關鍵字),但很多並不是這樣(引入頁面的想法還有一些阻力;關鍵字如何關聯到其它內容則完全是各有各的想法)。我們的教科書並沒有提供解決這些問題的指南。不過,敏捷開發原則則可以:

  • 構建最簡單的東西。儘管我們無法在早期解決所有的細節,但通常能對構建下一個有用的功能有足夠的理解。
  • 頻繁發佈。通過發佈此功能,我們能看到功能如何實際地運轉。進一步的調整和進化步驟因此變得最爲明顯(不可避免,它們往往不是我們所預期的)。
  • 降低變化的成本。利用這些不可避免的調整和進化步驟,減少變化的成本很有必要。對我們來說這包括自動化構建過程和自動化測試等。
  • 經常重構。經過幾個演進步驟,我們會看到技術債務累積,這需要予以解決。

與此相關的是第二個問題:與舊模型有太多的精神聯繫。比如說,我們的遺留系統要求編輯和製作人員單獨安排頁面佈局,而新系統的願景則是基於關鍵字自動生成頁面。在新系統裏,Guantánamo Bay頁面無需任何人工干預,許多內容會給出Guantánamo Bay關鍵字,僅簡單地憑藉這一事實就能顯示出來。但結果卻是,這只是由技術團隊持有的過度機械化的願景,技術團隊希望減少體力勞動和所有頁面的持續管理。相比之下,編輯人員高度重視人的洞察力,他們帶入過程的不僅有記述新聞,還有展現新聞;對他們來說,爲了突出最重要的故事(而不僅僅是最新的),爲了用不同的方法和靈敏性(比如9·11和Web 2.0報導)區別對待不同的主題,個人的佈局是很有必要的。

對這類問題沒有放之四海而皆準的解決辦法,但我們發現了兩個成功的關鍵:專注於業務問題,而不是技術問題;銘記“創造性衝突”這句話。在這裏的例子裏,見解有分歧,但雙方表達了他們在商業上的動機後,我們就開始在同一個環境裏面工作了。該解決方案是創造性的,源於對每個人動機的理解,也因此解決了大家了疑慮。在這種情況下,我們構建了大量的模板,每個都有不同的感覺和影響等,編輯可以從中選擇並切換。此外,每個模板的關鍵區域允許手動選擇顯示的故事、頁面的其餘部分自動生成內容(小心不要重複內容),對於該手動區域,如果管理變得繁重,就可以隨時關閉,從而使網頁完全自動化。

我們發現的第三個挑戰是隨着業務人員“入鄉隨俗”,也就是說他們已深深融入技術且牽涉到了要點,以至於他們會忘記對新使用系統的內部用戶來說,系統該是什麼樣。當業務代表發現跟他們的同事很難溝通事情如何運轉,或者很難指出價值有限的功能時,就有危險的信號了。Kent Beck在《解析極限編程》第一版中說,現場客戶與技術團隊直接交互絕不會佔用他們很多的時間,通過強調這一點,就可以保證在現場的客戶。但我們在與有幾十個開發人員、多名BA、多名 QA的團隊一起工作時,我們發現即使有三個全職的業務代表,有時也是不夠的。由於業務人員花費了太多的時間與技術人員在一起,以至他們與同事失去聯繫成爲真正的問題。這些都是人力解決方案帶來的人力問題。解決方案是要提供個人備份和支持,讓新的業務人員輪流加入團隊(可能從助理開始着手進行,逐步成長爲關鍵決策角色),允許代表有時間迴歸他們自己的核心工作,比如一天、一週等。事實上,這還有一個附加的好處,就是能讓更多的業務代表接觸到軟件開發,還可以傳播技巧和經驗。

 

演進第一步:超越文章

首次發佈後不久,編輯要求系統能處理更多的內容類型,而非只有文章一類。儘管這對我們來說毫不稀奇,但在我們構建模型的第一個版本時,我們還是明確決定對此不予考慮太多。

這是個關鍵點:我們關注整個團隊能很好地理解小規模、可管理塊中的模型和建模過程,而不是試圖預先對整個系統進行架構設計。隨着理解的深入或變化,稍後再改變模型並不是個錯誤。這種做法符合YAGNI編碼原則(你不會需要它),因爲該做法防止開發人員引入額外的複雜度,因而也能阻止Bug的引入。它還能讓整個團隊安排時間去對系統中很小的一塊達成共識。我們認爲,今天產出一個可工作的無Bug系統要比明天產出一個完美的、包括所有模型的系統更爲重要。

我們的編輯在下一個迭代中要求的內容類型有音頻和視頻。我們的技術團隊和編輯再次坐在一起討論了領域建模過程。編輯先跟技術團隊談道,音頻和視頻很顯然與文章相似:應該可以將視頻或音頻放在一個頁面上。每個頁面只允許有一種內容。視頻和音頻可以通過關鍵字分類。關鍵字可以屬於版面。編輯還指明,在以後的迭代中他們會添加更多類型的內容,他們認爲現在該是時候去理解我們應該如何隨着時間的推移去演進內容模型。

對我們的開發人員來說,很顯然編輯想在語言中明確引入兩個新條目:音頻和視頻。音頻、視頻和文章有一些共同點:它們都是內容類型,這一點也很明確。我們的編輯並不熟悉繼承的概念,所以技術團隊可以給編輯講解繼承,以便技術團隊能正確表述編輯所看到的模型。

這裏有一個明顯的經驗:通過利用敏捷開發技術將軟件開發過程細分爲小的塊,我們還能使業務人員的學習曲線變得平滑。久而久之他們能加深對領域建模過程的理解,而不用預先花費大量的時間去學習面向對象設計所有的組件。

這是我們的編輯根據添加的新內容類型設計的模型。

這個單一的模型演變是大量更細微的通用語言演進的結果。現在我們有三個外加的詞:音頻、視頻和內容;我們的編輯已經瞭解了繼承,並能在以後的模型迭代中加以利用;對添加新的內容類型,我們也有了以後的擴展策略,並使其對我們的編輯來說是簡單的。如果編輯需要一個新的內容類型,而這一新的內容類型與我們已有的內容類型相同、在頁面和關鍵字之間有大致相同的關係,那編輯就能要求開發團隊產出一個新的內容類型。作爲一個團隊,我們正通過逐步生成模型來提高效率,因爲我們的編輯不會再詳細檢查漫長的領域建模過程去添加新的內容類型。

 

演進第二步:

由於我們的模型要擴展到包括更多的內容類型,它需要更靈活地去分類。我們開始在領域模型中添加額外的元數據,但編輯的最終意圖是什麼還不是非常清楚。然而這並不讓我們太過擔憂,因爲我們對元數據進行建模的方法與處理內容的方法一樣,將需求細分爲可管理的塊,將每個添加到我們的領域中。

我們的編輯想添加的第一個元數據類型是系列這一概念。系列是一組相關的內容,內容則有一個基於時間的隱含順序。在報紙中有很多系列的例子,也需要將這一概念解釋爲適用於Web的說法。

我們對此的初步想法非常簡單。我們將系列添加爲一個領域對象,它要關聯到內容和頁面。這個對象將用來聚集與系列關聯的內容。如果讀者訪問了一種內容,該內容屬於某個系列,我們就能從頁面鏈接到同一系列中的前一條和後一條內容。我們還能鏈接到並生成系列索引頁面,該頁面可以顯示系列中的所有內容。

這裏是編輯所設計的系列模型:

與此同時,我們的編輯正在考慮更多的元數據,他們想讓這些元數據與內容關聯。目前關鍵字描述了內容是關於什麼的。編輯還要求系統能根據內容的基調對內容進行不同的處理。不同基調的例子有評論、訃告、讀者供稿、來信。通過引入基調,我們就可以將其顯示給讀者,讓他們找到類似的內容(其它訃告、評論等)。這像是除關鍵字或系列外另一種類型的關係。跟系列一樣,基調可以附加到一條內容上,也能和頁面有關係。

這裏是編輯針對基調設計的模型:

完成開發後,我們有了一個能根據關鍵字、系列或基調對內容進行分類的系統。但編輯對達到這一點所需的技術工作量還有一些關注點。他們在我們下次演進模型時向技術團隊提出了這些關注點,並能提出解決方案。

演進第三步:重構元數據

模型的下一步演進是我們的編輯想接着添加類似於系列和基調的內容。我們的編輯想添加帶有貢獻者的內容這一概念。貢獻者是創建內容的人,可能是文章的作者,或者是視頻的製作人。跟系列一樣,貢獻者在系統中有一個頁面,該頁面會自動聚集貢獻者製作的所有內容。

編輯還看到了另一個問題。他們認爲隨着系列和基調的引入,他們已經向開發人員指明瞭大量非常相似的細節。他們要求構建一個工具去創建系列,構建另一個工具去創建基調。他們不得不指明這些對象如何關聯到內容和頁面上。每次他們都發現,他們在爲這兩種類型的領域對象指定非常相似的開發任務;這很浪費時間,還是重複的。編輯更加關注於貢獻者,還有更多的元數據類型會加入進來。這看起來又要讓編輯再次指明、處理大量昂貴的開發工作,所有這些都非常相似。

這顯然成爲一個問題。我們的編輯似乎已經發現了模型的一些錯誤,而開發人員還沒有。爲什麼添加新的元數據對象會如此昂貴呢?爲什麼他們不得不一遍又一遍地去指定相同的工作呢?我們的編輯問了一個問題,該問題是“這僅僅是‘軟件開發如何工作’,還是模型有問題?”技術團隊認爲編輯熟悉一些事情,因爲很顯然,他們理解模型的方式與編輯不同。我們與編輯一起召開了另一個領域建模會議,試圖找出問題所在。

在會議上我們的編輯建議,所有已有的元數據類型實際上源於相同的基本思想。所有的元數據對象(關鍵字、系列、基調和貢獻者)可以和內容有多對多的關係,而且它們都需要它們自己的頁面。(在先前的模型版本中,我們不得不知道對象和頁面之間的關係)。我們重構了模型,引入了一個新的超類——Tag(標籤),並作爲其它元數據的超類。編輯們很喜歡使用“超類”這一技術術語,將整個重構稱爲“Super-Tag”,儘管最終也回到了現實。

由於標籤的引入,添加貢獻者和其它預期的新元數據類型變得很簡單,因爲我們能夠利用已有的工具功能和框架。

我們修訂後的模型現在看起來是這樣的:

我們的業務代表在以這種方式考慮開發過程和領域模型,發現這一點非常好,還發現領域驅動設計有能力促進在兩個方向都起作用的共同理解:我們發現技術團隊對我們正努力解決的業務問題有良好且持續的理解,而且出乎意料,業務代表能“洞察”開發過程,還能改變這一過程以更好地滿足他們的需求。編輯們現在不僅能將他們的需求翻譯爲領域模型,還能設計、檢查領域模型的重構,以確保重構能與我們目前對業務問題的理解保持同步。

編輯規劃領域模型重構併成功執行它們的能力是我們領域驅動設計guardian.co.uk成功的一個關鍵點。

 

構建模型

在構建領域模型時,要確認的第一件事就是領域中出現的聚集。聚集可認爲是相關對象的集合,這些對象彼此相互引用。這些對象不應該直接引用其它聚集中的其它對象;不同聚集之間的引用應該由根聚集來完成。

 

看一下我們在上面定義的模型示例,我們開始看到對象成形。我們有Page和Template對象,它們結合起來能給Web頁面提供URL和觀感。由於Page是系統的入口點,所以在這裏Page就是根聚集。

我們還有一個聚集Content,它也是根聚集。我們看到Content有Article、Video、Audio等子類型,我們認爲這些都是內容的子聚集,核心的Content對象則是根聚集。

我們還看到形成了另一個聚集。它是元數據對象的集合:Tag、Series、Tone等。這些對象組成了標籤聚集,Tag是根聚集。

Java編程語言提供了理想的方式來對這些聚集進行建模。我們可以使用Java包來對每個聚集進行建模,使用標準的POJO對每個領域對象進行建模。那些不是根聚集、且只在聚集中使用的領域對象可以有包範圍內使用的構造函數,以防它們在聚集外被構造。

上述模型的包結構如下所示(“r2”是我們應用套件的名稱):

com.gu.r2.model.page
com.gu.r2.model.tag
com.gu.r2.model.content
com.gu.r2.model.content.article
com.gu.r2.model.content.video
com.gu.r2.model.content.audio

 

我們將內容聚集細分爲多個子包,因爲內容對象往往有很多聚集特定的支持類(這裏的簡化圖中沒有顯示)。所有以標籤爲基礎的對象往往要更爲簡單,所以我們將它們放在了一個包裏,而沒有引入額外的複雜性。

不過不久之後,我們認識到上述包結構會給我們帶來問題,我們打算修改它。看看我們前端應用的包結構示例,瞭解一下我們如何組織控制器,就能闡述清楚這一問題:

com.gu.r2.frontend.controller.page
com.gu.r2.frontend.controller.articl

這裏看到我們的代碼集要開始細分爲片段。我們提取了所有的聚集,將其放入包中,但我們沒有單獨的包去包含與聚集相關的所有對象。這意味着,如果以後領域變得太大而不能作爲一個單獨的單元來管理,我們希望將應用分解,處理依賴就會有困難。目前這還沒有真正帶來什麼問題,但我們要重構應用,以便不會有太多的跨包依賴。經過改進的結構如下:

 com.gu.r2.page.model   (domain objects in the page aggregate)
 com.gu.r2.page.controller (controllers providing access to aggregate)
 com.gu.r2.content.article.model
 com.gu.r2.content.article.controller
 ...
 etc

除了約定,我們在代碼集中沒有其它任何的領域驅動設計實施原則。創建註解或標記接口來標記聚集根是有可能的,

實際上是爭取在模型包鎖定開發,減少開發人員建模時出錯的機率。

但實際上並不是用這些機械的強制來保證在整個代碼集中都遵循標準約定,而是我們更多地依賴了人力技術,

比如結對編程和測試驅動開發。如果我們確實發現已創建的一些內容違反了我們的設計原則(這相當少見),

那我們會告訴開發人員並讓他完善設計。我們還是喜歡這個輕量級的方法,因爲它很少在代碼集中引入混亂,

反而提升了代碼的簡單性和可讀性。這也意味着我們的開發人員更好地理解了爲什麼一些內容是按這種方式組織,

而不是被迫去簡單地做這些事情。

 

核心DDD概念的演進

根據領域驅動設計原則創建的應用會具有四種明顯的對象類型:實體、值對象、資源庫和服務。在本節中,我們將看看應用中的這些例子。

 

實體

實體是那些存在於聚集中並具有標識的對象。並不是所有的實體都是聚集根,但只有實體才能成爲聚集根。

開發人員,尤其是那些使用關係型數據庫的開發人員,都很熟悉實體的概念。不過,我們發現這個看似很好理解的概念卻有可能引起一些誤解。

這一誤解似乎跟使用Hibernate持久化實體有點兒關係。由於我們使用Hibernate,我們一般將實體建模爲簡單的POJO。每個實體具有屬性,這些屬性可以利用setter和getter方法進行存取。每個屬性都映射到一個XML文件中,定義該屬性如何持久化到數據庫中。爲了創建一個新的持久化實體,開發人員需要創建用於存儲的數據庫表,創建適當的Hibernate映射文件,還要創建有相關屬性的領域對象。由於開發人員要花費一些時間處理持久化機制,他們有時似乎認爲實體對象的目的僅僅只是數據的持久化,而不是業務邏輯的執行。等他們後來開始實現業務邏輯時,他們往往在服務對象中實現,而不是在實體對象本身中。

在下面(簡化)的代碼片段中可以看出此類錯誤。我們用一個簡單的實體對象來表示一場足球賽:

該實體對象使用FootballTeam實體去對球隊進行建模,看起來很像使用Hibernate的開發人員所熟悉的對象類型。該實體的每個屬性都持久化到數據庫中,儘管從領域驅動設計的角度來說這個細節並不真的重要,我們的開發人員還是將持久化的屬性提升到一個高於它們應該在的水平上去。在我們試圖從 FootballTeam對象計算出誰贏得了比賽的時候這一點就可以顯露出來。我們的開發人員要做的事情就是造出另一種所謂的領域對象,就像下面所示:

片刻的思考應該表明已經出現了錯誤。我們已經創建了一個FootballMatchSummary類,該類存在於領域模型中,但對業務來說它並不意味

着什麼。它看起來是充當了FootballMatch對象的服務對象,提供了實際上應該存在於FootballMatch領域對象中的功能。引起這一誤解的原

因好像是開發人員將FootballMatch實體對象簡單地看成了是反映數據庫中持久化信息,而不是解決所有的業務問題。我們的開發人員將實體

考慮爲了傳統ORM意義上的實體,而不是業務所有和業務定義的領域對象。

不願意在領域對象中加入業務邏輯會導致貧血的領域模型,如果不加以制止還會使混亂的服務對象激增——就像我們等會兒看到的一樣。作爲

一個團隊,現在我們來檢視一下創建的服務對象,看看它們實際上是否包含業務邏輯。我們還有一個嚴格的規則,就是開發人員不能在模型中

創建新的對象類型,這對業務來說並不意味着什麼。

作爲團隊,我們在項目開始時還被實體對象給弄糊塗了,而且這種困惑也與持久化有關。在我們的應用中,大部分實體與內容有關,而且大部

分都被持久化了。但當實體不被持久化,而是在運行時由工廠或資源庫創建的話,有時候還是會混淆。

一個很好的此類例子就是“標籤合成的頁面”。我們在數據庫中持久化了編輯創建的所有頁面的外觀,但我們可以自動生成從標記組合

(比如 USA+Economics或Technology+China)聚集內容的頁面。由於所有可能的標記組合總數是個天文數字,我們不可能持久化所有

的這些頁面,但系統還必須能生成頁面。在渲染標記組合頁面時,我們必須在運行時實例化尚未持久化的新Page實例。項目初期我們傾向於

認爲這些非持久化對象與“真正的”持久化領域對象不同,也不像在對它們建模時那麼認真。從業務觀點看,這些自動生成的實體與持久化

實體實際上並沒有什麼不同,因此從領域驅動設計觀點來看也是如此。不論它們是否被持久化,對業務來說它們都有同樣的定義,都不過是

領域對象;沒有“真正的”和“不是真正的”領域對象概念。

 

值對象

值對象是實體的屬性,它們沒有特性標識去指明領域中的內容,但表達了領域中有含義的概念。這些對象很重要,因爲它們闡明瞭統一語言。

值對象闡述能力的一個例子可以從我們的Page類更詳細地看出。系統中任何Page都有兩個可能的URLs。一個URL是讀者鍵入Web瀏覽器

以訪問內容的公開URL。另一個則是從應用服務器直接提供服務時內容依存的內部URL。我們的Web服務器處理從用戶那裏傳入的URL,並

將它轉換爲適合後端CMS服務器的內部URL。

一種簡化的觀點是,在Page類中兩個可能的URL都被建模爲字符串對象:

 public String getUrl(); 
 public String getCmsUrl(); 

不過,這並沒有什麼特別的表現力。除了這些方法返回字符串這一事實之外,只看這些方法的簽名很難確切地知道它們會返回什麼。另外

想象一下這種情況,我們想基於頁面的URL從一個數據訪問對象中加載頁面。我們可能會有如下的方法簽名:

public Page loadPage(String url);
 

這裏需要的URL是哪一個呢?是公開的那個還是CMS URL?不檢查該方法的代碼是不可能識別出來的。這也很難與業務人員談論頁面的URL。

我們指的到底是哪一個呢?在我們的模型中沒有表示每種類型URL的對象,因此在我們的詞彙裏面也就沒有相關條目。

這裏還含有更多的問題。我們對內部URL和外部URL可能有不同的驗證規則,也希望對它們執行不同的操作。如果我們沒有地方放置這個

邏輯,那我們怎麼正確地封裝該邏輯呢?控制URLs的邏輯一定不屬於Page,我們也不希望引入更多不必要的服務對象。

領域驅動設計建議的演進方案是明確地對這些值對象進行建模。我們應該創建表示值對象的簡單包裝類,以對它們進行分類。如果我們

這樣做,Page裏面的簽名就如下所示:

 public Url getUrl();
 public CmsPath getCmsPath();

現在我們可以在應用中安全地傳遞CmsPath或Url對象,也能用業務代表理解的語言與他們談論代碼。

 

資源庫

資源庫是存在於聚集中的對象,在抽象任何持久化機制時提供對聚集根對象實體的訪問。這些對象由業務問題請求,與領域對象一起響應。

將資源庫看成是類似於有關數據庫持久化功能的數據訪問對象,而非存在於領域中的業務對象,這一點很不錯。但資源庫是領域對象:他們

響應業務請求。資源庫始終與聚集關聯,並返回聚集根的實例。如果我們請求一個頁面對象,我們會去頁面資源庫。如果我們請求一個頁面

對象列表來響應特定的業務問題,我們也會去頁面資源庫。

我們發現一個很好的思考資源庫的方式,就是把它們看成是數據訪問對象集合之上的外觀。然後它們就成爲業務問題和數據傳輸對象的

結合點,業務問題需要訪問特定的聚集,而數據傳輸對象提供底層功能。

這裏舉一小段頁面資源庫的代碼例子,我們來實際看下這個問題:

我們的資源庫有業務請求:獲取PublicationDate請求的特定播客系列的頁面。獲取特定版面的最新頁面。我們可以看看這裏使用的

業務領域語言。它不僅僅是一個數據訪問對象,它本身就是一個領域對象,跟頁面或文章是領域對象一樣。

我們花了一段時間才明白,把資源庫看成是領域對象有助於我們克服實現領域模型的技術問題。我們可以在模型中看到,標籤和內容是

一種雙向的多對多關係。我們使用Hibernate作爲ORM工具,所以我們對其進行了映射,Tag有如下方法:

public List<Content> getContent();

Content有如下方法:

 public List<Tag>  getTags(); 

儘管這一實現跟我們的編輯看到的一樣,是模型的正確表達,但我們有了自己的問題。對開發人員來說,代碼可能會編寫成下面這樣:

 if(someTag.getContent().size() == 0){
     ... do some stuff
 }

這裏的問題是,如果標籤關聯有大量的內容(比如“新聞”),我們最終可能會往內存中加載幾十萬的內容條目,而只是爲了看看標記是否

包含內容。這顯然會引起巨大的網站性能和穩定性問題。

隨着我們演進模型、理解了領域驅動設計,我們意識到有時候我們必須要注重實效:模型的某些遍歷可能是危險的,應該予以避免。在這種

情況下,我們使用資源庫來用安全的方式解決問題,會爲系統的性能和穩定性犧牲模型個別的清晰性和純潔性。

 

服務

服務是通過編排領域對象交互來處理業務問題執行的對象。我們所理解的服務是隨着我們過程演進而演進最多的東西。

首要問題是,對開發人員來說創建不應該存在的服務相當容易;他們要麼在服務中包含了本應存在於領域對象中的領域邏輯,要麼扮演了

缺失的領域對象角色,而這些領域對象並沒有作爲模型的一部分去創建。

項目初期我們開始發現服務開始突然涌現,帶着類似於ArticleService的名字。這是什麼呀?我們有一個領域對象叫Article;那文章服務的

目的是什麼?檢查代碼時,我們發現該類似乎步了前面討論的FootballMatchSummary的後塵,有類似的模式,包含了本該屬於核心領域

對象的領域邏輯。

爲了對付這一行爲,我們對應用中的所有服務進行了代碼評審,並進行重構,將邏輯移到適當的領域對象中。我們還制定了一個新的規則:

任何服務對象在其名稱中必須包含一個動詞。這一簡單的規則阻止了開發人員去創建類似於ArticleService的類。取而代之,我們創建

ArticlePublishingService和ArticleDeletionService這樣的類。推動這一簡單的命名規範的確幫助我們將領域邏輯移到了正確的地方,

但我們仍要求對服務進行定期的代碼評審,以確保我們在正軌上,以及對領域的建模接近於實際的業務觀點。

 

演進架構中DDD的一些教訓

儘管面臨挑戰,但我們發現了在不斷演進和敏捷的環境中利用DDD的顯著優勢,此外我們還總結了一些經驗教訓:

  • 你不必理解整個領域來增加商業價值。你甚至不需要全面的領域驅動設計知識。團隊的所有成員差不多都能在他們需要的任何時間
  • 內對模型達成一個共同的理解。
  • 隨着時間的推移,演進模型和過程是可能的,隨着共同理解的提高,糾正以前的錯誤也是可能的。

我們系統的完整領域模型要比這裏描述的簡化版本大很多,而且隨着我們業務的擴展在不斷變化。在一個大型網站的動態世界裏,創新

永遠發生着;我們始終要保持領先地位並有新的突破,對我們來說,有時很難在第一次就得到正確的模型。事實上,我們的業務代表

往往想嘗試新的想法和方法。有些人會取得成果,其他人則會失敗。是逐步擴展現有領域模型——甚至在現有領域模型不再滿足需求時進行

重構——的業務能力爲開發過程中遇到的大部分創新提供了基礎。

 

本文轉至:http://www.infoq.com/cn/articles/ddd-evolving-architecture

發佈了130 篇原創文章 · 獲贊 15 · 訪問量 30萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章