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 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)
==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);
}
}
本文地址:https://www.cnblogs.com/huangzelin/p/9861439.html ,轉載請申明出處。
結語
到這裏,不知道大家註意沒有,上面的編碼過程我們沒有提到任何的數據庫設計與存儲之類的問題。我們一心都在奔著分析業務,設計模型和實現業務處理邏輯來編碼,DDD的設計上有個原則叫忘掉數據庫。
在我看來我們的大多數應用程序的運行過程是這樣的:
- 接收用戶輸入
- 程序內存組裝業務對象
- 將對象持久化到存儲設備(數據庫等)
當然還有另外一種是:
- 接收用戶輸入
- 從持久化設備讀取數據(數據庫等)
- 程序根據讀取的數據內存組裝業務對象
- 將對象返回調用端
==從上面的分析來看內存中領域對象組裝過程是最核心的,因其業務千變萬化,沒法用代碼做到通用處理。而數據的持久化相對來說沒啥具體業務邏輯,代碼上的通用也比較容易。所以,我們可以說DDD方式編程的項目,領域模型設計的合理就意味著這個項目已經成功大半了。==
最後,感謝各位看官聽我嘮叨了這麽久,有問題請給我留言。謝謝
查看源碼請移步到:https://github.com/hzl091/NewSale支付寶打賞 | 微信打賞 |
DDD簡明入門之道 - 開篇