举例:如果开发一个电子商务系统,首先电子商务本身是一个领域,它应该包括但不限于以下子领域:
商品管理:商品管理是电子商务的核心领域,负责处理与商品相关的所有操作,如商品的新增、删除、修改等购物车管理:购物车应该是电子商务中另一个关键领域,购物车处理的是与用户选购商品相关的所有操作。订单管理:订单处理涉及到与订单相关的所有操作,如订单生成、支付、发货、收货等。支付: 支付是电子商务的一个支撑域。用户管理:用户的登陆登出、资料的新增和修改等上下文环境,保证在领域之内的一些术语、业务相关对象等有一个确切的含义,没有二义性。我们将限界上下文内的领域模型映射到微服务,就完成了从问题域到软件的解决方案业务概念、角色和行为定义系统边界的概念其主要工作就是将大的领域分解成高内聚低耦合的子域,找出最理想的微服务划分策略实体、值对象、领域服务和事件。业务功能的划分、业务模型的聚合或者系统的职责界限。职责,并针对这些职责来描述其对外提供的接口和行为。是否能有效的封装出业务领域的逻辑,不足则继续调整。交互关系,例如共享内核、客户/供应者和防腐层等。通俗点理解就是多个服务之间的交互方式,或者多个团队、多个业务之间的交互方式。
在这种关系中,上游的变化会影响到下游,但下游的改变不会影响到上游。实质上这是一种单向依赖关系。
要求:
特点:
开发与部署的依赖关系:
适用性:
例子:
客户依赖于供应者提供的服务或数据,同时供应者要考虑客户的情况。是双向依赖。
要求:
这个接口是代之,可能是rpc接口,http访问链接,或者是上游提供的数据)以供客户使用,且在设计和实现这些接口时,需要考虑到客户的需求;需要与客户进行充分的沟通,并在修改完成后告知客户。特点:
部署和开发的依赖
适用性:
需考虑到客户的需求。两个限界上下文需要密切协作以完成共同的目标。
要求:
特点:
开发与部署的依赖关系:
适用性:
两个或更多的限界上下文共享相同的模型或代码段(内核)。
要求:
特点:
开发与部署的依赖关系:
适用性:
在这种关系中,一个限界上下文(下游)选择完全遵守另一个限界上下文(上游)的模型、规则和策略。
要求:
特点:
开发与部署的依赖关系:
适用性:
防腐层位于两个限界上下文之间,用于隔离它们的模型和交互方式,防止一个上下文的模型被另一个上下文“污染”。
要求:
将上游限界上下文的模型或数据转换为下游限界上下文可以理解的模型或数据,反之亦然。特点:
开发与部署的依赖关系:
适用性:
例子:
在这种关系中,一个限界上下文(主机)对外提供一组标准化的公开API,其他的限界上下文(客户端)可以通过这些API与主机进行交互。
要求:
特点:
开发与部署的依赖关系:
文档和支持。客户端则需要依据API文档进行开发,实现与主机的交互。适用性:
又需要保持一致性和控制权时,通常采用开放主机服务模式。发布语言(Published Language是限界上下文之间沟通的一种方式。它是一个预定义的、标准化的数据交换格式。发布语言可以是任何公认的模型或数据格式,如XML, JSON,甚至可以是特定的业务语言。
要求:
特点:
开发与部署的依赖关系:
适用性:
例子:
{
"orderId": "123",
"items": [
{
"productId": "abc",
"quantity": 2
},
{
"productId": "def",
"quantity": 1
}
]
}

共享语言是开发人员和非技术团队成员共同沟通业务概念的手段。以下是共享语言的一些要求和特点:
创建和维护一个良好的共享语言,可以有效地协助团队更深入地理解业务领域,减少沟通误解,并为创建精确的领域模型铺平道路。
同一词汇或概念,可能在不同限界上下文中有不同的含义。预订管理和客房服务。客户实体对象有着不同的职责,所以有着不同的方法。
Entity(实体):是一种具有唯一不变标识符的对象,并且通过时间和不同的表示形式保持连续性。例如,一个人(即使他们的姓名、地址等信息可能随着时间改变)或者一个帐户(即使帐户余额变动)都是实体。Value Object(值对象):是没有唯一标识符的对象,通常是来定义属性。例如,日期、颜色或者金额都可以是值对象。Aggregate(聚合):将一组实体和值对象组织在一起形成一种边界,并且规定所有外部对象只能持有聚合根的引用。聚合根是聚合中的某个实体,用来封装聚合的内部实现,对外提供统一的访问入口。Domain Service(领域服务):当一些操作既不属于实体,也不属于值对象,而且会跨越多个聚合的时候,我们通常会使用领域服务来进行这样的操作。例如,用户注册、支付等操作就可以是领域服务。领域服务通常是全局的,可以被任何实体、值对象或其他服务调用。Domain Event(领域事件):领域事件是领域中发生的重要变化,这些变化可能会影响到其他部分。例如,订单已经被支付就可以定义为一个领域事件。Repository(仓库):用于存储、检索和管理聚合或实体的集合。它可以看作是一种持久化机制,对外提供统一的存储接口。用户 (User):实体,每个用户都有唯一的标志(用户ID)来区分,其他信息更改也不影响其用户身份(如收获地址,昵称)收货地址 (Address):值对象。收货地址是由街道、城市、州/省、邮编这些属性组成的,我们通常不会直接去定位一个地址,而总是在定位到实体后再去查找其某个值对象。订单 (Order):聚合,其中包含多个订单项和一个用户,这整个聚合的聚合根就是订单本身。订单可以保障其内部所有订单项的数据一致性。库存扣减 (Inventory Deduction):领域服务,它涉及到的操作可能跨越用户、订单等多个聚合的边界。订单已支付 (Order Paid):领域事件,当用户完成支付动作,订单状态变为已支付,触发一个订单已支付的领域事件。包含足够的业务逻辑,而不仅仅是数据的容器。对象拥有唯一标识符,该对象在经历各种状态变更仍能保证标识符一致。在领域驱动设计中,实体类通常采用充血模型。即实体要包含足够的业务逻辑DDD设计中是先有领域模型,再针对业务场景构建实体对象,最后将实体对象映射与数据持久化Repository(仓库)进行数据持久化管理省、市、县和街道等属性构成了地址这个值类型。该类的一个具体实例就是一个值对象。
// 实体类
public class Person {
public String ID; // 实体类的标识字段
public String name;
public int age;
public boolean gender;
public Address address; // 值类型作为实体类的属性。
// 各种实体方法,省略
}
public class Address {
public String province;
public String city;
public String country;
public String streat;
// 值对象方法,省略。
}
属性嵌入
// 实体类
public class Person {
public String ID; // 实体类的标识字段
public String name;
public int age;
public boolean gender;
public Address address; // 值类型作为实体类的属性。
// 各种实体方法,省略
}
public class Address {
public String province;
public String city;
public String country;
public String streat;
// 值对象方法,省略。
}
序列化值对象
// 实体类
public class Person {
public String ID; // 实体类的标识字段
public String name;
public int age;
public boolean gender;
public String address; // json类型,将Address序列化后嵌入实体中。
// 各种实体方法,省略
}
public class Address {
public String province;
public String city;
public String country;
public String streat;
// 值对象方法,省略。
}
属性嵌入值对象
这种方式是直接将值对象作为实体的属性。这样可以更直观地表示实体与值对象之间的关系,并有利于在程序中理解和操作。
优点:
缺点:
序列化值对象
这种方式是将值对象序列化为一种可存储的格式(如 JSON 或者 XML),然后将其存储在实体的一个字段中。当我们需要使用该值对象时,可以再将其反序列化回对象。
优点:
灵活性高。在对值对象的结构修改时,只需要相应地调整序列化和反序列化的操作即可。缺点:
总结,属性嵌入值对象:代码容易理解且性能好;序列化值对象:存储方便且灵活性高。

在领域建模时,我们将部分对象设计为值对象,保留对象的业务含义,同时又减少了实体的数量;在数据建模时,我们可以将值对象嵌入实体,减少实体表的数量,简化数据库设计业务规则和数据的一致性保证必须在聚合内部实现,聚合的任何实体的状态变化都必须产生一致的、有效的聚合状态。外部对象唯一能够直接引用的对象。必须通过聚合根来进行,其他的实体和值对象则对外部对象不可见。(即外部对象不能直接修改聚合内部的其他对象,这个外部对象通常指的是领域服务)
订单项(非聚合根实体),领域服务不应该直接创建并添加到订单项集合中,而应该调用订单(聚合根)的一个方法(如 addOrderItem(product, quantity)),由订单聚合根来保证添加的合法性和一致性。确保聚合的内部状态的一致性和完整性,聚合根的任何方法都不应让聚合处于无效的状态。聚合根对实体和值对象采用直接对象引用的方式进行组织和协调。(所以当我们拿到聚合根实体后,那么该聚合下的所有实体和值对象也可以得到)领域服务操作聚合根,聚合根操作聚合内的其他对象com
└── mycompany
└── myproject
└── domain
├── model # 包含所有的领域模型,通常是按照业务上下文进行模块化组织
│ ├── order # "订单"业务上下文
│ │ ├── Order.java # 订单聚合根
│ │ └── OrderItem.java # 在订单聚合内的其他实体
│ └── customer # "客户"业务上下文
│ └── Customer.java # 客户聚合根
├── repository # 数据访问接口,一个聚合通常对应一个仓库
│ ├── OrderRepository.java
│ └── CustomerRepository.java
├── service # 领域服务,通常包含跨领域模型的业务逻辑
│ └── OrderService.java
└── event # 领域事件,描述领域中重要的业务事件
└── OrderCreatedEvent.java
上面给了一个通常的包目录划分规则,实际的项目可能会根据具体的业务需求和团队的编码规范来进行适当的调整。
以上的目录结构中包含了以下几个关键的部分:
model:按照业务上下文来组织的领域模型,每个业务上下文下包含了相应的聚合、实体、值对象等。
repository:数据访问接口,一般来说,一个聚合对应一个仓库。仓库用来封装领域对象的持久化细节。
service:领域服务层,包含了涉及到多个领域对象的复杂业务逻辑。
event:领域事件,描述了领域中的重要业务事件。
应用服务 ->领域服务 ->通过资源库获取聚合根
->通过资源库持久化聚合根
->发布领域事件
不直接引用另一个聚合根的对象。唯一识别符进行。
无意识地加载过多的对象,从而浪费内存和计算资源。通过ID引用,可以根据实际需要加载所需的聚合实例(通过仓库(Repository)和该唯一识别符获取到具体的聚合实例)。比如,一个"订单"聚合根(Order)可能需要引用"用户"聚合根(User),在订单聚合中,我们保存用户的ID,而不是用户对象的直接引用。当我们需要在订单中使用用户信息时,领域服务通过用户ID去用户仓库中获取实际的用户对象。
同限界上下文内的聚合之间的领域服务可直接调用。领域服务是被用来处理那些不适合放在实体或者值对象中进行的业务逻辑,或者涉及到多个聚合的业务逻辑。
这样的好处是:
保持实体和值对象的简洁和单一职责。对于某些业务逻辑无法归类到实体和值对象中。
业务需求:有"订单"(Order)和"库存"(Inventory)两个聚合。现在有一个业务需求,就是当下订单时,需要扣减库存。
简单方法:可以直接在"订单"聚合中调用"库存"聚合的扣减方法,
缺点:聚合间的耦合增加。一旦"库存"聚合的扣减逻辑发生改变,"订单"聚合也必须修改,这违反了聚合的设计原则。DDD设计方法:引入领域服务,称为OrderProcessingService。定义placeOrder方法,接收订单和商品数量作为参数,它首先调用"订单"聚合的方法生成订单,然后调用"库存"聚合的方法扣减库存。
优点:保持了"订单"和"库存"聚合的独立性和封闭性。补充一下,这里面消灭了变化吗?其实没有,原先库存逻辑发生改变,订单聚合也需要修改。采用DDD后,库存逻辑发生改变,虽然订单聚合不需要修改了,但是引入的领域服务却需要修改。所以变化没有被消灭,只是将变化转移到特定的地方,其实我们消灭不了变化,但我们能做到的事分门别类地管理变化,让变化在合适的位置,即封装变化到特定位置。
领域事件表示在业务领域内发生的重要事件,这些事件可能触发其他业务行为或在业务流程中扮演重要角色。具有以下好处:
强一致性,而是基于事件的最终一致性。事件驱动本质是将同步调用转为异步调用,由强一致性转为最终一致性目的是表示某种重要的业务状态变更,它主要被用作数据传输对象,因此领域事件对象本身通常不包含业务逻辑业务变化。描述系统中的状态改变,例如,“订单创建”、“付款成功”、“货物邮寄”等。明确的源头,以便跟踪到究竟是哪个聚合或实体发起的这个事件。这通常通过记录事件源的唯一标识符来实现。应该记录其发生的时间。这在很多情况下都是很重要的,比如进行审计、排序事件、或在某些业务规则中该事件必须在某个特定时间内被处理。假设我们正在设计一个电商系统,我们可以定义一个"订单支付成功"的领域事件。根据领域事件的要求,这个领域事件应该包含以下属性:
反映业务语义:订单支付成功是一个重要的业务事件,表示用户已经对订单进行了支付,系统需要对此做出响应。
记录状态改变:支付成功显著改变了订单的状态,使其从“未支付”变为“已支付”。
不可变性:一旦订单支付成功事件被创建,其携带的信息如支付金额、支付方式等就应该被锁定,不能更改。
有明确的事件源:订单支付成功事件的源头是具体的订单,可以用订单ID来表示。
可序列化:该事件需要被序列化成如 JSON 的格式,以便存储或在网络间传输。
包含时间戳:应该记录下订单何时支付成功,以便进行如退款等后续操作在适当的时间。
定义一个订单支付成功的领域事件可以如下:
{
"eventType": "OrderPaid",
"orderId": "123456",
"paymentMethod": "Credit Card",
"paymentAmount": "100.00",
"paidAt": "2022-01-01T10:00:00Z", // 使用ISO 8601格式的时间戳
}
系统中的其他服务如邮件通知服务,库存服务等可以监听这个OrderPaid事件,并根据自身的业务逻辑进行相应处理,如发邮件给用户,更新库存等。
微服务之间的领域事件
事件构建,发布与订阅,事件数据持久化,消息中间件以及分布式事务机制如果微服务之间不使用领域事件,则微服务之间的访问也可以采用应用服务直接调用的方式,但是这个需要引用分布式事务机制,以确保数据的强一致性。基本属性
业务属性
基本属性和业务属性一起构成了领域事件
public class DomainEvent {
public String ID;
public long timeStamp;
public String source;
public String type;
public String data;
// 领域事件一般只用作传输对象,一般不包含业务逻辑。
}

消息队列的消费者服务,其用户接口层可能的工作:
监听消息队列消息解包和预处理:比如反序列化成领域事件对象、验证消息的完整性。委托应用层处理消息接收应用层的反馈错误处理或日志记录与处理监控:如统计处理时长、打点监控、错误重试。web服务、rpc服务,其用户接口层主要可能工作
很薄的一层,理论上不应该有业务规则和逻辑。协调/编排应用的各种领域对象(实体、值对象、领域服务等);领域层、应用层以及用户界面层,为它们提供支持,使它们专注于执行业务逻辑。互相引用:在双向关联中,两个对象都拥有对方引用的权力。例如,在面向对象设计中,某个类的实例中可以包含指向另一个类实例的引用。
数据一致性:一对双向关联的实体需要处理的一个主要问题是数据的一致性。例如,当你更改一端的关联时,你必须确保另一端的关联也做出了相应的更改。否则,就可能导致数据不一致。
性能考虑:双向关联有助于提高查询效率。例如,当你拥有一个对象引用时,可以直接通过它来访问相关的其他对象,而不需要执行额外的查询。
复杂性考虑:虽然双向关联有其优点,但也增加了设计的复杂性,并可能引入循环引用的风险。因此在使用时需要谨慎。
内存管理问题:在某些情况下,双向关联可能导致对象不能被正确地垃圾回收,因为它们互相持有对方的引用,这可能会导致内存泄露。
在一个博客系统中,存在“博文”和“评论”两种实体。一个博文可以拥有多个评论,而每一个评论都对应于一个特定的博文。在这个例子中,一个“博文”实体可能会维护一个包含其所有“评论”的列表,反过来,“评论”实体也会持有其对应的“博文”的引用。这就构成了一个双向关联,即,可以从“博文”访问它的“评论”,也可以从“评论”访问其对应的“博文”。
在DDD中,关联的管理是一个非常重要的问题。设计得当,可以使得代码更清晰、更易理解;否则,可能会导致代码复杂难懂。常见有效管理和约束关联的方法:

领域服务中使用规则引擎:领域服务是没有明显的状态和行为逻辑,属于一种无状态服务,可以包含规则引擎来处理复杂的业务决策。领域服务可以接收特定的领域对象,然后应用规则引擎以决定如何处理这些对象。
在领域对象中嵌入规则:如果某些复杂的业务规则直接对应于特定的领域对象,那么这些规则可以直接写入领域对象的方法中。比如,如果有一个"用户"对象,并且存在一个规则,描述了"如果用户积分超过1000,那么用户成为VIP",那么这个规则可以直接写入"用户"对象的方法中。
使用策略模式:策略模式是一种行为设计模式,允许在运行时选择对象的行为。如果在领域模型中有多个规则可供选择,那么可以使用策略模式。每一种策略都封装了一个特定的规则,运行时可以动态选择使用哪一种策略。
规则引擎,即业务规则引擎,是一种计算机软件系统,专门用于执行业务规则。业务规则是逻辑描述,比如 “如果条件A满足,那么进行操作B”。规则引擎的目标是管理和执行这些规则。
规则引擎主要由两大部分组成:一个用于业务规则的定义和管理(比如添加、修改、删除规则)的规则管理系统,以及一个用于匹配和执行规则的规则执行系统。
规则管理:例如规则的版本管理、权限控制,以及规则的导入导出等。规则执行:规则引擎在接收到某个领域对象时,会根据定义的规则来指导这个对象应该怎么处理。处理方式可以是计算、执行动作,或将结果引导到其他流程。规则引擎有以下特点:
独立于业务流程:规则引擎使得业务逻辑可以从业务流程和应用程序代码中提取出来,独立管理。这样就可以在不改变业务流程和应用代码的情况下,灵活地修改业务逻辑。
逻辑表达能力:规则引擎通常包含一种或几种对业务逻辑进行表达的方式(如决策表、决策树、规则链、脚本等)。通过这些工具,业务人员或开发人员可以用相对直观的方式定义业务规则。
自动匹配与执行:当规则引擎接收到需要处理的数据(通常包装为一种称为"事实"的数据结构)时,它会自动匹配其业务规则库中所有与这些数据相关的规则,并对这些匹配的规则进行执行。
万能适用性:规则引擎不仅可以应用于业务系统,也可以用于各种需要决策逻辑的场景,如推荐系统、风险控制系统等。
通过使用规则引擎,企业能更方便地管理和调整其业务逻辑,提高业务处理效率,同时降低对IT资源的依赖。
确保领域对象范式的逻辑清晰性,同时让规则引擎灵活地处理多变的业务规则。
确定何处使用规则引擎:并非所有的业务规则都需要由规则引擎处理。对于稳定、不经常变更的核心业务规则,可以内置在领域对象中。对于经常变更或需要由业务人员直接管理的业务规则,可以交由规则引擎处理。
规则引擎与领域模型的交互:将规则引擎作为领域服务,允许它操作和决策领域对象。规则引擎可以在需要时接收领域对象,根据业务规则做出判断,然后更改领域对象的状态或者引导对象进行相应的行为。
明确领域模型的边界:明确哪些操作应该由领域模型完成,哪些决策应该由规则引擎来执行。领域对象应该封装业务的固有逻辑和行为,规则引擎主要侧重于处理可配置和可变的业务决策。
保持规则引擎的透明性:应对规则引擎进行足够的记录和审计,确保当规则执行产生结果时,我们能理解为何会产生该结果。这也对于调试和分析业务规则执行过程非常有帮助。
避免规则冲突:需要避免创建与领域对象内置业务逻辑冲突的规则。我们做领域模型设计时,已经对业务产生了深入理解。如果规则引擎中的规则与领域对象范式内置逻辑存在冲突,可能会导致错误的业务行为。
在实践过程中,我们的目标是平衡领域驱动设计和规则引擎的使用,两者合作可以让我们的应用更加灵活,并更好地符合业务的需求。
在领域驱动设计(DDD)中,隐式概念和显示概念是关于如何在代码中表示业务规则和业务逻辑的概念。
隐式概念(Implicit Concept):这些是在业务规则或业务逻辑中存在,却并未明确地在代码中体现出来的概念。它们通常存在于代码的某个特定部分,或分散在代码的多个地方。
显示概念(Explicit Concept):这些是直接在代码中定义的,明确体现了业务规则和业务逻辑的概念。这通常体现在定义的类、结构、方法、变量、常量等代码元素中。
在DDD中,推崇将隐式概念转变为显示概念,因为它可以简化代码,降低维护难度,提高代码质量,使得代码对业务需求的表述更加精确,也能更好地支持业务的变化和增长。
引入新的领域对象:如果当前的模型无法清晰地表示某个业务概念,可以考虑引入一个新的领域对象。例如,如果代码中到处都在对一个"日期范围"进行操作,那你可以创建一个新的"日期范围"领域对象。
重构领域对象:如果一个对象包括了太多职责或概念,那么可以尝试将它进行拆分,每个对象只表达一个概念。例如,如果你有一个表示"用户"的对象,但它同时也包括了"账户"相关的逻辑,那你可以将"账户"部分独立为一个新的领域对象。
引入领域服务:有时,某个特定的业务操作无法归属于任何一个特定的实体,而是跨越了多个实体,这时可以创建一个新的领域服务来承担这个业务操作。
使用值对象:如果有一些关联的属性被经常一起使用,那么这些属性可以被封装为一个值对象。例如,“地址"包括"街道”、“城市”、“省份"和"邮编”,它们通常被一起使用,所以可以把它们封装为一个"地址"值对象。
通过将隐式概念转换为显式概念,我们能够使代码更加符合业务需求,更易于阅读和维护,更容易适应和响应业务的变化。
柔性设计指的是建立一个能够适应变化、方便扩展和维护的设计。其目标是使得当业务需求或技术环境发生变化时,系统可以方便地进行适应,降低系统变化的成本。
非局部变量,也不会操作IO。不正确的状态或数据继续运行。确定哪些概念和操作应该归组在一起,哪些应该分开。独立存在、不需要依赖于其他类就能完成自己的功能的类。public class Product {
private String id;
private String name;
private double price;
public Product(String id, String name, double price) {
this.id = id;
this.name = name;
this.price = price;
}
// Getters and setters...
public void increasePrice(double increase) {
this.price += increase;
}
public void decreasePrice(double decrease) {
this.price -= decrease;
}
}
