本系列所有文章
如何一步一步用DDD設計一個電商網站(一)—— 先理解核心概念
如何一步一步用DDD設計一個電商網站(四)—— 把商品賣給用戶
如何一步一步用DDD設計一個電商網站(五)—— 停下腳步,重新出發
如何一步一步用DDD設計一個電商網站(六)—— 給購物車加點料,集成售價上下文
如何一步一步用DDD設計一個電商網站(七)—— 實現售價上下文
如何一步一步用DDD設計一個電商網站(八)—— 會員價的集成
如何一步一步用DDD設計一個電商網站(九)—— 小心陷入值對象持久化的坑
如何一步一步用DDD設計一個電商網站(十)—— 一個完整的購物車
如何一步一步用DDD設計一個電商網站(十一)—— 最后的准備
如何一步一步用DDD設計一個電商網站(十二)—— 提交並生成訂單
如何一步一步用DDD設計一個電商網站(十三)—— 領域事件擴展
閱讀目錄
一、前言
實際編碼已經寫了2篇了,在這過程中非常感謝有聽到觀點不同的聲音,借着這個契機,今天這篇就把大家提出的建議一個個的過一遍,重新整理,重新出發,為了讓接下去的DDD之路走的更好。
二、單元測試
蟋蟀兄在我的第三篇文章下面指出:
這點其實是我偷懶了,單元測試其實不單單在DDD中是一個很重要的一環,在我們崇尚敏捷,快速迭代的大背景下,有良好的單元測試模塊可以保證快速迭代下的項目質量。有甚至可以使用測試先行的TDD模式。
單元測試的好處我就不多說了,那么現在開始在項目中增加單元測試。單元測試有多種命名方式,我個人的方式是給每一個對象單獨建立一個測試類,然后里面每個單元測試方法的命名規則為"方法名_條件_預期的結果"這樣子。那么根據我們之前的Cart和CartItem的建模,編寫的單元測試如下:
[TestClass] public class CartTest { [TestMethod] [ExpectedException(typeof(ArgumentException))] public void Constructor_CartIdDefault_ThrowArgumentException() { var cart = new Cart(default(Guid), Guid.NewGuid(), DateTime.Now); Assert.AreNotEqual(null, cart); } [TestMethod] [ExpectedException(typeof(ArgumentException))] public void Constructor_UserIdDefault_ThrowArgumentException() { var cart = new Cart(Guid.NewGuid(), default(Guid), DateTime.Now); Assert.AreNotEqual(null, cart); } [TestMethod] [ExpectedException(typeof(ArgumentException))] public void Constructor_LastChangeTimeDefault_ThrowArgumentException() { var cart = new Cart(Guid.NewGuid(), Guid.NewGuid(), default(DateTime)); Assert.AreNotEqual(null, cart); } [TestMethod] public void AddCartItem_NotExisted_TotalItemCountIsIncreased() { var cart = new Cart(Guid.NewGuid(), Guid.NewGuid(), DateTime.Now); cart.AddCartItem(new CartItem(new Guid("11111111-1111-1111-1111-111111111111"), 1, 100)); Assert.AreEqual(1, cart.TotalItemCount()); cart.AddCartItem(new CartItem(new Guid("22222222-2222-2222-2222-222222222222"), 1, 100)); Assert.AreEqual(2, cart.TotalItemCount()); } [TestMethod] public void AddCartItem_Existed_TotalItemCountIsNotIncreasedTotalItemNumIsIncreased() { var cart = new Cart(Guid.NewGuid(), Guid.NewGuid(), DateTime.Now); cart.AddCartItem(new CartItem(new Guid("11111111-1111-1111-1111-111111111111"), 1, 100)); Assert.AreEqual(1, cart.TotalItemCount()); Assert.AreEqual(1, cart.TotalItemNum()); cart.AddCartItem(new CartItem(new Guid("11111111-1111-1111-1111-111111111111"), 1, 100)); Assert.AreEqual(1, cart.TotalItemCount()); Assert.AreEqual(2, cart.TotalItemNum()); } }
[TestClass] public class CartItemTest { [TestMethod] [ExpectedException(typeof(ArgumentException))] public void ModifyQuantity_LessZero_ThrowArgumentException() { var cartItem = new CartItem(new Guid("11111111-1111-1111-1111-111111111111"), 1, 100); cartItem.ModifyQuantity(-1); } [TestMethod] [ExpectedException(typeof(ArgumentException))] public void ModifyQuantity_EqualsZero_ThrowArgumentException() { var cartItem = new CartItem(new Guid("11111111-1111-1111-1111-111111111111"), 1, 100); cartItem.ModifyQuantity(0); } [TestMethod] public void ModifyQuantity_MoreZero_Success() { var cartItem = new CartItem(new Guid("11111111-1111-1111-1111-111111111111"), 1, 100); cartItem.ModifyQuantity(10); Assert.AreEqual(10, cartItem.Quantity); } [TestMethod] [ExpectedException(typeof(ArgumentException))] public void ModifyPrice_LessZero_ThrowArgumentException() { var cartItem = new CartItem(new Guid("11111111-1111-1111-1111-111111111111"), 1, 100); cartItem.ModifyPrice(-1); } [TestMethod] public void ModifyQuantity_EqualsZero_Success() { var cartItem = new CartItem(new Guid("11111111-1111-1111-1111-111111111111"), 1, 100); cartItem.ModifyQuantity(0); Assert.AreEqual(0, cartItem.Price); } [TestMethod] public void ModifyQuantity_MoreZero_Success() { var cartItem = new CartItem(new Guid("11111111-1111-1111-1111-111111111111"), 1, 100); cartItem.ModifyQuantity(10); Assert.AreEqual(10, cartItem.Price); } }
三、糾正錯誤,重新出發
在寫CartItemTest的時候發現了一個問題。領域對象的設計中有一個要點,就是實體必須需要通過其所屬的聚合根才能訪問,這樣才能體現出聚合的的整體性,並且減少外界對聚合內部過多的了解。而目前對於CartItem的運用卻有些背道而馳的意思,由外部對象進行實例化,必然增加了外部調用方對整個購物項構造過程的了解。有一位園友tubo有提到這點。
我思考了下,覺得這位園友的建議是對的。他建議的改法恰恰能夠滿足這個要求,隱藏了構造CartItem實體的細節。
好了那先把CartItem的構造函數訪問類型設置為internal吧,這樣也只能在CartItem所在的Domain項目中進行實例化了,然后再修改Cart.AddCartItem方法的參數。變為如下:
public void AddCartItem(Guid productId, int quantity, decimal price) { var cartItem = new CartItem(productId, quantity, price); var existedCartItem = this._cartItems.FirstOrDefault(ent => ent.ProductId == cartItem.ProductId); if (existedCartItem == null) { this._cartItems.Add(cartItem); } else { existedCartItem.ModifyPrice(cartItem.Price); //有可能價格更新了,每次都更新一下。 existedCartItem.ModifyQuantity(existedCartItem.Quantity + cartItem.Quantity); } }
單元測試也做出相應的更改:
[TestClass] public class CartTest { [TestMethod] [ExpectedException(typeof(ArgumentException))] public void Constructor_CartIdDefault_ThrowArgumentException() { var cart = new Cart(default(Guid), Guid.NewGuid(), DateTime.Now); Assert.AreNotEqual(null, cart); } [TestMethod] [ExpectedException(typeof(ArgumentException))] public void Constructor_UserIdDefault_ThrowArgumentException() { var cart = new Cart(Guid.NewGuid(), default(Guid), DateTime.Now); Assert.AreNotEqual(null, cart); } [TestMethod] [ExpectedException(typeof(ArgumentException))] public void Constructor_LastChangeTimeDefault_ThrowArgumentException() { var cart = new Cart(Guid.NewGuid(), Guid.NewGuid(), default(DateTime)); Assert.AreNotEqual(null, cart); } [TestMethod] public void AddCartItem_NotExisted_TotalItemCountIsIncreased() { var cart = new Cart(Guid.NewGuid(), Guid.NewGuid(), DateTime.Now); cart.AddCartItem(new Guid("11111111-1111-1111-1111-111111111111"), 1, 100); Assert.AreEqual(1, cart.TotalItemCount()); cart.AddCartItem(new Guid("22222222-2222-2222-2222-222222222222"), 1, 100); Assert.AreEqual(2, cart.TotalItemCount()); } [TestMethod] public void AddCartItem_Existed_TotalItemCountIsNotIncreasedTotalItemNumIsIncreased() { var cart = new Cart(Guid.NewGuid(), Guid.NewGuid(), DateTime.Now); cart.AddCartItem(new Guid("11111111-1111-1111-1111-111111111111"), 1, 100); Assert.AreEqual(1, cart.TotalItemCount()); Assert.AreEqual(1, cart.TotalItemNum()); cart.AddCartItem(new Guid("11111111-1111-1111-1111-111111111111"), 1, 100); Assert.AreEqual(1, cart.TotalItemCount()); Assert.AreEqual(2, cart.TotalItemNum()); } }
[TestClass] public class CartItemTest { [TestMethod] [ExpectedException(typeof(ArgumentException))] public void ModifyQuantity_LessZero_ThrowArgumentException() { var cart = new Cart(Guid.NewGuid(), Guid.NewGuid(), DateTime.Now); cart.AddCartItem(new Guid("11111111-1111-1111-1111-111111111111"), 1, 100); var cartItem = cart.GetCartItem(new Guid("11111111-1111-1111-1111-111111111111")); Assert.AreNotEqual(null, cartItem); Assert.AreEqual(1, cartItem.Quantity); cartItem.ModifyQuantity(-1); } [TestMethod] [ExpectedException(typeof(ArgumentException))] public void ModifyQuantity_EqualsZero_ThrowArgumentException() { var cart = new Cart(Guid.NewGuid(), Guid.NewGuid(), DateTime.Now); cart.AddCartItem(new Guid("11111111-1111-1111-1111-111111111111"), 1, 100); var cartItem = cart.GetCartItem(new Guid("11111111-1111-1111-1111-111111111111")); Assert.AreNotEqual(null, cartItem); Assert.AreEqual(1, cartItem.Quantity); cartItem.ModifyQuantity(0); } [TestMethod] public void ModifyQuantity_MoreZero_Success() { var cart = new Cart(Guid.NewGuid(), Guid.NewGuid(), DateTime.Now); cart.AddCartItem(new Guid("11111111-1111-1111-1111-111111111111"), 1, 100); var cartItem = cart.GetCartItem(new Guid("11111111-1111-1111-1111-111111111111")); Assert.AreNotEqual(null, cartItem); Assert.AreEqual(1, cartItem.Quantity); cartItem.ModifyQuantity(10); Assert.AreEqual(10, cartItem.Quantity); } [TestMethod] [ExpectedException(typeof(ArgumentException))] public void ModifyPrice_LessZero_ThrowArgumentException() { var cart = new Cart(Guid.NewGuid(), Guid.NewGuid(), DateTime.Now); cart.AddCartItem(new Guid("11111111-1111-1111-1111-111111111111"), 1, 100); var cartItem = cart.GetCartItem(new Guid("11111111-1111-1111-1111-111111111111")); Assert.AreNotEqual(null, cartItem); Assert.AreEqual(100, cartItem.Price); cartItem.ModifyPrice(-1); } [TestMethod] public void ModifyPrice_EqualsZero_Success() { var cart = new Cart(Guid.NewGuid(), Guid.NewGuid(), DateTime.Now); cart.AddCartItem(new Guid("11111111-1111-1111-1111-111111111111"), 1, 100); var cartItem = cart.GetCartItem(new Guid("11111111-1111-1111-1111-111111111111")); Assert.AreNotEqual(null, cartItem); Assert.AreEqual(100, cartItem.Price); cartItem.ModifyPrice(0); Assert.AreEqual(0, cartItem.Price); } [TestMethod] public void ModifyPrice_MoreZero_Success() { var cart = new Cart(Guid.NewGuid(), Guid.NewGuid(), DateTime.Now); cart.AddCartItem(new Guid("11111111-1111-1111-1111-111111111111"), 1, 100); var cartItem = cart.GetCartItem(new Guid("11111111-1111-1111-1111-111111111111")); Assert.AreNotEqual(null, cartItem); Assert.AreEqual(100, cartItem.Price); cartItem.ModifyPrice(10); Assert.AreEqual(10, cartItem.Price); } }
這樣一來,被玻璃魚兒和netfocus2位園友所指出的奇怪的“UserBuyProductDomainService”也自然消失了。應用層代碼變成:
public Result Buy(Guid userId, Guid productId, int quantity) { var product = DomainRegistry.ProductService().GetProduct(productId); if (product == null) { return Result.Fail("對不起,未能獲取產品信息請重試~"); } var cart = _getUserCartDomainService.GetUserCart(userId); cart.AddCartItem(productId, quantity, product.SalePrice); DomainRegistry.CartRepository().Save(cart); return Result.Success(); }
四、結語
DDD的道路是坎坷的,我希望通過在園子里發布的文章能夠結交到志同道合的DDD之友,歡迎大家不吝嗇自己的見解,多多留言,也讓想學習或者正在學習DDD的園友少走一些彎路。
本文的源碼地址:https://github.com/ZacharyFan/DDDDemo/tree/Demo5。
作者:Zachary
出處:https://zacharyfan.com/archives/141.html
▶關於作者:張帆(Zachary,個人微信號:Zachary-ZF)。堅持用心打磨每一篇高質量原創。歡迎掃描右側的二維碼~。
定期發表原創內容:架構設計丨分布式系統丨產品丨運營丨一些思考。
如果你是初級程序員,想提升但不知道如何下手。又或者做程序員多年,陷入了一些瓶頸想拓寬一下視野。歡迎關注我的公眾號「跨界架構師」,回復「技術」,送你一份我長期收集和整理的思維導圖。
如果你是運營,面對不斷變化的市場束手無策。又或者想了解主流的運營策略,以豐富自己的“倉庫”。歡迎關注我的公眾號「跨界架構師」,回復「運營」,送你一份我長期收集和整理的思維導圖。