DDD簡明入門之道 - 開篇
DDD簡明入門之道 - 開篇
猶豫了很久才寫下此文,一怕自己對DDD的理解和實踐方式有偏差,二怕誤人子弟被貽笑大方,所以紕漏之處還望各位諒解。不囉嗦,馬上進入正題,如果你覺得此文不錯就點個贊吧。
概述
“Domain-Driven Design領域驅動設計”簡稱DDD,是一套綜合軟體系統分析和設計的面向物件建模方法。關於DDD的學習資料園子裡面有很多,大家可以自行參考,這裡不過多介紹。
核心
DDD的核心是領域物件的建模,說白了就是怎麼樣從業務需求中抽象出我們需要的資料結構,通過這些資料結構之間的相互作用來實現我們的業務功能。這裡的所說的資料結構是廣義的,Domain裡面的每一個類其實就是一個數據結構。這裡說的有點抽象了,接下來我們將通過一個具體業務需求的開發來展開。
案例
假設需要開發一個電商平臺,我們把平臺按功能拆分成多個子系統,子系統之間以微服務形式進行互動呼叫。拆分後的子系統大致如下:
- 產品系統(PMS)
- 訂單系統(OMS)
- 交易系統(TMS)
- 發貨系統(DMS)
- 其他系統...
而你將會負責訂單系統的開發工作,訂單系統需要支撐的業務包括使用者下單、支付、平臺發貨、使用者確認收貨、使用者取消訂單等業務場景,下面我們就圍繞這些場景來對訂單業務進行建模。
訂單建模
//訂單資訊 public class Order { public int Id{get;set;} public string OrderNo{get;set;} public OrderStatus Status{get;set;} public Address Address{get;set;} public List<OrderLine> Lines{get;set;} public decimal ShippingFee{get;set;} public decimal Discount{get;set;} public decimal GoodsTotal{get;set;} public decimal DueAmount{get;set;} } //訂單狀態 public enum OrderStatus { PendingPayment = 0, PendingShipment = 10, PendingReceive = 20, Received = 30, Cancel = 40 } //地址 public class Address { public string FullName{get;set;} public string FullAddress{get;set;} public string Tel{get;set;} }
OrderLine.cs //訂單明細 public class OrderLine { public int Id{get;set;} public int SkuId{get;set;} public string SkuName{get;set;} public string Spec{get;set;} public int Qty{get;set;} public decimal Cost{get;set;} public decimal Price{get;set;} public decimal Total{get;set;} }
Txn.cs //交易信息 public class Txn { .... }
Shipment.cs //發貨資訊 public class Shipment { .... }
模型改進
類似上面的模型我們在傳統的三層中經常使用,模型中只包含簡單的業務屬性,這些業務屬性的賦值將會在服務層中去進行。這些模型只是用來裝資料的殼子,或者叫做容器,完全就是為了和資料庫表建立對應關係而存在的。還記得DataTable時代嗎?我們完全可以連上面這些模型都不要也是一樣可以操作資料庫表的。
- Class 不等於 OO
- 給模型賦予行為
- 深度面向物件程式設計
/// <summary> /// 訂單資訊 /// </summary> public class Order { private List<OrderLine> _lines; public Order() { _lines = new List<OrderLine>(); } /// <summary> /// 建立訂單(簡單工廠) /// </summary> /// <param name="orderNo"></param> /// <param name="address"></param> /// <param name="skus"></param> /// <returns></returns> public static Order Create(string orderNo, Address address, SaleSkuInfo[] skus) { Order order = new Order(); order.OrderNo = orderNo; order.Address = address; order.Status = OrderStatus.PendingPayment; foreach(var sku in skus) { order.AddLine(sku.Id,sku.Qty); } order.CalculateFee(); return order; } /// <summary> /// Id /// </summary> public int Id{get; private set;} /// <summary> /// 訂單號 /// </summary> public string OrderNo{get; private set;} /// <summary> /// 訂單狀態 /// </summary> public OrderStatus Status{get; private set;} /// <summary> /// 收貨地址 /// </summary> public Address Address{get; private set;} /// <summary> /// 訂單明細 /// </summary> public List<OrderLine> Lines { get{return this._lines;} private set { this._lines = value; } } /// <summary> /// 運費 /// </summary> public decimal ShippingFee { get; private set; } /// <summary> /// 折扣金額 /// </summary> public decimal Discount{ get; private set; } /// <summary> /// 商品總價值 /// </summary> public decimal GoodsTotal { get; private set; } /// <summary> /// 應付金額 /// </summary> public decimal DueAmount { get; private set; } /// <summary> /// 實付金額 /// </summary> public decimal ActAmount { get; private set; } /// <summary> /// 新增明細 /// </summary> /// <param name="skuId"></param> /// <param name="qty"></param> public void AddLine(int skuId, int qty) { var product = ServiceProxy.ProductService.GetProduct(new GetProductRequest{SkuId = skuId}); if(product == null) { throw new SkuNotFindException(skuId); } OrderLine line = new OrderLine(skuId, product.SkuName, product.Spec, qty, product.Cost, product.Price); this._lines.Add(line); } /// <summary> /// 訂單費用計算 /// </summary> public void CalculateFee() { this.CalculateGoodsTotal(); this.CalculateShippingFee(); this.CalculateDiscount(); this.CalculateDueAmount(); } /// <summary> /// 訂單支付 /// </summary> /// <param name="money"></param> public void Pay(decimal money) { if (money <= 0) { throw new ArgumentException("支付金額必須大於0"); } this.ActAmount += money; if (this.ActAmount >= this.DueAmount) { if (this.Status == OrderStatus.PendingPayment) { this.Status = OrderStatus.PendingShipment; } } } /// <summary> /// 計算運費 /// </summary> private decimal CalculateShippingFee() { //夠買商品總價值小於100則收取8元運費 this.ShippingFee = this.CalculateGoodsTotal() > 100 ? 0 : 8m; return this.ShippingFee; } /// <summary> /// 計算折扣 /// </summary> private decimalCalculateDiscount() { this.Discount = decimal.Zero; //todo zhangsan 暫未實現 return this.Discount; } /// <summary> /// 計算商品總價值 /// </summary> private decimal CalculateGoodsTotal() { this.GoodsTotal = this.Lines.Sum(line => line.CalculateTotal()); return this.GoodsTotal; } /// <summary> /// 計算應付金額 /// </summary> /// <returns></returns> private decimal CalculateDueAmount() { this.DueAmount = this.CalculateGoodsTotal() + CalculateShippingFee() - CalculateDiscount(); return this.DueAmount; } }
在上面的Order類中,我們給它添加了一系列業務相關的行為(方法),使得其不再象普通三層裡的模型只是一個數據容器,而且整個類的設計也更加的面向物件。
- public static Order Create(string orderNo, Address address, SaleSkuInfo[] skus)
==Create()方法用來建立新訂單,訂單的建立是一個複雜的裝配過程,這個方法可以封裝這些複雜過程,從而降低呼叫端的呼叫複雜度。== - public void AddLine(int skuId, int qty)
==AddLine()方法用於將使用者購買的商品新增到訂單中,該方法中使用者只需要傳遞購買的商品Id和購買數量即可。至於商品的具體資訊,比如名稱、規格、價格等資訊,我們將會在方法中呼叫產品介面實時去查詢。這裡涉及到和產品系統的互動,我們定義了一個ServiceProxy類,專門用來封裝呼叫其他系統的互動細節。== - public void CalculateFee()
==CalculateFee()方法用於計算訂單的各種費用,如商品總價、運費、優惠等。== - public void Pay(decimal money)
==Pay()方法用於接收交易系統在使用者支付完畢後的呼叫,因為在上文中我們說到訂單系統和交易系統是兩個單獨的系統,他們是通過webapi介面呼叫進行互動的。訂單系統如何知道某個訂單支付了多少錢,就得依賴於交易系統的呼叫傳遞交易資料了,因為訂單系統本身不負責處理使用者的交易。==
/// <summary> /// 訂單明細 /// </summary> public class OrderLine { public OrderLine() { } public OrderLine(int skuId, string skuName, string spec, int qty, decimal cost, decimal price) : this() { this.SkuId = skuId; this.SkuName = skuName; this.Spec = spec; this.Qty = qty; this.Cost = cost; this.Price = price; } /// <summary> /// Id /// </summary> public int Id { get; set; } /// <summary> /// 商品Id /// </summary> public int SkuId { get; set; } /// <summary> /// 商品名稱 /// </summary> public string SkuName { get; set; } /// <summary> /// 商品規格 /// </summary> public string Spec { get; set; } /// <summary> /// 購買數量 /// </summary> public int Qty { get; set; } /// <summary> /// 成本價 /// </summary> public decimal Cost { get; set; } /// <summary> /// 售價 /// </summary> public decimal Price { get; set; } /// <summary> /// 小計 /// </summary> public decimal Total { get; set; } /// <summary> /// 小計金額計算 /// </summary> /// <returns></returns> public decimal CalculateTotal() { this.Total = Qty * Price; return this.Total; } }
/// <summary> /// 服務代理 /// </summary> public class ServiceProxy { public static IProductServiceProxy ProductService { get { return new ProductServiceProxy(); } } public static IShipmentServiceProxy ShipmentServiceProxy { get { return new ShipmentServiceProxy(); } } }
/// <summary> /// 產品服務代理介面 /// </summary> public class ProductServiceProxy : IProductServiceProxy { public GetProductResponse GetProduct(GetProductRequest request) { //todo zhangsan 這裡先硬編碼資料進行模擬呼叫,後期需要呼叫產品系統Api介面獲取資料 if (request.SkuId == 1138) { return new GetProductResponse() { SkuId = 1138, SkuName = "蘋果8", Spec = "128G 金色", Cost = 5000m, Price = 6500m }; } if (request.SkuId ==1139) { return new GetProductResponse() { SkuId = 1139, SkuName = "小米充電寶", Spec = "10000MA 白色", Cost = 60m, Price = 100m }; } if (request.SkuId == 1140) { return new GetProductResponse() { SkuId = 1140, SkuName = "怡寶瓶裝礦泉水", Spec = "200ML", Cost = 1.5m, Price = 2m }; } return null; } }
邏輯驗證
上面程式碼的邏輯是否與我們預期的一致,該如何驗證?這裡我們通過單元測試的方式來進行校驗,且看我們是如何測試的吧。
[TestClass] public class OrderTest { /// <summary> /// 訂單建立邏輯測試 /// </summary> [TestMethod] public void CreateOrderTest() { Address address = new Address(); address.FullName = "張三"; address.FullAddress = "廣東省深圳市福田區xxx街道888號"; address.Tel = "13800138000"; List<SaleSkuInfo> saleSkuInfos = new List<SaleSkuInfo>(); saleSkuInfos.Add(new SaleSkuInfo(1138,2)); saleSkuInfos.Add(new SaleSkuInfo(1139, 3)); //商品總金額大於100分支 Order order = Order.Create("181027887609", address, saleSkuInfos.ToArray()); Assert.AreEqual(OrderStatus.PendingPayment, order.Status); Assert.AreEqual(2, order.Lines.Count); Assert.AreEqual(13300, order.DueAmount); //商品總金額小於100分支 Order order1 = Order.Create("181027887610", address, new SaleSkuInfo[]{ new SaleSkuInfo(1140, 3)}); Assert.AreEqual(OrderStatus.PendingPayment, order1.Status); Assert.AreEqual(1, order1.Lines.Count); Assert.AreEqual(8m, order1.ShippingFee); Assert.AreEqual(14, order1.DueAmount); } /// <summary> /// 訂單支付邏輯測試 /// </summary> [TestMethod] public void PayOrderTest() { Address address = new Address(); address.FullName = "張三"; address.FullAddress = "廣東省深圳市福田區xxx街道888號"; address.Tel = "13800138000"; List<SaleSkuInfo> saleSkuInfos = new List<SaleSkuInfo>(); saleSkuInfos.Add(new SaleSkuInfo(1138, 2)); saleSkuInfos.Add(new SaleSkuInfo(1139, 3)); //商品總金額大於100分支 Order order = Order.Create("181027887609", address, saleSkuInfos.ToArray()); Assert.AreEqual(OrderStatus.PendingPayment, order.Status); Assert.AreEqual(2, order.Lines.Count); Assert.AreEqual(13300, order.DueAmount); //部分支付分支 order.Pay(5000); Assert.AreEqual(5000m, order.ActAmount); Assert.AreEqual(OrderStatus.PendingPayment, order.Status); //部分支付分支 order.Pay(1000); Assert.AreEqual(6000m, order.ActAmount); Assert.AreEqual(OrderStatus.PendingPayment, order.Status); //全部支付分支 order.Pay(7300); Assert.AreEqual(13300m, order.ActAmount); Assert.AreEqual(OrderStatus.PendingShipment, order.Status); } }
本文地址: ofollow,noindex" target="_blank">https://www.cnblogs.com/huangzelin/p/9861439.html ,轉載請申明出處。
結語
到這裡,不知道大家注意沒有,上面的編碼過程我們沒有提到任何的資料庫設計與儲存之類的問題。我們一心都在奔著分析業務,設計模型和實現業務處理邏輯來編碼,DDD的設計上有個原則叫忘掉資料庫。
在我看來我們的大多數應用程式的執行過程是這樣的:
- 接收使用者輸入
- 程式記憶體組裝業務物件
- 將物件持久化到儲存裝置(資料庫等)
當然還有另外一種是:
- 接收使用者輸入
- 從持久化裝置讀取資料(資料庫等)
- 程式根據讀取的資料記憶體組裝業務物件
- 將物件返回呼叫端
==從上面的分析來看記憶體中領域物件組裝過程是最核心的,因其業務千變萬化,沒法用程式碼做到通用處理。而資料的持久化相對來說沒啥具體業務邏輯,程式碼上的通用也比較容易。所以,我們可以說DDD方式程式設計的專案,領域模型設計的合理就意味著這個專案已經成功大半了。==
最後,感謝各位看官聽我嘮叨了這麼久,有問題請給我留言。謝謝
檢視原始碼請移步到: https://github.com/hzl091/NewSale
支付寶打賞 | 微信打賞 |
![]() |
![]() |