DDD 主要关注于创建与业务领域紧密相关的软件模型,以确保软件能够准确地解决实际问题。
DDD 的核心理念
领域模型(Domain Model):
领域模型是对特定业务领域知识的精确表述,它包括业务中的实体(Entities)、值对象(Value Objects)、服务(Services)、聚合(Aggregates)、聚合根(Aggregate Roots)等概念。领域模型是DDD的核心,它反映了业务专家的语言和决策。
统一语言(Ubiquitous Language):
统一语言是开发团队与业务专家共同使用的语言,它在整个项目中保持一致。统一语言确保所有人都对业务概念有着相同的理解,减少沟通成本和误解。
限界上下文(Bounded Context):
限界上下文是明确界定的系统边界,在这个边界内部有一套统一的模型和语言。不同的限界上下文之间可能有不同的模型,它们通过上下文映射(Context Mapping)来进行交互和集成。
聚合(Aggregate):
聚合是一组相关对象的集合,它们被视为数据修改的单元。每个聚合都有一个聚合根,它是外部对象与聚合内部对象交互的唯一入口。
领域服务(Domain Services):
当某些行为不自然属于任何实体或值对象时,这些行为可以被定义为领域服务。领域服务通常表示领域中的一些操作或业务逻辑。
应用服务(Application Services):
应用服务是软件的一部分,它们协调领域对象来执行任务。它们负责应用程序的工作流程,但不包含业务规则或知识。
基础设施(Infrastructure):
基础设施包括为领域模型提供持久化机制(如数据库)、消息传递、应用程序的配置等技术组件。
领域事件(Domain Events):
领域事件是领域中发生的有意义的业务事件,它们可以触发其他子系统的反应或流程。
DDD 的目标是通过将软件的关注点集中在核心领域上,并通过丰富的领域模型来管理复杂性,从而提高软件的质量和维护性。DDD 强调与业务专家的紧密合作,以确保软件解决方案能够准确反映业务需求。通过这种方法,软件开发团队可以创建出更加灵活、可扩展且与业务紧密结合的系统。
其中DDD所提到的软件设计方法涵盖了;范式、模型、框架、方法论,主要活动包括建模、测试、工程、开发、部署、维护。
软件设计方法是指一系列用于指导软件开发过程的原则、概念和实践。这些方法通常包括范式、模型、框架和方法论。下面我将分别介绍这些概念以及软件设计的主要活动。
范式 (Paradigms)
范式是指软件设计和开发的基本风格或哲学。它通常定义了编程的基本原则和模式。常见的软件设计范式包括:
结构化编程:强调程序结构的重要性,使用顺序、选择和循环控制结构。
面向对象编程 (OOP):基于对象的概念,将数据和处理数据的方法封装在一起。
函数式编程:将计算视为数学函数的评估,避免状态改变和可变数据。
事件驱动编程:以事件为中心,响应用户操作、消息或其他系统事件。
面向接口编程:功能效用基于接口
模型 (Models)
模型是对软件系统的抽象表示,用于帮助理解、设计和测试系统。常用的软件设计模型包括:
UML (统一建模语言):一套图形化的建模语言,用于描述、设计和文档化软件项目。
ER模型 (实体-关系模型):用于数据库设计,描述数据的实体及其之间的关系。
状态机模型:描述系统可能的状态、事件和在这些事件发生时的转换。
Domain(领域)的概念
在DDD中,领域是指具体业务领域的知识、业务逻辑、数据以及业务规则的集合。它是软件要解决问题的业务环境,通常由一系列子领域组成,每个子领域代表业务中的一个特定部分。
领域的特性
业务中心:领域是围绕业务需求和业务规则构建的,它是软件设计的核心。
模型驱动:领域模型是对业务知识的抽象,它通过领域实体、值对象、服务、聚合等概念来表达。
语言一致性:领域模型的构建基于统一语言(Ubiquitous Language),这是开发团队与业务专家共同使用的语言,确保沟通无歧义。
边界清晰:领域模型定义了清晰的边界,这些边界划分了不同的子领域和聚合,有助于管理复杂性和维护性。
领域的用途
业务逻辑的封装:领域模型封装了业务逻辑,使得业务规则和数据操作集中管理,便于理解和维护。
沟通工具:领域模型作为开发团队与业务专家之间的共同语言,有助于提高沟通效率,确保软件开发紧密跟随业务需求。
软件设计的基础:领域模型是软件设计的基础,它指导着软件的架构和实现。
实现手段
实体(Entity):具有唯一标识的领域对象,代表业务中的实体。
值对象(Value Object):描述领域中的一些特性或概念,没有唯一标识,通常是不可变的。
聚合(Aggregate):一组相关的实体和值对象的集合,它们一起构成一个数据和业务规则的单元。
领域服务(Domain Service):在领域模型中执行特定业务逻辑的无状态服务,通常操作多个实体或聚合。
领域事件(Domain Event):表示领域中发生的重要业务事件,用于解耦系统的不同部分。
仓储(Repository):提供对聚合根的持久化操作,如保存和检索,通常与数据库交互。
领域适配器(Domain Adapter):领域适配器是适配器模式在DDD中的应用,它的目的是使得领域模型能够与外部系统或技术细节进行交互,而不会受到污染。
工厂(Factory):用于创建复杂的聚合或实体,封装创建逻辑。如 OpenAi项目、Lottery 项目都运用了工厂,也包括如 chatglm-sdk-java 的开发,就是会话模型结构用工厂对外提供服务。
聚合对象
聚合是领域模型中的一个关键概念,它是一组具有内聚性的相关对象的集合,这些对象一起工作以执行某些业务规则或操作。聚合定义了一组对象的边界,这些对象可以被视为一个单一的单元进行处理。
关键:聚合内实现事务一致性、聚合外实现最终一致性。
特性
一致性边界:聚合确保其内部对象的状态变化是一致的。当对聚合内的对象进行操作时,这些操作必须保持聚合内所有对象的一致性。
根实体:每个聚合都有一个根实体(Aggregate Root),它是聚合的入口点。根实体拥有一个全局唯一的标识符,其他对象通过根实体与聚合交互。
事务边界:聚合也定义了事务的边界。在聚合内部,所有的变更操作应该是原子的,即它们要么全部成功,要么全部失败,以此来保证数据的一致性。
用途
封装业务逻辑:聚合通过将相关的对象和操作封装在一起,提供了一个清晰的业务逻辑模型,有助于业务规则的实施和维护。
保证一致性:聚合确保内部状态的一致性,通过定义清晰的边界和规则,聚合可以在内部强制执行业务规则,从而保证数据的一致性。
简化复杂性:聚合通过组织相关的对象,简化了领域模型的复杂性。这有助于开发者更好地理解和扩展系统。
实现手段
定义聚合根:选择合适的聚合根是实现聚合的第一步。聚合根应该是能够代表整个聚合的实体,并且拥有唯一标识。
限制访问路径:只能通过聚合根来修改聚合内的对象,不允许直接修改聚合内部对象的状态,以此来维护边界和一致性。
设计事务策略:在聚合内部实现事务一致性,确保操作要么全部完成,要么全部回滚。对于聚合之间的交互,可以采用领域事件或其他机制来实现最终一致性。
封装业务规则:在聚合内部实现业务规则和逻辑,确保所有的业务操作都遵循这些规则。
持久化:聚合根通常与数据持久化层交互,以保存聚合的状态。这通常涉及到对象-关系映射(ORM)或其他数据映射技术。
以下是一个简化的Java代码示例,展示了如何在DDD中实现一个聚合。在这个例子中,我们将创建一个简单的订单系统,其中包含订单聚合(Order Aggregate)和订单项(OrderItem)作为内部实体。订单聚合根(Order)将封装所有业务规则,并通过聚合根进行所有的交互。
首先,定义实体和值对象的基类:
public abstract class BaseEntity {
protected Long id;
public Long getId() {
return id;
}
}
然后,定义订单项(OrderItem)作为实体:
public class OrderItem extends BaseEntity {
private String productName;
private int quantity;
private double price;
public OrderItem(String productName, int quantity, double price) {
this.productName = productName;
this.quantity = quantity;
this.price = price;
}
public double getTotalPrice() {
return quantity * price;
}
//省略getter和setter方法
}
接下来,定义订单聚合根(Order):
public class OrderAggregate extends BaseEntity {
private List<OrderItem> orderItems;
private String customerName;
private boolean isPaid;
public OrderAggregate(String customerName) {
this.customerName = customerName;
this.orderItems = new ArrayList<>();
this.isPaid = false;
}
public void addItem(OrderItem item) {
//业务规则:订单未支付时才能添加订单项
if (!isPaid) {
orderItems.add(item);
} else {
throw new IllegalStateException("Can not add items to apaid order.");
}
}
public double getTotalAmount() {
return orderItems.stream().mapToDouble(OrderItem::getTotalPrice).sum();
}
public void markAsPaid() {
//业务规则:订单总金额必须大于0才能标记为已支付
if (getTotalAmount() > 0) {
isPaid = true;
} else {
throw new IllegalStateException("Order total must begreater than 0 to bepaid.");
}
}
//省略getter和setter方法
}
最后,创建一个订单,并添加一些订单项:
public class OrderDemo{
public static void main(String[] args){
//创建订单聚合
OrderAggregate orderAggregate= new OrderAggregate("Clearwind");
//添加订单项
orderAggregate.addItem(new OrderItem("手机",1,1000.00));
orderAggregate.addItem(new OrderItem("数据线",2,25.00));
//获取订单总金额
System.out.println("Total amount:" + orderAggregate.getTotalAmount());
//标记订单为已支付
orderAggregate.markAsPaid();
}
}
实体
实体 = 唯一标识 + 状态属性 + 行为动作(功能),是DDD中的一个基本构建块,它代表了具有唯一标识的领域对象。实体不仅仅包含数据(状态属性),还包含了相关的行为(功能),并且它的标识在整个生命周期中保持不变。
特性
唯一标识:实体具有一个可以区分其他实体的标识符。这个标识符可以是一个ID、一个复合键或者是一个自然键,关键是它能够唯一地标识实体实例。
领域标识:实体的标识通常来源于业务领域,例如用户ID、订单ID等。这些标识符在业务上有特定的含义,并且在系统中是唯一的。
委派标识:在某些情况下,实体的标识可能是由ORM(对象关系映射)框架自动生成的,如数据库中的自增主键。这种标识符虽然可以唯一标识实体,但它并不直接来源于业务领域。
用途
表达业务概念:实体用于在软件中表达具体的业务概念,如用户、订单、交易等。通过实体的属性和行为,可以描述这些业务对象的特征和能力。
封装业务逻辑:实体不仅仅承载数据,还封装了业务规则和逻辑。这些逻辑包括验证数据的有效性、执行业务规则、计算属性值等。这样做的目的是保证业务逻辑的集中和一致性。
保持数据一致性:实体负责维护自身的状态和数据一致性。它确保自己的属性和关联关系在任何时候都是正确和完整的,从而避免数据的不一致性。
实现手段
在实现实体时,通常会采用以下手段:
定义实体类:在代码中定义一个类,该类包含实体的属性、构造函数、方法等。
实现唯一标识:为实体类提供一个唯一标识的属性,如ID,并确保在实体的生命周期中这个标识保持不变。
封装行为:在实体类中实现业务逻辑的方法,这些方法可以操作实体的状态,并执行相关的业务规则。
使用ORM框架:利用ORM框架将实体映射到数据库表中,这样可以简化数据持久化的操作。
实现领域服务:对于跨实体或跨聚合的操作,可以实现领域服务来处理这些操作,而不是在实体中直接实现。
使用领域事件:当实体的状态发生变化时,可以发布领域事件,这样可以通知其他部分的系统进行相应的处理。
通过上述手段,实体在DDD架构中扮演着重要的角色,它不仅代表了业务概念,还封装了业务逻辑,并通过其唯一标识确保了数据的一致性。
以下是一个简单的Java代码示例,展示了如何在领域驱动设计(DDD)中实现一个实体。我们将创建一个User实体,它代表了一个用户,具有唯一的用户ID、姓名和电子邮件地址,并且可以执行一些基本的行为。
public class UserEntity {
//实体的唯一标识符
private final UUID id;
//用户的状态属性
private String name;
private String email;
//构造函数,用于创建实体实例
public UserEntity(UUID id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
//可以在这里添加验证逻辑,确保创建的实体是有效的
}
//实体的行为方法,例如更新用户的姓名
public void updateName(String newName) {
//可以在这里添加业务规则,例如验证姓名的格式
this.name = newName;
}
//实体的行为方法,例如更新用户的电子邮件地址
public void updateEmail(String newEmail) {
//可以在这里添加业务规则,例如验证电子邮件地址的格式
this.email = newEmail;
}
//Getter方法
public UUID getId() {
return id;
}
public String getName() {
return name;
}
public String getEmail() {
return email;
}
//实体的equals和hashCode方法,基于唯一标识符实现
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UserEntity user = (UserEntity) o;
return id.equals(user.id);
}
//toString方法,用于打印实体信息
@Override
public String toString() {
return "UserEntity{" +
"id=" + id +
",name='" + name + '\'' +
",email='" + email + '\'' +
'}';
}
}
public class UserEntityDemo {
public static void main(String[] args) {
//创建一个新的用户实体
UserEntity user = new UserEntity(UUID.randomUUID(), "clearwind", "clearwind@qq.com");
//打印用户信息
System.out.println(user);
//更新用户的姓名
user.updateName("clearwind111");
//打印更新后的用户信息
System.out.println(user);
//更新用户的电子邮件地址
user.updateEmail("clearwind111@qq.com");
//打印更新后的用户信息
System.out.println(user);
}
}
在这个例子中,User类代表了用户实体,它有一个唯一的标识符id,这个标识符在实体的整个生命周期中保持不变。name和email是用户的状态属性,updateName和updateEmail是封装了业务逻辑的行为方法。
值对象
在领域驱动设计(Domain-Driven Design, DDD)中,值对象(Value Object)是一个核心概念,用于封装和表示领域中的概念,其特点是它们描述了领域中的某些属性或度量,但不具有唯一标识。
值对象 = 值 + 对象,用于描述对象属性的值,表示具体固定不变的属性值信息。
概念
值对象是由一组属性组成的,它们共同描述了一个领域概念。与实体(Entity)不同,值对象不需要有一个唯一的标识符来区分它们。值对象通常是不可变的,这意味着一旦创建,它们的状态就不应该改变。
特性
不可变性(Immutability):值对象一旦被创建,它的状态就不应该发生变化。这有助于保证领域模型的一致性和线程安全性。
等价性(Equality):值对象的等价性不是基于身份或引用,而是基于对象的属性值。如果两个值对象的所有属性值都相等,那么这两个对象就被认为是等价的。
替换性(Replaceability):由于值对象是不可变的,任何需要改变值对象的操作都会导致创建一个新的值对象实例,而不是修改现有的实例。
侧重于描述事物的状态:值对象通常用来描述事物的状态,而不是事物的唯一身份。
可复用性(Reusability):值对象可以在不同的领域实体或其他值对象中重复使用。
用途
值对象的用途非常广泛,它们可以用来表示:
金额和货币(如价格、工资、费用等)
度量和数据(如重量、长度、体积等)
范围或区间(如日期范围、温度区间等)
复杂的数学模型(如坐标、向量等)
任何其他需要封装的属性集合
实现手段
在实现值对象时,通常会遵循以下几个步骤:
定义不可变类:确保类的所有属性都是私有的,并且只能通过构造函数来设置。
重写equals和hashCode方法:这样可以确保值对象的等价性是基于它们的属性值,而不是对象的引用。
提供只读访问器:只提供获取属性值的方法,不提供修改属性值的方法。
使用工厂方法或构造函数创建实例:这有助于确保值对象的有效性和一致性。
考虑序列化支持:如果值对象需要在网络上传输或存储到数据库中,需要提供序列化和反序列化的支持。
示例
以订单状态为例,可以定义一个值对象来表示不同的状态:
public enum OrderStatusVO {
PLACED(0, "下单"),
PAID(1, "支付"),
COMPLETED(2, "完成"),
CANCELLED(3, "退单");
private final int code;
private final String description;
OrderStatusVO(int code, String description) {
this.code = code;
this.description = description;
}
public int getCode() {
return code;
}
public String getDescription() {
return description;
}
//根据code获取对应的OrderStatus
public static OrderStatusVO fromCode(int code) {
for (OrderStatusVO status : OrderStatusVO.values()) {
if (status.getCode() == code) {
return status;
}
}
throw new IllegalArgumentException("Invalid code for OrderStatus:" + code);
}
}
在这个例子中,OrderStatusVO是一个枚举类型的值对象,它封装了订单状态的代码和描述。它是不可变的,并且提供了基于属性值的等价性。通过定义一个枚举,我们可以确保订单状态的值是受限的,并且每个状态都有一个明确的含义。
在数据库中,订单状态可能会以整数形式存储(例如,0表示下单,1表示支付等)。在应用程序中,我们可以使用OrderStatusVO枚举来确保我们在代码中使用的是类型安全的值,而不是裸露的整数。这样可以减少错误,并提高代码的可读性和可维护性。
当需要将订单状态存储到数据库中时,我们可以存储枚举的code值。当从数据库中读取订单状态时,我们可以使用fromCode方法来将整数值转换回OrderStatusVO枚举,这样就可以在代码中使用丰富的枚举类型而不是简单的整数。
值对象也可以用来表示更复杂的结构,比如一个地址:
public final class Address {
private final String street;
private final String city;
private final String zipCode;
private final String country;
public Address(String street, String city, String zipCode, String country) {
//这里可以添加验证逻辑以确保地址的有效性
this.street = street;
this.city = city;
this.zipCode = zipCode;
this.country = country;
}
//只读访问器
public String getStreet() {
return street;
}
public String getCity() {
return city;
}
public String getZipCode() {
return zipCode;
}
public String getCountry() {
return country;
}
//重写equals和hashCode方法
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Address address = (Address) o;
return street.equals(address.street) &&
city.equals(address.city) &&
zipCode.equals(address.zipCode) &&
country.equals(address.country);
}
@Override
public int hashCode() {
return Objects.hash(street, city, zipCode, country);
}
}
在这个例子中,AddressVO是一个不可变的值对象,它封装了一个地址的所有部分。它提供了只读访问器,并且重写了equals和hashCode方法以确保基于属性值的等价性。这样的设计有助于确保地址的一致性,并且可以在不同的实体之间重复使用,例如用户和商店都可能有地址。
总的来说,值对象是DDD中用于封装领域概念的重要工具,它们通过提供不可变性、基于属性的等价性和替换性来帮助构建一个清晰、一致和可维护的领域模型。