天天看點

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

廢話

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

  • 業務分析和設計說明參考:https://gitee.com/bxjg1987/abp/wikis/購物車?sort_id=3392905。
  • 完整購物車源碼參考:https://gitee.com/bxjg1987/abp/tree/master/src/Shop

下面這些内容隻是個人了解,未必正确。

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

購物車子產品涉及到兩個實體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做映射,做資料庫操作,做資料傳遞用。

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

我們思考下:

  • 如果我們将購物車關聯的顧客id設定為隻讀的,然後通過構造函數來初始化它,因為不希望别人調用我們的購物車對象時将關聯的顧客設定為空
  • 如果我們将購物車明細設定為隻讀的,然後在購物車實體上提供:添加商品到購物車、從購物車移除商品、清空購物車等操作如何?因為購物車明細的變化會影響到購物車金額和積分的統計
  • 如果我們在購物車操作的不同點觸發一些事件如何?比如:當将商品添加進購物車時觸發一個事件,因為希望将來别人使用我們的購物車子產品時能加入它們的業務邏輯。

這樣一來,購物車相關的操作都封裝進購物車實體,将來應用層的代碼就會變得很少,代碼複用性、可擴充性也高。本屬于購物車的功能就定義在購物車實體上也更直覺。

很多屬性應該是私有的

先說一點,我們定義一個方法、一個屬性、一個類、一個軟體時,一定要考慮這些功能可能在任何時候、任何地方、被任何一個SB(包括我自己)調用,他們很可能不按你的預期來。

購物車必須是屬于某個顧客的,也就是必須有個關聯的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);

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

事件處理程式預留給子產品使用方進行擴充的

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

應用層最後儲存資料到資料庫(事務)

用支付寶掃一掃,咱倆都可以獲得一個小紅包

關注我的今日頭條,有不錯的c#.net經驗分享

本文來自部落格園,作者:變形精怪,轉載請注明原文連結:https://www.cnblogs.com/jionsoft/p/14333041.html

abp