DDD簡明入門之道 - 開篇
猶豫了好久才寫下此文,一怕本身對DDD的理解和實踐方式有誤差,二怕誤人子弟被貽笑大方,因此紕漏之處還望各位諒解。不囉嗦,立刻進入正題,若是你以爲此文不錯就點個贊吧。html
概述
「Domain-Driven Design領域驅動設計」簡稱DDD,是一套綜合軟件系統分析和設計的面向對象建模方法。關於DDD的學習資料園子裏面有不少,你們能夠自行參考,這裏不過多介紹。git
核心
DDD的核心是領域對象的建模,說白了就是怎麼樣從業務需求中抽象出咱們須要的數據結構,經過這些數據結構之間的相互做用來實現咱們的業務功能。這裏的所說的數據結構是廣義的,Domain裏面的每個類其實就是一個數據結構。這裏說的有點抽象了,接下來咱們將經過一個具體業務需求的開發來展開。github
案例
假設須要開發一個電商平臺,咱們把平臺按功能拆分紅多個子系統,子系統之間以微服務形式進行交互調用。拆分後的子系統大體以下:web
- 產品系統(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
- 給模型賦予行爲
- 深度面向對象編程
<html> <img src="https://files.cnblogs.com/files/huangzelin/%E7%9B%AE%E5%BD%95%E7%BB%93%E6%9E%84.gif"/> </html>api
/// <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 decimal CalculateDiscount() { 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) <br/> ==Create()方法用來建立新訂單,訂單的建立是一個複雜的裝配過程,這個方法能夠封裝這些複雜過程,從而下降調用端的調用複雜度。==
- public void AddLine(int skuId, int qty) <br/> ==AddLine()方法用於將用戶購買的商品添加到訂單中,該方法中用戶只須要傳遞購買的商品Id和購買數量便可。至於商品的具體信息,好比名稱、規格、價格等信息,咱們將會在方法中調用產品接口實時去查詢。這裏涉及到和產品系統的交互,咱們定義了一個ServiceProxy類,專門用來封裝調用其餘系統的交互細節。==
- public void CalculateFee() <br/> ==CalculateFee()方法用於計算訂單的各類費用,如商品總價、運費、優惠等。==
- public void Pay(decimal money) <br/> ==Pay()方法用於接收交易系統在用戶支付完畢後的調用,由於在上文中咱們說到訂單系統和交易系統是兩個單獨的系統,他們是經過webapi接口調用進行交互的。訂單系統如何知道某個訂單支付了多少錢,就得依賴於交易系統的調用傳遞交易數據了,由於訂單系統自己不負責處理用戶的交易。==
<br/>數據結構
/// <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; } }
邏輯驗證
上面代碼的邏輯是否與咱們預期的一致,該如何驗證?這裏咱們經過單元測試的方式來進行校驗,且看咱們是如何測試的吧。<br/>數據庫設計
[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); } }
本文地址:https://www.cnblogs.com/huangzelin/p/9861439.html ,轉載請申明出處。
<html> <img src="https://files.cnblogs.com/files/huangzelin/%E5%88%9B%E5%BB%BA%E8%AE%A2%E5%8D%95%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95.gif"/> </html>
結語
到這裏,不知道你們注意沒有,上面的編碼過程咱們沒有提到任何的數據庫設計與存儲之類的問題。咱們一心都在奔着分析業務,設計模型和實現業務處理邏輯來編碼,DDD的設計上有個原則叫忘掉數據庫。<br/> 在我看來咱們的大多數應用程序的運行過程是這樣的:
- 接收用戶輸入
- 程序內存組裝業務對象
- 將對象持久化到存儲設備(數據庫等)
固然還有另一種是:
- 接收用戶輸入
- 從持久化設備讀取數據(數據庫等)
- 程序根據讀取的數據內存組裝業務對象
- 將對象返回調用端
==從上面的分析來看內存中領域對象組裝過程是最核心的,因其業務變幻無窮,無法用代碼作到通用處理。而數據的持久化相對來講沒啥具體業務邏輯,代碼上的通用也比較容易。因此,咱們能夠說DDD方式編程的項目,領域模型設計的合理就意味着這個項目已經成功大半了。== <br/><br/>
最後,感謝各位看官聽我嘮叨了這麼久,有問題請給我留言。謝謝
<html> 查看源碼請移步到:<a href="https://github.com/hzl091/NewSale"><b>https://github.com/hzl091/NewSale</b></a> <br/><br/> <table> <tr> <td><b>支付寶打賞</b></td> <td><b>微信打賞</b></td> </tr> <tr> <td><img src="https://files.cnblogs.com/files/huangzelin/zhifubao.bmp" style="heigh:200px;height:200px"/></td> <td><img src="https://files.cnblogs.com/files/huangzelin/weixin.bmp" style="heigh:200px;height:200px"/></td> </tr> </table> </html>