用領域驅動DDD的方式實現購物車-基於abp一代6.2

廢話

以前七七八八看了些DDD相關概念,充血模型、領域事件、領域服務、應用服務等,大體能理解但從未實踐。最近在用ABP作個電商模塊,嘗試用DDD方式來實現購物車功能,感受還行,下面作個記錄。前端

下面這些內容只是我的理解,未必正確。git

領域實體充血模型-定義知足業務規則的實體類

購物車模塊涉及到兩個實體ShoppingCartEntity(購物車)、ShoppingCartItemEntity(購物車明細),按我以前的作法會直接定義成POCO,僞代碼以下:數據庫

public class ShoppingCartEntity
{
     public long Id { get; set; }
     //關聯的顧客id
     public long CustomerId{ get; set; }  
     public CustomerEntity Customer { get; set; }
     //購物車明細集合
     public List<ShoppingCartItemEntity> Items { get; set; }

     //,...略
}

購物車明細定義就省了,意思就是屬性所有get; set; ,它只是用來給EF作映射,作數據庫操做,作數據傳遞用。session

但這樣的領域實體類沒法真是表達業務規則,添加商品到購物車、從購物車移除商品等購物車相關操做,咱們可能會放到應用層(或者老式的業務邏輯層BLL),這樣沒法體現購物車實體的功能,代碼複用性也很低。app

咱們思考下:函數

  • 若是咱們將購物車關聯的顧客id設置爲只讀的,而後經過構造函數來初始化它,由於不但願別人調用咱們的購物車對象時將關聯的顧客設置爲空
  • 若是咱們將購物車明細設置爲只讀的,而後在購物車實體上提供:添加商品到購物車、從購物車移除商品、清空購物車等操做如何?由於購物車明細的變化會影響到購物車金額和積分的統計
  • 若是咱們在購物車操做的不一樣點觸發一些事件如何?好比:當將商品添加進購物車時觸發一個事件,由於但願未來別人使用咱們的購物車模塊時能加入它們的業務邏輯。

這樣一來,購物車相關的操做都封裝進購物車實體,未來應用層的代碼就會變得不多,代碼複用性、可擴展性也高。本屬於購物車的功能就定義在購物車實體上也更直觀。ui

不少屬性應該是私有的

先說一點,咱們定義一個方法、一個屬性、一個類、一個軟件時,必定要考慮這些功能可能在任什麼時候候、任何地方、被任何一個SB(包括我本身)調用,他們極可能不按你的預期來。spa

購物車必須是屬於某個顧客的,也就是必須有個關聯的CustomerId,這是咱們的業務規則,也是約束,但按咱們上面的定義爲get; set; 別人可能給他賦值個0或負數,這就讓購物車實體處於不正確的狀態,因此應該把CustomerId設置爲{ get; private set; },同理在定義購物車明細時關聯的ProductId(商品Id)也應該是隻讀的,由於購物車明細必須與某個商品關聯纔是正常的。設計

咱們能夠在構造函數中定義參數來初始化這些只讀屬性。對象

如此這般,當建立一個購物車實體後,這個對象不管被誰訪問,CRUD工程師們沒法像之前同樣破壞它的狀態。

至於到底哪些屬性該是隻讀的,哪些是public的應該根據場景,每一個屬性認真思考再決定。

若是非要在某個階段造成一個不符合業務要求的實體,能夠考慮使用Builder模式

有時候你發現僅僅是經過構造函數才能初始化一個對象,感受很不方便,由於對象可能須要先new出來,而後在各個步驟對它進行賦值,最後才能造成一個咱們滿意的對象(有嚴格約束,且符合業務規則),我的以爲這個時候應該爲它建立一個對應的Builder對象,把那些臨時的狀態屬性設置到Builder上,最後Builder.Build();生成一個符合業務規則的對象。這種狀況不只僅適用用域領域實體,整個軟件設計中都適用。

在目前的購物車功能好像體現不了這個。

EF查詢時能夠訪問到私有構造函數、設置只讀屬性

這個很重要,以前一直曉得領域實體屬性有些應該是隻讀的,但考慮用ef沒法給只讀屬性賦值,因此後來放棄了,也不曉得從啥時候開始,咱們定義的領域實體的私有構造函數和屬性EF是能夠直接訪問的,這就給咱們定義符合業務規則的實體創造了機會。

AutoMapper能夠經過構造函數作映射

上面的領域實體若是關鍵屬性爲只讀的了,我們作dto到實體的映射呢?印象裏AutoMapper是能夠經過構造函數作映射的,恰好咱們上面說了咱們的實體是有對應的構造函數的。這個規則有待證明。

