DDD簡明入門之道 - 開篇

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>

相關文章
相關標籤/搜索