• 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

    支付宝打赏 微信打赏
  • 相关阅读:
    luogu P2639 [USACO09OCT]Bessie的体重问题Bessie's We…
    1001. 害死人不偿命的(3n+1)猜想 (15)
    1003. 我要通过!(20)
    1002. 写出这个数 (20)
    《C语言程序设计(第四版)》阅读心得(一)
    1006. 换个格式输出整数 (15)
    背包问题之多重背包
    背包问题之完全背包
    背包问题之0-1背包
    动态规划例题
  • 原文地址:https://www.cnblogs.com/huangzelin/p/9861439.html
Copyright © 2020-2023  润新知