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

支付宝打赏 微信打赏

原文地址:https://www.cnblogs.com/huangzelin/p/9861439.html

时间: 2024-10-09 08:45:38

DDD简明入门之道 - 开篇的相关文章

有趣的机器学习:最简明入门指南

有趣的机器学习:最简明入门指南 首页 最新文章 IT 职场 前端 后端 移动端 数据库 运维 其他技术 - 导航条 - 首页 最新文章 IT 职场 前端 - JavaScript - HTML5 - CSS 后端 - Python - Java - C/C++ - PHP - .NET - Ruby - Go 移动端 - Android - iOS 数据库 运维 - Linux - UNIX 其他技术 - Git - 机器学习 - 算法 - 测试 - 信息安全 - Vim 伯乐在线 > 首页 >

OsharpNS轻量级.net core快速开发框架简明入门教程-Osharp.Hangfire使用

OsharpNS轻量级.net core快速开发框架简明入门教程 教程目录 从零开始启动Osharp 1.1. 使用OsharpNS项目模板创建项目 1.2. 配置数据库连接串并启动项目 1.3. OsharpNS.Swagger使用实例(登录和授权) 1.4. Angular6的前端项目启动 Osharp代码生成器的使用 2.1 生成器的使用 2.2 生成代码详解(如何自己实现业务功能) Osharp部分模块使用 3.1 Osharp.Redis使用 3.2 Osharp.Hangfire使用

Ruby语言简明入门与提高

rails请参考我写的简明Ruby系列:rails简明实用入门指南 本书其他版本: github版本 简书版本 作者:chenge 联系方式: * 邮件:chenge3k at qq.com * chenge微博谈 第一章 简介 内容简介 目标读者: 初次编程者 初次学习Ruby的程序员 Ruby是适合于初学者的,即使是从未写过程序的人来学,我相信初级部分一天就能学完. 为了帮助你学习更轻松些,我尽量选择了简单的内容. 通过浏览目录,就知道我选择了一些什么内容.初级我选择了三组共四个概念,本书定

Java超简明入门学习笔记(零)

Java编程思想第4版学习笔记(零) 前言   这个笔记本主要记录了我在学习Java编程思想(第4版,中文版)的过程中遇到的重难点及其分析.主要参考了C++11版本的C++语言,对比了它们不同的部分. <Java编程思想(第四版)>早在2007年就已经出版了,时值Java SE5~Java SE6升级的时间节点,现在10年过去了,Java语法标准已经到了Java 8,Java 9也快出来了,不过这本书仍然充满对Java探索的智慧,书上所讲的语法也绝大多数没有失效 ,是很方便读者全面系统了解Ja

discuz简明入门教程

一.整个站点的基本结构: 1.首先,介绍网站最主要的三个文件夹:root\source\.root\template\.root\static\,分别是代码文件夹.模版文件夹.静态文件夹(js.css.image等). 2.其次,介绍一些基本的控制文件. 入口文件[ root\xxx.PHP ],包括forum.php.portal.php.home.php等,作用是控制页面的转向,每一个入口文件中都包含了内核(core_class).方法(function_xxx).模块(module)三个最

Java入门到精通——开篇

本系列博客大体框架构思了一段时间了,本系列博客包含了对现有知识的总结也有对未来知识的展望. 本系列博客包括七大部分如下: 第一部分  Java基础应用 讲述JAVA的基础从以下几方面讲述:安装与配置.基本语法.面向对象.异常处理.数组.常用类.容器.IO.线程.网络等. 第二部分  Java高级应用 讲述Jvm优化.J2EE十三个规范等. 第三部分  Java工具应用 从三个方面讲述:开发工具.管理工具.代码质量控制工具 开发工具:Maven.Nexus.Jetty.Eclipse等. 管理工具

Jenkins简明入门(一) -- 安装

如今Jenkins官网的Guide里使用了Docker,网上很多Jenkins入门教程都已过时了,所以写这一篇入门教程. 官网的Guide Link是:https://jenkins.io/doc/pipeline/tour/getting-started/ 我写的Jenkins入门是基于官网Guide,但是有所不同:除了汉语翻译外,官网的Guide中启动Jenkins时也用了docker,然后在Jenkins里跑pipline的时候,build stage和test stage也用了docke

JavaScript 入门教程一 开篇介绍

1.JavaScript 刚开始是为了解决一些由服务器端进行的验证而开发的前端语言.在宽带还不普及的90年代,当用户辛苦输入很多信息并提交给服务器后,等了漫长的时间,等到的不是提交成功的提示而是某些必填字段没有填写的警告,于是各大浏览器公司开始开发自己的前端语言,但是由于很多公司开发了各自的JavaScript.为了有个统一的标准,才有了后来的ECMAScript. 2.ECMAScript 可以为不同种类的宿主环境提供核心的脚本编程能力,因此核心的脚本语言是与任何特定的宿主环境分开进行规定的.

Java超简明入门学习笔记(二)

Java编程思想第4版学习笔记(二) 第三章 操作符 & 第四章 控制执行流程(流程控制语句)   第三章和第四章的内容主要是讲操作符和流程控制语句,Java的大多数操作符和流程控制语句都和C/C++的十分类似,因此把这两章内容汇成一章,挑出Java独特的地方进行学习.         第三章   知识点1:P39,3.2,操作符,优先级 Java操作符和其他语言一样,作用于操作数,产生新值.各个操作符的优先级和结合性类似C/C++. 这里有一些特殊的地方: + 操作符可以用于字符串,把字符串和