A0.领域驱动设计(DDD)之开篇与六边形架构实践

前言

这是一个基于Java 21 的 六边形架构与领域驱动设计的一个通用项目,并且结合现有的最新版本技术架构实现了 领域驱动设计模式和六边形架构模式组件定义. 并且结合微服务,介绍了领域层,领域事件,资源库,分布式锁,序列化,安全认证,日志等,并提供了实现功能. 并且我会以日常发布文章和更新代码的形式来完善它.

模式介绍

想要了解这个项目,我们首先要知道领域驱动设计,六边形架构,与微服务架构模式。并且这个项目也是围绕这些模式进行实现的。 接下来,我会介绍这些模式的核心概念,然后再以项目代码的形式来作为补充说明。

领域驱动设计(DDD)

领域驱动设计有很多的概念,例如 通用语言,领域模型(实体,值对象,聚合),子域(核心,支撑,通用),界限上下文,服务(应用服务,领域服务),领域事件,工厂,资源库,建模方法论等等。

在开始初期,我先介绍领域模型,服务,事件,资源库,其他部分可以在建模方法论和微服务架构的实现来一一逐步了解。

领域模型

领域模型是领域内核心概念的精确表达,包括实体(Entities)、值对象(Value Objects)、聚合/聚合根(Aggregates/Aggregate Roots)、领域服务(Domain Services)等元素,反映了业务领域的结构、策略和操作规则。

值对象

在业务角度上,由一个或多个属性组合成一个整体概念,关注点在属性上,它是一个没有 标识符 的对象。

实体

在业务角度上,拥有唯一标识符,在经历各种状态的变更,仍然可以保持一致,对于这类对象,重要的是延续和标识,而非属性。它可以包含多个实体和多个值对象。

聚合根

指定某个实体为聚合根,该实体下所有的实体,值对象的操作都是通过该聚合根作为操作入口,是操作的最小单位,负责协调实体和值对象按照固定的业务规则协同完成共同的业务逻辑, 具有高内聚特点,

服务

在领域驱动设计中,服务分为两种:应用服务,领域服务。但这两种概念完全不同。

领域服务

领域服务是在领域层,当模型的业务逻辑过于复杂,会将这部分业务逻辑转移到领域服务,也是业务领域的核心组件,负责处理复杂业务场景和流程控制。

应用服务

应用服务,不涉及到领域逻辑,或与横切关注点相关的服务,例如对锁,事务,通用日志等一些管理,

事件

在DDD中,我们所说的事件是领域事件,当一个完整的业务逻辑操作被完成后,业务领域会发送一个已完成的领域事件,该领域事件与业务的完成操作是同时存在的(共存亡)。 是该领域与其他领域或支撑业务交互的一种方式。

资源库

资源库用于保存和获取聚合对象,将实际的存储和查询技术封装起来,对外隐藏封装了数据访问机制。只为那些确实需要直接访问的聚合提供Repository。

六边形架构

六边形架构也称为端口和适配器模式,这种架构模式将应用的核心业务逻辑与外部依赖分离开来。核心业务逻辑只包含核心业务侧的完整性和复杂性,而外部依赖只关注技术侧的复杂性。

hexagonal architecture

六边形架构的内部其实包裹了领域驱动设计的领域层+应用服务层; 应用服务层提供统一的业务数据操作入口,不同业务场景的数据通过各自的适配器(入站)进行数据的转化, 领域模型不关心数据是怎么存储的,领域层与数据的存储层也是使用适配器(出站)进行数据的转化。

微服务架构

介绍了领域驱动设计和六边形架构,到了微服务部分,我们重点关注在分布式锁,和分布式事务这两个点。

代码实现

基于我们对领域驱动设计,六边形架构,微服务的理解,在项目初期,我们创建了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);

}

实体

package com.iokays.common.core.domain;

import java.io.Serializable;

/**
 * 实体
 */
public interface Entity<T> extends Serializable {

    /**
     * 实体通过标识比较,而不是通过属性比较。
     *
     * @param other 另一个实体。
     * @return 如果标识相同,则返回true,而不管其他属性如何。
     */
    boolean sameIdentityAs(T other);

}

聚合根

package com.iokays.common.core.domain;

import java.io.Serializable;

/**
 * 聚合根接口
 */
public interface AggregateRoot extends Serializable {
}

服务

该项目分别定义了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;

}

未完待续…​

项目的顶层类和注解已经介绍完了,在接下来的系列文章,我会逐步讲解领域驱动设计,微服务架构,六边形架构各个模式和组件的实现与实践.