1.概念
- DDD : Domain Drive Design 领域驱动设计
- MVC : model view controller 模型视图控制
- OPP:Procedure Oriented Programming 面向过程编程
- AOP:Object Oriented Programming 面向切面编程
- OOP:Aspect Oriented Programming 面向对象编程 DDD只是一种方法论,软件设计的一种思想。没有一个稳定的技术框架。要求领域跟技术无关,跟存储无关,跟通讯无关。是绝对高内聚低耦合的思想。
2.对比
单体模式 vs MVC模式 vs DDD领域模型
2.1传统的MVC架构
- 传统开发都是以数据库为核心出发,自下而上,从DB-> dao -> service -> controller
- 随着业务的不断增大POJO会越来越多信息,单从字段无法分辨出属于哪个业务,业务与数据是割裂的,属于贫血模型,会导致贫血失忆症。
- 如下图,商品表既有基本信息,又有价格和库存,但是你完全不知道具体的业务是什么逻辑。
- 到导致大泥团问题,dao和pojo越来越大,不利于微服务的拆分。即使在service拆开了,但是dao和db还是一起。都是假拆分。
2.2DDD架构
-
自上而下,通过领域倒推数据库的设计。
-
在entity里面,会加放入对应的业务行为或方法,属于充血模型
-
如下图,在充血模型下,使用entity描述,并且会加入对应的业务方法。
-
应用层只能通过domain service 访问 不同的领域entity
-
通过如果需要组合多个信息,也是通过domain service 组合再返回
-
领域优先原则,细分不同领域,不同领域之间是互相隔离,不能直接通讯,需要通过mq或者接口访问,实际开发中可以使用实践驱动实现通讯。
-
领域层是一个沙盒,所有领域收保护的,需要通过domain service调用访问。
3为何要领域驱动设计?
简化数据存储 领域驱动设计有很多原因,谈到我为啥要在公司推行领域驱动设计,说起来还是很好玩的,因为原来基于数据驱动的开发方式,也就是传统的多层开发架构,大家定义了一堆DAL来操作数据, 在.Net大家一般有两种使用方式,一种是用ORM像Entity Framework, 另一种想使用Dapper这样轻量级的Mapping工具,这些都要把关系型数据转换为对象。结果导致以下几种结果。
没有正确的使用ORM, 导致数据加载过多,导致系统性能很差。 为了解决性能问题,就不加载一些导航属性,但是却把DB Entity返回上层,这样对象的一些属性为空,上层使用这个数据时根本不知道什么时间这个属性是有值的,这个是很丑陋的是不是? 如是又开始使用一些轻量级的数据方法,比如使用Dapper然后自己写SQL语句,这本来是很不错的方式,但是大部分人的SQL能力实在不敢恭维,大部分写出来的SQL语句,甚至比EnityFramework生成的语句还差。 所以,我就想我们做项目,大部分处理的应该是业务,如何让程序员从数据存储,模型转换的大泥潭里解放出来,领域驱动设计就进入了我的视线,当然光从数据这个角度还不足以选择领域驱动设计,用一个NoSQL数据库是不是就解决了? 但是NoSQL也有一些问题,比如MongoDB如何更优雅的保证事务以及数据的一致性等。
更多了解上下文 我们很多软件的问题,大家都知道是需求的问题,也就是客户的需求我们很难理解准确,导致程序员更加关注"HOW" 而忽略了"WHAT", 最终做了几个礼拜甚至更长时间,结果客户会说:"What?! I told you", 但是客户告诉我的,我们理解是不一样的。比如客户说:“ Great job, I love you!” 这个Love肯定不是男女之间的Love, 我们拿到的是一个客户的需求,他的上下文是什么? 比如说:“这个球打的好”, 如果是在打篮球,肯定说的事篮球,如果是在打乒乓球肯定说的是乒乓球。 而领域驱动设计里我们可以让业务人员更多的参与系统,更早的参与系统。
统一语言(Ubiquitous Language) 业务人员和我们使用一样的语言,我们的程序比如让业务尽量集中在领域里,比如在传统的数据驱动里,如果说Jack爱Rose, 我们一般会这么写
UserService.Love(Jack, Rose) 但是我们业务人员很奇怪谁Love谁? 为什么要UserService?, 如果我们写成下面这样
Jack.Love(Rose) 还有如果我们用
Company.hire(employee) 来代替
companyservice.hire(company,employee) 这样我们就更容易让业务人员参与进来,而且代码可以更易于表示真实的业务场景。
4.领域模型
前言 领域驱动设计里有很多东西,我们可以应用在各种各样的开发模式里,所以接下来说的一些东西,我们可以部分使用。
说道领域驱动的领域,大家肯定就要开始说Bounded Context,聚合,聚合根,容易让大家搞糊涂。 我觉得先抛开这些概念,后面再来说如何设计聚合,先简单来说。
模型 过去,我们在多层设计里定义了很多Model, 数据库的Model(DB Entity), 然后为了不依赖数据库,我们有设计了业务的Domain Model, 同时我们又设计了ViewModel, 这样一般也没什么问题,职责也很清晰。但是有几个问题
我们要做很多的模型转换,转入转出。当然我们可以用AutoMapper来但是AutoMapper的性能实在难以恭维,大家可以在网上搜索AutoMapper performance. 领域模型成了一个单纯的DTO了。 领域模型 首先我们要看领域,就是我们尽量把业务聚合到一个领域里,比如我们要做一个功能,可以看到用户每一次的登录日志,那个这个登录日志其实就是属于用户这个领域里。
其次我们看模型,原来我们的模型都是只有属性,也就是贫血模型,贫血的意思就是没有行为,像木乃伊一样,但是实际上领域是我们要完成业务的最主要的地方,我们希望领域能够自制,也就是领域自己管理自己。
示例 比如有一个Employee, 他的状态有Active, Pending, DeActive, 业务上是Pending只能改为Active.
public class Employee : Entity
{
public Name Name { get; set; }
public EmployeeStatus EmployeeStatus { get; set; }
}
如果是贫血的Employee模型,我们往往代码如下
public class EmployeeService : IEmployeeService
{
private readonly IUnitOfWorkFactory _unitOfWorkFactory;
private readonly IEmployeeRepository _employeeRepository;
public EmployeeService(IUnitOfWorkFactory unitOfWorkFactory, IEmployeeRepository employeeRepository)
{
_unitOfWorkFactory = unitOfWorkFactory;
_employeeRepository = employeeRepository;
}
public void ChangeStatus(EmployeeStatus status, Guid employeeId)
{
using (var unitOfWork = _unitOfWorkFactory.GetCurrentUnitOfWork())
{
var employee = _employeeRepository.GetById(employeeId);
employee.EmployeeStatus = status;
unitOfWork.Commit();
}
}
}
但是上面的代码的问题就是领域没有自治,本来修改我的状态是我的事,你能不能修改,外面随意修改我的状态是很危险的,比如Pending状态只能改为Active状态。 所以如果不是贫血的模型,我们代码就会这样,让领域自己来管理
public class Employee : Entity
{
public UserId UserId { get; private set; }
public EmployeeStatus EmployeeStatus { get; private set; }
public void ChangeStatus(EmployeeStatus status)
{
if (this.EmployeeStatus == EmployeeStatus.Pending && status != EmployeeStatus.Active)
{
throw new Exception("Only can Active when status is pending");
}
this.EmployeeStatus = status;
}
}
public class EmployeeService : IEmployeeService
{
private readonly IUnitOfWorkFactory _unitOfWorkFactory;
private readonly IEmployeeRepository _employeeRepository;
public EmployeeService(IUnitOfWorkFactory unitOfWorkFactory, IEmployeeRepository employeeRepository)
{
_unitOfWorkFactory = unitOfWorkFactory;
_employeeRepository = employeeRepository;
}
public void ChangeStatus(EmployeeStatus status, Guid employeeId)
{
using (var unitOfWork = _unitOfWorkFactory.GetCurrentUnitOfWork())
{
var employee = _employeeRepository.GetById(employeeId);
employee.ChangeStatus(status);
unitOfWork.Commit();
}
}
}
因此可以看出,我们把业务代码尽量写在领域里让领域自治。
后记 其实领域驱动设计最难的就是设计领域(Domain), 也就是后面会说到的AggregateRoot 聚合,但是我想我们先让领域不再贫血,这样在传统的多层设计,数据驱动等架构都可以使用这种模式。