領域實體應該有業務方法

想象下,將商品加入購物車這個功能,按我原來的作法會在應用層查詢出購物車,好比這個對象叫shoppingCart,那麼我會直接

在應用層中:
shoppingCart.Items.Add(item);
//計算明細對應的金額(明細數量*關聯商品的單價)
//其它處理

仔細考慮下,將商品加入購物車這個方法不是應該定義在購物車實體上嗎?若是這樣,商品進入購物車,後續要重新計算金額、積分之類的邏輯也都會寫在購物車實體內部,而不是放在應用層。這樣,應用層未來只須要shoppingCart.AddItem(item);是否是更符合業務場景?

領域實體的方法只修改本身的狀態屬性

以訂單支付這個方法爲例,訂單支付 要修改支付狀態爲已支付、改變支付金額、將物流狀態改成待發貨等等,支付狀態、物流狀態、支付金額 這些屬性都是訂單實體類的,購物車實體中的方法也只是修改本身實體的狀態屬性。

別想在領域實體裏去作依賴注入、訪問數據庫或其它服務

領域實體裏只是根據業務定義相關方法,這些操做都是跟這個領域實體相關的,狀態屬性的改變。依賴注入、訪問數據庫或其它服務能夠在應用層或領域服務去作。

經過領域事件實現可擴展性

咱們能夠在購物車中定義這樣的事件:當商品明細加入購物車後觸發、當移除購物車明細時觸發、當購物車明細數量變動時觸發.....等等。這樣咱們的購物車模塊能夠作得很乾淨,未來別人使用這個模塊時能夠訂閱這些事件來擴展購物車模塊。

這個事件的功能是abp自帶的事件總線,能夠去參考官方文檔。

而且這個事件仍是事務性的,意思說若是未來別人擴展咱們的模塊,在它們的事件處理代碼中若操做數據庫,和咱們處理購物車邏輯是在一個數據庫事務中,他們能夠拋出異常來阻止咱們的正常提交。

領域服務

DDD的說法是當一個功能沒法只歸結到一個領域實體上時能夠考慮領域服務,協調多個實體或其它領域服務時也行。

目前在購物車模塊中沒有使用領域服務,仍是以訂單支付爲例

上面說了,訂單實體自己定義了個「支付」的方法,它內部改變訂單本身的狀態(修改訂單狀態、修改支付狀態、修改物流狀態),然而訂單支付還涉及到其它處理,好比:要先判斷顧客會員等級、餘額狀況、是否是黑名單 等等,這裏就涉及到多個實體和服務了,因此在訂單領域服務中有個支付方法,它會作各類業務判斷處理後再調用訂單實體.支付();

領域服務中也能夠觸發領域事件

領域服務也屬於領域層,也能夠觸發相關事件,以這種方式來預留擴展點。abp也提供了這個功能。

什麼時候使用領域服務?合適使用領域事件?

我比較傾向用事件,上面說支付訂單前要作各類業務判斷,好比會員等級決定折扣、餘額檢查等,用領域服務很直觀,可是不夠靈活,好比未來又變了,要在支付前作更多判斷呢?此時若是在支付前觸發一個事件,那麼未來有新的需求就能夠加新的事件處理器,不符合業務規則的狀況,在事件處理邏輯中拋異常就能夠了。

領域服務中是否訪問當前用戶(session)?

不建議,當前登錄用戶嚴格來講是應用程序狀態,而領域服務是細小的領域邏輯,它與應用程序狀態無關。

應用服務

領域層整好了,這個代碼會變得不多,

它訪問數據庫獲得領域實體,也能夠依賴注入領域服務。按業務流程逐個調用領域實體和領域服務的相關方法,一般感受對應用戶的一個操做,好比點個按鈕提交

它訪問當前用戶

它作權限判斷等。

它作基本數據校驗

它作dto到實體的映射

開始事務、調用領域服務、實體後提交事務

將商品加入購物車的流程

顧客點擊「加入購物車」,前端上傳商品(或skuId)

應用層作權限判斷、基本數據驗證、而後查詢當前用戶關聯的購物車

調用購物車.AddItem(item);

購物車領域實體檢測這個商品是否已存在購物車了,若在則累加數量,並觸發購物車明細數量改變的事件;若不存在則添加商品到購物車並觸發 購物車明細增長成功的事件

事件處理程序預留給模塊使用方進行擴展的

若是業務流程複雜,在應用層可能還有好幾個步驟要作,但如何完成一般是交給領域服務和實體

應用層最後保存數據到數據庫(事務)

相關文章
相關標籤/搜索