1. 程式人生 > >DDD簡明入門之道 - 開篇

DDD簡明入門之道 - 開篇

金額 rod 分享 except 賦值 業務需求 蘋果 image disco

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簡明入門之道 - 開篇