人物介紹:
Jack Chen ——「寵物商店」的首席架構架構師,擁有豐富的軟體設計與建模經驗,但對新生事物持懷疑態度。
王總——「寵物商店」的總經理,從美國留學後回國創立「寵物商店」網站。 一路來唾手可得的成功讓他養成了固執專橫的行事作風。
Spark —— Jack Chen的大學同學,一家商務軟體公司的高級諮詢顧問。 最近熱衷於宣揚「領域驅動設計」的最佳實踐。
引子
就象大家所聽說過的那些神奇小子創業故事一樣,幾只從大西洋游回的海龜找到了一個偉大的idea——在互聯網上開辦線上商店銷售寵物。 幸虧的是他們找到了投資者而且發展的很不錯。 但是隨著時間的推移,當初「完美」的技術架構隨著越來越多的裝進籃子的需求後變得不堪重負。 作為公司首席架構師的Jack Chen已經被這幾個月「雞毛蒜皮」的需求折磨失眠好幾天啦。
Jack Chen週一一早就被興奮的王總給喊進了辦公室,立即就被王總扔出來的idea嚇傻了。
「我有一個很cool的想法,我們可以線上為寵物醫院提供線上預約的服務業務。 而不僅僅是賣掉它們,你知道這意味著什麼嗎? 這是一個年產值上百億的市場!!! 」。
「可是王總,我們的系統不能支援這種非實物的服務預訂銷售,它可能對我們原有的網站形成巨大的衝擊,我們需要三個月的時間對這個業務進行全方面的評估...」
Jack Chen立即就被氣勢洶洶的王總打斷了,「三個月的評估? 我需要在兩個月內就給我上線這個新業務。 我們的投資人非常認可我的idea,並要求我們立即把這個專案上線,它可能會説明我們提高明年的IPO價格。 你明白嗎? DO IT ASAP! 」
評估
「好吧,也許這個該死的王胖子是對的。 我們這個將技術與業務混在一起的亂攤子也是到了該整理整理的時候。 」自言自語發了半小時牢騷後的Jack Chen終於恢復到正常狀態上來了,我想我應該看看我們現在是什麼樣子的,為了支援這個該死的「線上為寵物醫院提供線上預約的服務」的需求我們需要做出哪些改變。 於是Jack Chen在白板上很快的就畫出了下面的Use Case圖來。
圖1 原寵物商店UseCase匯總圖
為了支援「線上預約」這種特殊的產品,它會影響到大部分的Use Case,具體列舉如下:
商品資訊需要增加「預約時間」這個屬性,客戶在下訂單時會把它作為標識一個預約的關鍵要素。 「線上預約」是個虛擬的商品,它可不需要真的需要去檢貨和包裝發貨,如果真的那麼做啦,我就太傻了。 每個寵物醫院每天都只能接受一定數量的預約,從這個概念上來說,它與實物商品有類似的庫存概念。 可是我該怎麼去表達它們呢? 最要命的是:我真的要把這些所有受影響的Use Case都翻出來去讓它們支援虛擬物品的業務嗎? 我怎麼可能在2個月內完成這些重構? 銀彈
了無生趣的Jack Chen在王總的辦公室門口徘徊了N圈,還是沒有勇氣去迎接那一通狂風暴雨般的中英文雙語版的羞辱謾駡。 「也許事情是有轉機的,我好象在哪裡聽說過有種銀彈可以解決這種系統重構的問題的」。 「該死,誰把Spark送給我的《領域驅動設計》墊在顯示器下啦,他一直在向我佈道這本書給他的專案帶來的種種神奇改變,也許我也可以試試它的威力」。
「好吧,Spark,我承認你給推薦的書非常棒,你說的也很有道理。 我讀了它,明白並一些概念——例如:領域分割 、Entity、Service、Value Object...,可我對於該如何去做還是一頭霧水。 你能不能直接把你從重構專案中獲得的最佳實踐直接分享給我呢? 不然的話,週一王胖子是不會放過交不出答案的我的! 」。 讀完了這本書,Jack Chen覺得很有收穫,但又不知道怎麼開始,打個電話給領域建模的先行者Spark也許真的是解決問題最快的方法。
「什麼,這個問題說來話長? 不要緊,我已經在你家門口了,你同我慢慢說」,Jack Chen帶著星巴克咖啡+肯德基全家桶+久久鴨脖+諂媚的笑容出現在Spark家門口。
佈道
Spark聽完了Jack Chen對於現狀及需求的描述之後,一幅氣定神閑的樣子訕訕地說出「這個很簡單嘛,你現在需要做的只是這樣一些事情:」
用大比例結構對你的系統進行領域劃分找出這個需求影響的領域及對外介面建立一個適合你們公司的領域驅動設計的技術框架按照需求的緊急度來重構各個領域的設計與編碼
下面我們就按照這個順序來實踐一下:
一、概要領域劃分
Jack Chen立即把自己之前畫的Use Case重畫了一遍,然後用希冀的眼神看著Spark等待著認可。 「你的錯誤是過於看重Case或者操作者身份,領域的劃分不是基於功能或角色來進行的,通常來說我們是將內聚程度較高的Use Case歸到一個上下文中。 儘量使得領域自閉程度較高,並擁有相同的業務語言環境。 例如基於你的Use Case圖,我會畫出以下的領域」
圖2 寵物商店領域通道圖
通道圖是一個對業務領域建模非常有説明的工具,它可以同時表達出執行序列與分片的作用。
二、找出受影響的領域與介面
從領域的角度來看,只有商品對外暴露出來的介面是會影響到各個領域,需要優先建立商品領域(ProductDomain)及讀取商品資訊服務介面(GetProductService)來進行重構。
之外,在【圖2】 中用綠色標識出來的Use Case是由於增加支援「線上預約」這種虛擬商品所需要進行代碼重構的部分。 這部分工作如果工期比較緊,可以優先使用模式的方式來進行代碼重構,這樣也可以在之後更加容易用領域驅動設計的方法再次重構。
三、建立技術框架
這一點,是《領域驅動設計》這本書沒有過多提及的內容。 這個需要結合你們公司的原來技術框架用最小化改造成本最大化收益的方式來建立領域驅動的技術框架。 下面是一個可以廣泛使用的領域驅動的技術框架,可以在這之上增加更多的個性元素形成你公司自己的框架。
圖3 領域驅動設計參考技術框架圖
這個框架的各個元素基本上在 《領域驅動設計》一書中都可以找到對應的解釋,但這裡需要解釋一下我建立這個框架的個性理解:
領域對外(頁面、AJAX、ESB調用)只暴露領域服務,其它所有領域類都是包內自閉的,對外不可見。 基礎倉庫的引入,基礎倉庫是一個抽象的倉庫,它封裝了大量常用工具方法、業務物件生命週期維護(實體OR映射、DAO調用)、外部介面調用。 可以降低業務倉庫不必要的重複編碼與複雜性。 業務倉庫是繼承基礎倉庫的子類。 基礎設施的引用,基礎設施是用來承載引用非領域調用的樁,我們在使用領域驅動設計的時候往往是從一個舊的系統重構開始。 這時我們不可能要求所有的業務子系統相互調用都通過Domain Service調用,這時我們可以通過Infrastructure優美的把調用封裝在業務倉庫的業務方法內。
四、重構受影響領域的設計與編碼
圖4 重構後的商品詳情頁類圖
Spark以商品詳情頁這個Use Case為例展示了以領域驅動設計的重構類圖:
增加行為表ProductExt用於存儲商品的擴展資訊,如預約時間段、預約醫院。 並為表建立一一對應的實體Entity。 基礎倉庫Repository通過Infrastructure中的DAO封裝了對實體的操作,如create()、update()、delete()、findById()、findList() 商品業務倉庫ProductRepository擴展了基礎倉庫,客戶程式可以用productId為參數,通過ProductVo.getProduct()方法獲得商品詳細資訊的業務實現, 由於業務倉庫的的公開方法對外返回的都是Value Object,因此不會直接暴露Entity類型給客戶程式。 GetProductService服務類通過invoke()服務方法 對外(商品詳情頁面)提供服務,它通調用業務倉庫中的業務方法,並將介面規格化。 事務配置在DomainService的invoke()方法上,即事務控制以Use Case為細微性進行控制。 尾聲
在Spark的説明下,Jack Chen成功的脫離了困境。 現在他正在公司裡積極推行自己的領域驅動設計框架,他們公司的網站正在以每三週一次的重構速度快速反覆運算演進。 他象Spark一樣,成為了一個領域驅動的佈道者。
來源:InfoQ