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简明入门之道 - 开篇的更多相关文章

  1. Web jquery表格组件 JQGrid 的使用 - 从入门到精通 开篇及索引

    因为内容比较多,所以每篇讲解一些内容,最后会放出全部代码,可以参考.操作中总会遇到各式各样的问题,个人对部分问题的研究在最后一篇 问题研究 里.欢迎大家探讨学习. 代码都经过个人测试,但仍可能有各种未 ...

  2. Jenkins简明入门(三) -- Blue Ocean,让一切变得简单

    我们在上一节Jenkins简明入门(二) 中见识到了Jenkins能做些什么:利用Jenkins完成python程序的build.test.deployment. 同时,也有一种简单的方法,不需要写J ...

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

    OsharpNS轻量级.net core快速开发框架简明入门教程 教程目录 从零开始启动Osharp 1.1. 使用OsharpNS项目模板创建项目 1.2. 配置数据库连接串并启动项目 1.3. O ...

  4. OsharpNS轻量级.net core快速开发框架简明入门教程-从零开始启动Osharp

    OsharpNS轻量级.net core快速开发框架简明入门教程 教程目录 从零开始启动Osharp 1.1. 使用OsharpNS项目模板创建项目 1.2. 配置数据库连接串并启动项目 1.3. O ...

  5. OsharpNS轻量级.net core快速开发框架简明入门教程-代码生成器的使用

    OsharpNS轻量级.net core快速开发框架简明入门教程 教程目录 从零开始启动Osharp 1.1. 使用OsharpNS项目模板创建项目 1.2. 配置数据库连接串并启动项目 1.3. O ...

  6. OsharpNS轻量级.net core快速开发框架简明入门教程-基于Osharp实现自己的业务功能

    OsharpNS轻量级.net core快速开发框架简明入门教程 教程目录 从零开始启动Osharp 1.1. 使用OsharpNS项目模板创建项目 1.2. 配置数据库连接串并启动项目 1.3. O ...

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

    OsharpNS轻量级.net core快速开发框架简明入门教程 教程目录 从零开始启动Osharp 1.1. 使用OsharpNS项目模板创建项目 1.2. 配置数据库连接串并启动项目 1.3. O ...

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

    OsharpNS轻量级.net core快速开发框架简明入门教程 教程目录 从零开始启动Osharp 1.1. 使用OsharpNS项目模板创建项目 1.2. 配置数据库连接串并启动项目 1.3. O ...

  9. OsharpNS轻量级.net core快速开发框架简明入门教程-切换数据库(从SqlServer改为MySql)

    OsharpNS轻量级.net core快速开发框架简明入门教程 教程目录 从零开始启动Osharp 1.1. 使用OsharpNS项目模板创建项目 1.2. 配置数据库连接串并启动项目 1.3. O ...

随机推荐

  1. tomcat 部署swagger 请求到后端乱码

    问题: @ApiOperation(value = "", notes = "查看关键词列表") @ResponseBody @RequestMapping(v ...

  2. JavaScript学习-1

    本章目录: --------①数据类型. --------②定义变量. --------③类型转换. --------④运算符. --------⑤比较符. --------⑥if语句. ------ ...

  3. python批量处理文件夹中文件的问题

    用os模块读取文件夹中文件 原来的代码: import osfrom scipy.misc import imread filenames=os.listdir(r'./unprocess')for ...

  4. [Ting's笔记Day6]活用套件carrierwave gem:(1)在Rails实现图片上传功能

    carrierwave是一款经典的图片上传套件,本篇的目标是先在本地端(development)的rails项目试成功gem. (预计中集的进度会练习怎么利用Amazone S3架设图片上传Host, ...

  5. 绑定checkedComboBox

    using System; namespace CommonLib{ /// <summary> /// CommonCode 的摘要说明. /// </summary> [S ...

  6. PhoenixFD插件流体模拟——UI布局【Simulation】详解

    前言 之前使用RealFlow做流体模拟,但是总得和3ds导来导去,略显麻烦,特意学习PhoenixFD插件,直接在3ds中进行流体模拟.若读者有更好的流体模拟方法,欢迎在评论区交流. 原文地址:ht ...

  7. hmtl div水平、垂直居中

    最近写网页经常需要将div在屏幕中居中显示,遂记录下几个常用的方法,都比较简单.水平居中直接加上<center>标签即可,或者设置margin:auto;当然也可以用下面的方法 下面说两种 ...

  8. 【python深入】collections-Counter使用总结

    关于collections的使用,首先介绍:Counter的使用 需要执行:from collections import Counter 在很多使用到dict和次数的场景下,Python中用Coun ...

  9. python note 05 字典及其操作

    1. '''#数据类型划分:可变数据类型,不可变数据类型不可变数据类型:元组,bool int str 可哈希可变数据类型:list,dict set 不可哈希dict key 必须是不可变数据类型, ...

  10. Tomcat的三种部署方式

    Tomcat是目前web开发中非常流行的Web 服务器,也就是tomcat在部署项目的时候,必须要把应用程序中所用到的jar包放到tomcat的lib目录下,然后再一起部署到服务器上. 那么tomca ...