DDD分战术模式(架构设计/模型设计)与战略模式(业务分析)
读后感的范围是战术模式 (涉及 聚合根,实体,值对象, 领域服务, 工厂, 资源库)以及涉及的一些知识
① 知道必须按照领域知识分析建模, 而不是表建模
② 建模后按照 战术模式review模型 并设计好编
初步建模的方法论, DDD上是不完善的, 但是在初步建模之后, 分析/调整模型 到最后编码落地, 这块DDD是很不错的
//一个对象的基本构成
//8个基本类型 + String + 另外一个对象
Customer {
String name;
int
age;
Address address;
}
1 值对象
在讲实体之前, 先讲讲值对象:
① 值对象只是一个值,类似(int a = 3)中的『3』,只不过是用对象来表示。值对象虽然是只读的,是一个完整的不可分割的整体,但是可以被整个替换掉:类似(a = 4)把a的值由『3』替换为为『4』,
eg: java基本类型 long int short byte boolean, char, double , float 以及string
那么在这里断言, java的基本类型(包含装箱) + string 是值对象, 值对象是可读不可变的(值变了就是替换)
② 某些实际的对象也是值对象, 例如 customer关联了一个address(不被外界关联)
eg: 当修改 Customer 的 Address 对象引用时,不是通过 Customer.Address.Street 这样的方式来修改属性,可以这样做:Customer.Address = new Address(…)
总结:
- 值对象没有唯一标识,这是它和实体的最大不同。值对象在判断是否是同一个对象时是通过它们的所有属性是否相同,如果相同则认为是同一个值对象。在区分是否是同一个实体时,只看实体的唯一标识是否相同,而不管实体的属性是否相同。
- 值对象是不可变的,即所有属性都是只读的,所以可以被安全的共享。
2 实体
在一个聚合根中, 所有的属性要么是值对象(基本类型加不变对象), 要么是实体
那么实体与值对象有什么不同的了:
- 实体就是领域中需要唯一标识的领域概念。因为我们有时需要区分是哪个实体:有两个实体,如果唯一标识不一样,那么即便实体的其他所有属性都一样,也认为他们是两个不同的实体。
- 不应该给实体定义太多的属性或行为,而应该寻找关联,将属性或行为转移到其他关联的实体或值对象上。比如:Customer 实体,有一些地址信息,由于地址信息是一个完整的有业务含义的概念,所以我们可以定义一个 Address 对象,然后把 Customer 的地址相关的信息转移到 Address 对象上。如果没有 Address 对象,而把这些地址信息直接放在 Customer 对象上,然后对于一些其他的类似Address的信息也都直接放在Customer 上,会导致 Customer 对象很混乱,结构不清晰,最终导致它难以维护和理解。
3 聚合根
1 充血模型
我们现在用的SSH or SSM 其实是面向过程的, 我们使用的模型都是贫血模型, 只有属性, 没有行为
例如:
保存顾客的例子
public
class
Customer {
private
int
id;
private
int
age;
private
string name;
private
String address;
///下面一大坨getter setter
// 米有任何业务逻辑
}
public
class
CustomerService {
private
CustomerMapper customerMapper;
public
Customer save(Customer customer) {
return
customerMapper.save(customer);
}
}
public
class
CustomerController {
private
CustomerService customerService;
//处理方式
@ReuqestMapper
(method=POST)
public
Customer save(Customer customer) {
return
customerService.save(customer);
}
}
controller → service → mapper
真真的模型应该是充血的, 同时具有属性与行为:
//修改名字的例子
public
class
Customer
extends
Model{
private
int
id;
private
int
age;
private
string name;
private
String address;
///下面一大坨getter setter
//具有具体的修改的行为
public
Customer changeName(String newName) {
customer.name = name;
customer.save();
return
customer;
}
}
public
class
CustomerController {
@ReuqestMapper
(method=PUT )
public
Customer changeName(String name) {
Customer customer = Customer.findById(id);
//不考虑null
customer.changeName(newName);
return
customer;
}
}
controller 直接调用模型
面向过程感觉是 模型都有我们来操纵
面向对象感觉是 模型的行为都是自发的
从维护性上来说, 面向对象显然更好理解, 而且砍掉了SPRING体系下产生一套没啥用的代码, 代码更加聚焦。
2 划分领域
public class Order extends Model { //关联ordersItems与customer private Customer customer; private List<OrderItem> items; public Customer getCustomer() { return this .customer; } } public class OrderController { public Customer findCustomerByOrder( int orderId) { Order order = Order.findById(orderId) return order.getCustomer() } } 有一个疑问就是充血模型会关联外部对象, 那么在一个系统中如此关联下去结果可能如下 模型直接互相关联最终会产生一个巨大的蜘蛛网, 那么这样的代码耦合性太大了! 这个时候DDD出现了, 他要求我们从领域划分对象,落地到代码是这样的 public class Order extends Model{ //继续关联ordersItems private List<OrderItem> items; //此处关联一个外部顾客领域的id private long customerId; public long getCustomerId() { return this .customerId; } } public class OrderController { public Custoemr findCustomerByOrder( int orderId) { Order order = Order.findById(orderId) return Customer.findById(order.getCustomerId()); } } 这个时候你们肯定有疑问, 为什么items还是保留在Order里面 我们看下聚合根的定义 聚合定义了一组具有内聚关系的相关对象的集合,以及对象之间清晰的所属关系和边界,避免了错综复杂的难以维护的对象关系网的形成。我们把聚合看作是一个修改数据的单元。 聚合有以下特点:
- 每个聚合有一个根和一个边界:根是聚合内的某个实体;边界定义了一个聚合内部有哪些实体或值对象;
- 聚合根是外部可以保持对聚合引用的唯一元素,负责与外部其他对象打交道并维护自己内部的业务规则。聚合内部的对象之间可以相互引用,但是聚合外部如果要访问聚合内部的对象时,必须通过聚合根开始导航,绝对不能绕过聚合根直接访问聚合内的对象;
- 聚合内除根以外的其他实体的唯一标识都是本地标识,也就是只要在聚合内部保持唯一即可,因为它们总是从属于这个聚合的;
- 聚合内部的对象可以保持对其他聚合根的引用;
- 删除一个聚合根时必须同时删除该聚合内的所有相关对象,因为他们都同属于一个聚合,是一个完整的概念;
- 基于聚合的以上概念,我们可以推论出从数据库查询时的单元也是以聚合为一个单元,不能直接查询聚合内部的某个非根的对象;
另外在DDD中有一条原则:一个业务用例对应一个事务,一个事务对应一个聚合根,也即在一次事务中,只能对一个聚合根进行操作 customer与order是两个领域的东西, 所以order保留对customer域的id引用
order与orderitems 是一个领域的东西, 所以保存orderitems的实体
那如何划分出聚合根了? 简单的方法: 生命周期是否一致(一起诞生, 一起消亡) 另外聚合根要求
- 写操作必须保证一个事务内解决
- 读操作必须保证把一个聚合根内部对象全部加载出来
- 访问聚合根内部的某对象(例如orderitems), 比如用根对象做(order)入口, 不能单独直接访问
- 生命周期一致
你可以看到orderItems 是符合这几点要求的 4 领域服务 每个领域模型负责自己领域的行为, 比如用户改名, 比如查看订单下订单子项个数
public class Order extends Model { private List<OrderItems> items; public int countItems() { return countItems.size(); } } 那问题来了, 如果两个领域发生的复杂的关系, 该怎么办了? eg: 订单生成后发个短信给用户 这个时候就要使用领域服务了
public class OrderService { //实际逻辑可能不是这样,举个栗子而已 public Order createOrder(Order order) { order = order.save(); long customerId = order.getCustomerId(); Customer customer = Customer.findById(customerId); SMS.send(String.format(SMS.CREATE_ORDER_TEMPLATE, customer.mobile)); return order; } } |
看下领域服务的定义:
- 领域中的一些概念不太适合建模为对象(实体对象或值对象),因为它们本质上就是一些操作、动作,而不是事物。这些操作往往需要协调多个领域对象。如果强行将这些操作职责分配给任何一个对象,则被分配的对象就是承担一些不该承担的职责,从而会导致对象的职责不明确很混乱。DDD认为领域服务模式是一个很自然的范式用来对应这种跨多个对象的操作。一般的领域对象都是有状态和行为的,而领域服务没有状态只有行为。
- 领域服务还有一个很重要的功能就是可以避免领域逻辑泄露到应用层。因为如果没有领域服务,那么应用层会直接调用领域对象完成本该是属于领域服务该做的操作,需要了解每个领域对象的业务功能,以及它可能会与哪些其他领域对象交互等一系列领域知识。这样一来,领域层可能会把一部分领域知识泄露到应用层。对于应用层来说,通过调用领域服务提供的简单易懂且意义明确的接口肯定也要比直接操纵领域对象容易的多。
5 工厂 一个对象的生命周期如下 在建模时候, 我们需要思考一个对象的生命周期如何 ERP系统商户在入驻时诞生, 暂无消亡 在看看工厂的定义
- DDD中的工厂也是一种体现封装思想的模式。DDD中引入工厂模式的原因是:有时创建一个领域对象是一件比较复杂的事情,不仅仅是简单的new操作。工厂是用来封装创建一个复杂对象尤其是聚合时所需的知识,将创建对象的细节(如何实例化对象,然后做哪些初始化操作)隐藏起来。
- 客户传递给工厂一些简单的参数,如果参数符合业务规则,则工厂可以在内部创建出一个相应的领域对象返回给客户;但是如果参数无效,应该抛出异常,以确保不会创建出一个错误的对象。当然也并不总是需要通过工厂来创建对象,事实上大部分情况下领域对象的创建都不会太复杂,只需要简单的使用构造函数就可以了。隐藏创建对象的好处:可以不让领域层的业务逻辑泄露到应用层,同时也减轻了应用层的负担,它只需要简单的调用领域工厂创建出期望的对象即可。
6 仓库 大家可以理解成仓库就是我们平时写mapper 或者我上面写的例子里面继承的model,里面默认带了一个仓库 另外一个聚合与只有一个仓库, 那么上面的order 与order items 虽然是存储两张表, 但是实际上只有一个仓库去读取 定义:
- 仓储被设计出来的原因:领域模型中的对象自从创建后不会一直留在内存活动,当它不活动时会被持久化到DB中,当需要的时候会重建该对象。所以,重建对象是一个和DB打交道的过程,需要提供一种机制,提供类似集合的接口来帮助我们管理对象。
- 仓储里存放的对象一定是聚合,因为之前提到的领域模型是以聚合的概念来划分边界的。我们只对聚合设计仓储,把整个聚合看成一个整体,要么一起取出来,要么一起被删除,不会单独对某个聚合内的子对象进行单独查询和更新。仓储还有一个重要的特征就是分为仓储定义部分和仓储实现部分,在领域模型中定义仓储的接口,而在基础设施层实现具体的仓储。
三 其余知识 1 DDD分层
- 用户界面/展示层:1)请求应用层获取用户所需的展示数据;2)发送命令给应用层执行用户的命令
- 应用层:薄薄的一层,定义软件要完成的任务。对外为展示层提供各种应用功能,对内调用领域层(领域对象或领域服务)完成各种业务逻辑。应用层不包含业务逻辑
- 领域层:表达业务概念、业务状态信息及业务规则,是业务软件的核心
- 基础设施层:为其他层提供通用的技术能力,提供了层间通信;为领域层提供持久化机制。
下层不可反向调用上层(只能发送消息), 上层可以跨层调用
|
3 DDD建模过程
- 根据需求建立初步的领域模型,识别明显的领域概念和之间的关联(1:1, 1:n的关系),用文字精确没有歧义的描述出每个领域概念的含义;
- 分析主要的软件功能,识别主要的应用层的类,这样有助于及早发现哪些是应用层的职责,哪些是领域层的职责;
- 进一步分析领域模型,识别出实体、值对象、领域服务;
- 分析关联,通过对业务的深入分析和软件设计原则及性能方面的权衡,明确关联的方向,去掉一些不需要的关联;
- 找出聚合边界及聚合根,在分析过程中会出现难以清洗判断的选择问题,这就依赖平时分析经验的积累了;
- 为聚合根配置仓储,一般情况下为一个聚合分配一个仓储,此时设计好仓储的接口即可;
- 遍历所有场景,确定设计的领域模型能有效解决业务需求;
- 考虑如何创建实体和值对象,是通过工厂还是构造函数;
- 重构模型,寻找模型中有疑问或蹩脚的地方,比如思考:聚合的设计是否正确,模型的性能等等;
领域建模是一个不断重构,持续完善的过程,大家会在讨论中将变化的部分反映到模型中,从而模型不断细化并朝正确的方向走。 4 关联的设计 关联在领域建模的过程中非常重要,关联的设计可以遵循如下的一些原则:
- 关联尽量少。对象之间复杂的关联容易形成对象的关系网,对于理解和维护单个对象很不利,同时也很难划分对象与对象之间的边界;另外,减少关联有助于简化对象之间的遍历;
- 关联尽量保持单向的关联;
- 在建立关联时,需要挖掘是否存在关联的限制条件。如果存在,那么最好把限制条件加到关联上,往往这样的限制条件能将关联化繁为简,即将多对多简化为1对多,或将1对多简化为1对1;
参考:https://wiki.sankuai.com/pages/viewpage.action?pageId=685272586#DDD%E6%88%98%E6%9C%AF%E6%A8%A1%E5%BC%8F-3%E8%81%9A%E5%90%88%E6%A0%B9 |