A0.领域驱动设计(DDD)之开篇与六边形架构实践
前言
这是一个基于Java 21 的 六边形架构与领域驱动设计的一个通用项目,并且结合现有的最新版本技术架构实现了 领域驱动设计模式和六边形架构模式组件定义. 并且结合微服务,介绍了领域层,领域事件,资源库,分布式锁,序列化,安全认证,日志等,并提供了实现功能. 并且我会以日常发布文章和更新代码的形式来完善它.
模式介绍
想要了解这个项目,我们首先要知道领域驱动设计,六边形架构,与微服务架构模式。并且这个项目也是围绕这些模式进行实现的。 接下来,我会介绍这些模式的核心概念,然后再以项目代码的形式来作为补充说明。
领域驱动设计(DDD)
领域驱动设计有很多的概念,例如 通用语言,领域模型(实体,值对象,聚合),子域(核心,支撑,通用),界限上下文,服务(应用服务,领域服务),领域事件,工厂,资源库,建模方法论等等。
在开始初期,我先介绍领域模型,服务,事件,资源库,其他部分可以在建模方法论和微服务架构的实现来一一逐步了解。
领域模型
领域模型是领域内核心概念的精确表达,包括实体(Entities)、值对象(Value Objects)、聚合/聚合根(Aggregates/Aggregate Roots)、领域服务(Domain Services)等元素,反映了业务领域的结构、策略和操作规则。
代码实现
基于我们对领域驱动设计,六边形架构,微服务的理解,在项目初期,我们创建了common-core模块,并利用Java注解,接口等方式定义了组件, 当我们在使用这些组件时,可以通过注解,实现,继承,组合等方式知道属于哪部分。
领域层对象定义
我们分别使用三个接口来定义领域对象(值对象,实体,聚合根), 其中值对象与实体都定义了一个比较的方法,来判断是否为同一个对象。
值对象
package com.iokays.common.core.domain;
import java.io.Serializable;
/**
* 值对象
*/
public interface ValueObject<T> extends Serializable {
/**
* 值对象通过属性值比较,它们没有标识。
*
* @param other 另一个值对象。
* @return 如果给定的值对象和这个值对象的属性相同,则返回<code>true</code>。
*/
boolean sameValueAs(T other);
}
服务
该项目分别定义了2个服务,即上述的应用服务和领域服务
package com.iokays.common.core.service;
/**
* 应用服务
* 管理事务,安全,锁, 通用日志等非业务性行为
*/
public interface ApplicationService {
}
package com.iokays.common.core.service;
/**
* 领域服务
* 一般参数为实体,返回值对象
*/
public interface DomainService {
}
事件
事件属于消息的一种,所以在项目中首先定义了一个接口Message,用来标识消息, 然后才定义了事件接口。
package com.iokays.common.core.message;
import java.io.Serializable;
/**
* 消息标识
*/
public interface Message extends Serializable {
}
package com.iokays.common.core.event;
import com.iokays.common.core.message.Message;
/**
* 事件接口
*/
public interface Event extends Message {
}
与此同时,我们还定义了一个事件ID类,因为ID是固定不变的,所以我们使用了Record。额外添加了静态方法,用于生产事件ID。
package com.iokays.common.core.event;
import java.io.Serializable;
import java.util.UUID;
import static org.apache.commons.lang3.Validate.notNull;
/**
* 事件ID
*
* @param id {@link UUID} 事件唯一标识ID
*/
public record EventId(UUID id) implements Serializable {
public EventId {
notNull(id, "事件ID不能为空");
}
/**
* 创建事件ID {@link EventId}
*
* @param value {@link String} UUID类型的原始字符串
* @return 事件ID {@link EventId}
*/
public static EventId form(final String value) {
return new EventId(UUID.fromString(value));
}
/**
* 创建一个唯一的事件ID {@link EventId}
*
* @return 事件ID {@link EventId}
*/
public static EventId generate() {
return new EventId(UUID.randomUUID());
}
}
首先,定义了领域事件.
package com.iokays.common.core.event;
/**
* 领域事件接口
* 领域事件是唯一的,但没有生命周期的东西。标识可以是显式的,例如付款的序列号,也可以从事件的各个方面派生,例如何时发生了什么。
*/
public interface DomainEvent<T> extends Event {
/**
* @param other 另一个领域事件。
* @return <code>true</code>如果给定的领域事件和这个事件被认为是相同的事件。
*/
boolean sameEventAs(T other);
}
同时我们而外定义了一个应用事件,该应用事件是定义在领域层之外,且该事件的发送成功与失败都不会影响到业务的进行, 可以理解该事件是可以接受丢失的。
package com.iokays.common.core.event;
/**
* 应用事件接口
* 该事件,一般用于业务的开始,同时该事件丢失,是可以接受的。
*/
public interface ApplicationEvent<T> extends Event {
}
资源库
资源库属于基础设施层, 所以我们定义了基础设施层顶层接口, 然后再定义资源库。
package com.iokays.common.core.infrastructure;
/**
* 基础设施层顶层接口
*/
public interface Infrastructure {
}
package com.iokays.common.core.infrastructure;
/**
* 基础设施层 之 资源库接口
* 隔离对外部数据库的访问,对应的适配器提供聚合的持久化能力。
*/
public interface Repository extends Infrastructure {
}
同时我们还定义了2个基础设施层, 分别是客户端和发布器
package com.iokays.common.core.infrastructure;
/**
* 基础设施层 之 客户端口
* 隔离对上游限界上下午或第三方服务的访问,对应的适配器提供对服务的调用能力
*/
public interface Client extends Infrastructure {
}
package com.iokays.common.core.infrastructure;
/**
* 基础设施层 之 发布器端口:
* 隔离对外部时间总数的访问,对应的适配器提供发布事件消息的能力。
*/
public interface Publisher extends Infrastructure {
}
上述基于DDD的顶层类定义,接下来我们看看六边形架构的顶层基类定义。
适配器
适配器分为两种,主适配器(入站)和次适配器(出站)。
package com.iokays.common.core.adapter;
import java.lang.annotation.*;
/**
* 主适配器(别名Driving Adapter)代表用户如何使用应用,
* 从技术上来说,它们接收用户输入,调用端口并返回输出。
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DriverAdapter {
}
package com.iokays.common.core.adapter;
import java.lang.annotation.*;
/**
* 次适配器(别名Driven Adapter)
* 实现应用的出口端口,向外部工具执行操作
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DrivenAdapter {
}
查询&命令接口
package com.iokays.common.core.query;
import java.io.Serializable;
/**
* 查询接口
*/
public interface Query extends Serializable {
}
package com.iokays.common.core.command;
import java.io.Serializable;
import java.time.Instant;
/**
* 命令接口
*/
public interface Command extends Serializable {
/**
* 从系统中获取当前时间
*
* @return {@link Instant} 当前时间
*/
static Instant now() {
return Instant.now();
}
/**
* 获取命令的唯一标识
*
* @return {@link CommandId} 命令的唯一标识
*/
CommandId id();
}
同时我们还定义了命令ID.
package com.iokays.common.core.command;
import java.io.Serializable;
import java.util.Objects;
import java.util.UUID;
/**
* 不可变的记录类型,用于标识命令的唯一标识
*
* @param value {@link UUID} 命令的唯一标识
*/
public record CommandId(UUID value) implements Serializable {
/**
* 默认构造函数
*
* @param value {@link UUID} 命令的唯一标识
*/
public CommandId {
Objects.requireNonNull(value, "CommandId value must not be null");
}
/**
* 从字符串中构造 {@link CommandId}
*
* @param value {@link String} UUID类型的原始字符串
* @return 实例 {@link CommandId}
*/
public static CommandId from(final String value) {
return new CommandId(UUID.fromString(value));
}
/**
* 生成一个唯一的 {@link CommandId}
*
* @return 实例 {@link CommandId}
*/
public static CommandId generate() {
return new CommandId(UUID.randomUUID());
}
}
微服务部分
分布式锁
该分布式锁只是一个注解,当使用该注解时,并放在应用服务层,就可以利用切面的方式来实现它。
package com.iokays.common.core.lock;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.time.temporal.ChronoUnit;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface DistributedLock {
/**
* 锁实例的名称
*
* @return 指定锁名称
*/
String value() default "";
/**
* 用于动态计算key的Spring表达式
*
* @return Spring Expression Language (SpEL) expression for computing the key dynamically.
*/
String key() default "";
/**
* 等待锁的最大时间
*
* @return the maximum time to wait for the lock
*/
long time() default 0;
/**
* 时间单位
*
* @return he time unit of the time argument
*/
ChronoUnit unit() default ChronoUnit.SECONDS;
}