DDD架构思想专栏一《初识领域驱动设计DDD落地》

引言

最近准备给自己之前写的项目做重构,这是一个单体架构的小项目,后端采用的是最常见的三层架构。因为项目比较简单,其实采用三层架构就完全够了。但是呢,小编最近在做DDD架构的项目,于是就先拿之前写的一个老项目试试手。在重构的过程中,对DDD设计思想也有一些体会。于是我就写了这一个系列的博客来记录我从学习DDD架构思想再到将这种架构思想投入到实践的过程。

这一篇博客主要是先学习一下DDD架构思想(也就是先入个门),我会先介绍DDD架构思想中的一些概念,然后再介绍根据DDD架构思想所设计出来的常见的架构分层,最后就是入门实战,根据上面介绍的架构分层搭建一个单体架构项目。

DDD分层架构

前言介绍

DDD(Domain-Driven Design 领域驱动设计)是由Eric Evans最先提出,目的是对软件所涉及到的领域进行建模,以应对系统规模过大时引起的软件复杂性的问题。整个过程大概是这样的,开发团队和领域专家一起通过 通用语言(Ubiquitous Language)去理解和消化领域知识,从领域知识中提取和划分为一个一个的子领域(核心子域,通用子域,支撑子域),并在子领域上建立模型,再重复以上步骤,这样周而复始,构建出一套符合当前领域的模型。

b65ec64998044dc596e89ac0583ecb4b.png

开发目标

依靠领域驱动设计的设计思想,通过事件风暴建立领域模型,合理划分领域逻辑和物理边界,建立领域对象及服务矩阵和服务架构图,定义符合DDD分层架构思想的代码结构模型,保证业务模型与代码模型的一致性。通过上述设计思想、方法和过程,指导团队按照DDD设计思想完成微服务设计和开发。

  1. 高内聚低耦合:通过将相关的领域概念和业务逻辑组织在一起,实现高内聚。同时,通过定义明确的上下文边界和良好的模块划分,降低模块之间的依赖关系,实现低耦合。这样可以提高代码的可读性、可维护性和可扩展性。

  2. 清晰的领域模型:通过充血模型的方式,将业务逻辑封装在领域对象中,使领域模型具备自己的责任和行为。领域模型应该能够准确地反映业务需求,对业务规则进行验证,并与领域专家的语言保持一致。这样可以提高开发人员对业务的理解,并减少误解和沟通成本。

  3. 易于扩展和演化:项目结构应该具有良好的可扩展性,能够支持未来的需求变更和功能扩展。通过使用聚合、领域事件等概念,将系统分解为更小的、独立的组件,可以降低变更的影响范围,提高系统的可维护性。同时,采用领域事件和事件驱动的方式,可以更好地应对业务规则的变化和演化。

  4. 可测试性:良好的项目结构应该支持单元测试、集成测试和端到端测试等不同层次的测试。通过将业务逻辑封装在领域对象中,可以更容易地编写和执行单元测试,并验证系统的功能和正确性。此外,通过使用依赖注入等技术,可以更方便地进行模块的替换和模拟,提高测试的灵活性和可靠性。

基本的服务分层

这里看不懂没关系,看完下面的内容再来看这里,就会理解不少了。

9ccffebf860e4df3b5e5adec1909601e.png

基础概念

领域

领域就是用来确定范围的,范围即边界,这也是 DDD 在设计中不断强调边界的原因。简言之,DDD 的领域就是这个边界内要解决的业务问题域

领域可以进一步划分为子领域。我们把划分出来的多个子领域称为子域,每个子域对应一个更小的问题域或更小的业务范围。领域可以拆分为多个子领域。一个领域相当于一个问题域,领域拆分为子域的过程就是大问题拆分为小问题的过程。

其实很好理解,DDD 的研究方法与自然科学的研究方法类似。当人们在自然科学研究中遇到复杂问题时,通常的做法就是将问题一步一步地细分,再针对细分出来的问题域,逐个深入研究,探索和建立所有子域的知识体系。当所有问题子域完成研究时,我们就建立了全部领域的完整知识体系了

在领域不断划分的过程中,领域会细分为不同的子域,子域可以根据自身重要性和功能属性划分为三类子域,它们分别是:核心域、通用域和支撑域

那么三类子域的作用分别是什么?

核心域:

核心域是指系统中最重要、最核心的业务领域,它包含了组织的竞争优势所在,也是系统的关键价值所在。在核心域中,包含了核心业务逻辑和最关键的业务规则,这些规则对系统的成功运行至关重要。在设计和开发过程中,核心域通常是最需要投入精力和资源来进行建模和实现的部分。核心域通常是系统的重点关注对象,是系统的灵魂所在。

通用域:

通用域是指那些在多个系统中都普遍存在的通用业务领域,它们通常不会直接带来组织的竞争优势,但是对系统整体的功能提供了一定的支持。通用域中包含了通用的业务逻辑和规则,这些规则在多个系统中都可能会有类似的实现。通用域通常是可以被复用的部分,可以在不同的系统中得到共享和重用。

支撑域:

支撑域是指那些对核心域和通用域提供支撑和服务的业务领域。支撑域通常包括了各种基础设施、技术支持、通用功能等,它们并不直接参与核心业务流程,但是对核心业务和通用业务的实现提供了必要的支持。支撑域为核心域和通用域提供了必要的基础设施和支持,使它们能够顺利地实现业务目标。

领域事件

领域事件(Domain Event)是领域驱动设计中的一个重要概念,用于表示在领域模型中发生的有意义的事件。它们通常代表着领域中的一些重要状态变化或业务行为,可以被其他领域对象订阅和响应。

举例来说的话,领域事件可以是业务流程的一个步骤,比如投保业务缴费完成后,触发投保单转保单的动作;也可能是定时批处理过程中发生的事件,比如批处理生成季缴保费通知单,触发发送缴费邮件通知操作;或者一个事件发生后触发的后续动作,比如密码连续输错三次,触发锁定账户的动作。

通过上面的例子我们可以看出,通过领域事件驱动的异步化机制,可以推动业务流程和数据在各个不同微服务之间的流转,实现微服务的解耦,减轻微服务之间服务调用的压力,提升用户体验。

一个完整的领域事件 = 事件发布 + 事件存储 + 事件分发 + 事件处理。

事件发布:构建一个事件,需要唯一标识,然后发布;
事件存储:发布事件前需要存储,因为接收后的事件也会存储,可用于重试或对账等;就是每次执行一次具体的操作时,把行为记录下来,执行持久化。
事件分发:服务内的应用服务或者领域服务直接发布给订阅者,服务外需要借助消息中间件,比如Kafka,RabbitMQ等,支持同步或者异步。
事件处理:先将事件存储,然后再处理。
当然了,实际开发中事件存储和事件处理不是必须的。

因此实现方案:发布订阅模式,分为跨上下文(kafka,RocketMq)和上下文内(spring事件,Guava Event Bus)的领域事件。

下面是一个领域事件的示例代码

// 领域事件类,表示订单已创建的事件
public class OrderCreatedEvent {
    private final String orderId;
    private final String customerId;

    public OrderCreatedEvent(String orderId, String customerId) {
        this.orderId = orderId;
        this.customerId = customerId;
    }

    public String getOrderId() {
        return orderId;
    }

    public String getCustomerId() {
        return customerId;
    }
}

// 领域模型中的订单实体
public class Order {
    private String orderId;
    private String customerId;
    // 其他属性和方法省略

    // 创建订单的方法
    public void create(String orderId, String customerId) {
        // 执行订单创建的业务逻辑

        // 触发订单已创建的领域事件
        OrderCreatedEvent event = new OrderCreatedEvent(orderId, customerId);
        DomainEventPublisher.publish(event);
    }
}

// 领域事件发布者
public class DomainEventPublisher {
    private static List<EventListener> listeners = new ArrayList<>();

    public static void publish(DomainEvent event) {
        for (EventListener listener : listeners) {
            listener.handleEvent(event);
        }
    }

    public static void subscribe(EventListener listener) {
        listeners.add(listener);
    }
}

// 领域事件监听器接口
public interface EventListener {
    void handleEvent(DomainEvent event);
}

// 订单创建事件的监听器
public class OrderCreatedEventListener implements EventListener {
    public void handleEvent(DomainEvent event) {
        if (event instanceof OrderCreatedEvent) {
            OrderCreatedEvent orderCreatedEvent = (OrderCreatedEvent) event;
            // 处理订单已创建的逻辑,例如发送邮件通知等
            System.out.println("订单已创建,订单号:" + orderCreatedEvent.getOrderId());
        }
    }
}

// 在应用层配置和使用领域事件
public class Application {
    public static void main(String[] args) {
        // 创建订单实例
        Order order = new Order();

        // 注册订单创建事件的监听器
        DomainEventPublisher.subscribe(new OrderCreatedEventListener());

        // 创建订单
        order.create("123456", "7890");
    }
}

在上述示例中,我定义了一个名为OrderCreatedEvent的领域事件类,它表示订单已创建的事件,并包含了订单ID和客户ID等信息。在订单实体的create方法中,当订单创建成功时,会触发一个OrderCreatedEvent的领域事件,并通过DomainEventPublisher来发布事件。

DomainEventPublisher是领域事件的发布者,它负责管理事件的订阅者并将事件分发给它们。在示例中,我定义了一个OrderCreatedEventListener作为订单创建事件的监听器,它会在接收到订单创建事件后执行相应的逻辑,例如发送邮件通知等。

在应用层的Application类中,我创建了一个订单实例,并注册了订单创建事件的监听器。当我调用订单的create方法时,订单创建事件会被发布和处理,从而完成相应的业务逻辑。

这是一个简单的领域事件示例,实际应用中,领域事件可以更复杂,并且可能会有多个事件和多个监听器。通过使用领域事件,我们可以更好地解耦业务逻辑,并且允许各个领域对象之间进行松散的协作。

限界上下文

我们可以将限界上下文拆解为两个词:限界和上下文。限界就是领域的边界,而上下文则是语义环境。通过领域的限界上下文,我们就可以在统一的领域边界内用统一的语言进行交流,简单来说限界上下文可以理解为语义环境。

综合一下,我认为限界上下文的定义就是:用来封装通用语言和领域对象,提供上下文环境,保证在领域之内的一些术语、业务相关对象等(通用语言)有一个确切的含义,没有二义性。这个边界定义了模型的适用范围,使团队所有成员能够明确地知道什么应该在模型中实现,什么不应该在模型中实现。

下面我举一个例子:

在一个明媚的早晨,孩子起床问妈妈:“今天应该穿几件衣服呀?”妈妈回答:“能穿多少就穿多少!”那到底是穿多还是穿少呢?

如果没有具体的语义环境,还真不太好理解。但是,如果你已经知道了这句话的语义环境,比如是寒冬腊月或者是炎炎夏日,那理解这句话的涵义就会很容易了。

所以语言离不开它的语义环境。

而业务的通用语言就有它的业务边界,我们不大可能用一个简单的术语没有歧义地去描述一个复杂的业务领域。限界上下文就是用来细分领域,从而定义通用语言所在的边界。

正如电商领域的商品一样,商品在不同的阶段有不同的术语,在销售阶段是商品,而在运输阶段则变成了货物。同样的一个东西,由于业务领域的不同,赋予了这些术语不同的涵义和职责边界,这个边界就可能会成为未来微服务设计的边界。看到这,我想你应该非常清楚了,领域边界就是通过限界上下文来定义的。

理论上限界上下文就是微服务的边界。我们将限界上下文内的领域模型映射到微服务,就完成了从问题域到软件的解决方案

可以说,限界上下文是微服务设计和拆分的主要依据。在领域模型中,如果不考虑技术异构、团队沟通等其它外部因素,一个限界上下文理论上就可以设计为一个微服务

贫血模型和充血模型

贫血模型:贫血模型指的是将数据和行为分离,将数据保存在对象中,而将业务逻辑操作放在服务层或外部类中。贫血模型认为对象只是简单地保存数据,不具备独立的行为和能力 。

贫血模型具有一堆属性和set get方法,存在的问题就是通过pojo这个对象上看不出业务有哪些逻辑,一个pojo可能被多个模块调用,只能去上层各种各样的service来调用,这样以后当梳理这个实体有什么业务,只能一层一层去搜service,也就是贫血失忆症,不够面向对象。

充血模型:充血模型指的是在领域模型中,将业务逻辑封装在实体对象中,实体对象不仅包含数据属性,还包含操作和行为方法。充血模型认为领域模型应该是富有行为和能力的,具有自己的责任和职责。

比如如下user用户有改密码,改手机号,修改登录失败次数等操作,都内聚在这个user实体中,每个实体的业务都是清晰的,就是充血模型,充血模型的内存计算会多一些,内聚核心业务逻辑处理。

说白了就是,不只是有贫血模型中setter getter方法,还有其他的一些业务方法,这才是面向对象的本质,通过user实体就能看出有哪些业务存在。

充血模式代码示例:

@NoArgsConstructor
@Getter
public class User extends Aggregate<Long, User> {

    /**
     * 用户名
     */
    private String userName;

    /**
     * 姓名
     */
    private String realName;

    /**
     * 手机号
     */
    private String phone;

    /**
     * 密码
     */
    private String password;

    /**
     * 锁定结束时间
     */
    private Date lockEndTime;

    /**
     * 登录失败次数
     */
    private Integer failNumber;

    /**
     * 用户角色
     */
    private List<Role> roles;

    /**
     * 部门
     */
    private Department department;

    /**
     * 用户状态
     */
    private UserStatus userStatus;

    /**
     * 用户地址
     */
    private Address address;

    public User(String userName, String phone, String password) {

        saveUserName(userName);
        savePhone(phone);
        savePassword(password);
    }

    /**
     * 保存用户名
     * @param userName
     */
    private void saveUserName(String userName) {
        if (StringUtils.isBlank(userName)){
            Assert.throwException("用户名不能为空!");
        }

        this.userName = userName;
    }

    /**
     * 保存电话
     * @param phone
     */
    private void savePhone(String phone) {
        if (StringUtils.isBlank(phone)){
            Assert.throwException("电话不能为空!");
        }

        this.phone = phone;
    }

    /**
     * 保存密码
     * @param password
     */
    private void savePassword(String password) {
        if (StringUtils.isBlank(password)){
            Assert.throwException("密码不能为空!");
        }

        this.password = password;
    }

    /**
     * 保存用户地址
     * @param province
     * @param city
     * @param region
     */
    public void saveAddress(String province,String city,String region){
        this.address = new Address(province,city,region);
    }

    /**
     * 保存用户角色
     * @param roleList
     */
    public void saveRole(List<Role> roleList) {

        if (CollectionUtils.isEmpty(roles)){
            Assert.throwException("角色不能为空!");
        }

        this.roles = roleList;
    }
}

实体和值对象

实体和值对象这两个概念都是领域模型中的领域对象。实体和值对象是组成领域模型的基础单元。

实体

在代码模型中,实体的表现形式是实体类,这个类包含了实体的属性和方法,通过这些方法实现实体自身的业务逻辑。在 DDD 里,这些实体类通常采用充血模型,与这个实体相关的所有业务逻辑都在实体类的方法中实现,跨多个实体的领域逻辑则在领域服务中实现

实体(Entity)是一个唯一标识(ID)的对象,它具有生命周期和可变状态,并通过其标识属性来区分不同的实例。比如商品是商品上下文的一个实体,通过唯一的商品 ID 来标识,不管这个商品的数据如何变化,商品的 ID 一直保持不变,它始终是同一个商品。

在领域模型映射到数据模型时,一个实体可能对应 0 个、1 个或者多个数据库持久化对象。大多数情况下实体与持久化对象是一对一。在某些场景中,有些实体只是暂驻静态内存的一个运行态 实体,它不需要持久化。比如,基于多个价格配置数据计算后生成的折扣实体。

而在有些复杂场景下,实体与持久化对象则可能是一对多或者多对一的关系。比如,用户 user 与角色 role 两个持久化对象可生成权限实体,一个实体对应两个持久化对象,这是一对多的场景。再比如,有些场景为了避免数据库的联表查询,提升系统性能,会将客户信息 customer 和账户信息 account 两类数据保存到同一张数据库表中,客户和账户两个实体可根据需要从一个持久化对象中生成,这就是多对一的场景。

代码示例:用户实体

@NoArgsConstructor
@Getter
public class User extends Aggregate<Long, User> {

    /**
     * 用户id-聚合根唯一标识
     */
    private UserId userId;

    /**
     * 用户名
     */
    private String userName;

    /**
     * 姓名
     */
    private String realName;

    /**
     * 手机号
     */
    private String phone;

    /**
     * 密码
     */
    private String password;

    /**
     * 锁定结束时间
     */
    private Date lockEndTime;

    /**
     * 登录失败次数
     */
    private Integer failNumber;

    /**
     * 用户角色
     */
    private List<Role> roles;

    /**
     * 部门
     */
    private Department department;

    /**
     * 领导
     */
    private User leader;

    /**
     * 下属
     */
    private List<User> subordinationList = new ArrayList<>();

    /**
     * 用户状态
     */
    private UserStatus userStatus;

    /**
     * 用户地址
     */
    private Address address;

    public User(String userName, String phone, String password) {

        saveUserName(userName);
        savePhone(phone);
        savePassword(password);
    }

    /**
     * 保存用户名
     * @param userName
     */
    private void saveUserName(String userName) {
        if (StringUtils.isBlank(userName)){
            Assert.throwException("用户名不能为空!");
        }

        this.userName = userName;
    }

    /**
     * 保存电话
     * @param phone
     */
    private void savePhone(String phone) {
        if (StringUtils.isBlank(phone)){
            Assert.throwException("电话不能为空!");
        }

        this.phone = phone;
    }

    /**
     * 保存密码
     * @param password
     */
    private void savePassword(String password) {
        if (StringUtils.isBlank(password)){
            Assert.throwException("密码不能为空!");
        }

        this.password = password;
    }

    /**
     * 保存用户地址
     * @param province
     * @param city
     * @param region
     */
    public void saveAddress(String province,String city,String region){
        this.address = new Address(province,city,region);
    }

    /**
     * 保存用户角色
     * @param roleList
     */
    public void saveRole(List<Role> roleList) {

        if (CollectionUtils.isEmpty(roles)){
            Assert.throwException("角色不能为空!");
        }

        this.roles = roleList;
    }

    /**
     * 保存领导
     * @param leader
     */
    public void saveLeader(User leader) {
        if (Objects.isNull(leader)){
            Assert.throwException("leader不能为空!");
        }
        this.leader = leader;
    }

    /**
     * 增加下属
     * @param user
     */
    public void increaseSubordination(User user) {

        if (null == user){
            Assert.throwException("leader不能为空!");
        }

        this.subordinationList.add(user);
    }
}

值对象

值对象(Value Object)是没有唯一标识的对象,它的价值在于其属性值本身。值对象是不可变的,没有生命周期,并且可以根据相等性来比较和判断是否相同。

简单来说,值对象本质上就是一个集合。那这个集合里面有什么呢?若干个用于描述目的、具有整体概念和不可修改的属性。那这个集合存在的意义又是什么?在领域建模的过程中,值对象可以保证属性归类的清晰和概念的完整性,避免属性零碎。

/**
 * 地址数据
 */
@Getter
public class Address extends ValueObject {
    /**
     * 省
     */
    private String province;

    /**
     * 市
     */
    private String city;

    /**
     * 区
     */
    private String region;

    public Address(String province, String city, String region) {
        if (StringUtils.isBlank(province)){
            Assert.throwException("province不能为空!");
        }
        if (StringUtils.isBlank(city)){
            Assert.throwException("city不能为空!");
        }
        if (StringUtils.isBlank(region)){
            Assert.throwException("region不能为空!");

        }
        this.province = province;
        this.city = city;
        this.region = region;
    }
}

下面举一个简单的例子

462228551f034fc1a4fc3fee8ef76813.png

人员实体原本包括:姓名、年龄、性别以及人员所在的省、市、县、街道等属性。这样显示地址相关的属性不就很零碎?现在我们可以将省、市、县、街道等属性拿出来构成一个地址属性集合,这个集合就是值对象了。

拿在上面情况下可以把实体的属性进行聚合写出一个值对象呢

值对象逻辑上是实体属性的一部分,用于描述实体的特征。值对象创建后就不允许修改了,只能用另外一个值对象来整体替换。值对象是一些不会修改,只能完整替换的属性值的集合,你更关注他的属性和值,它没有太多的业务行为,用于描述实体的一些属性集,被实体引用,依附于实体的值对象基本没有自己的数据库表。是否要设计成值对象,你要看这个对象是否后续还会来回修改,会不会有生命周期。如果不可修改,并且以后也不会专门针对它进行查询或者统计,你就可以把它设计成值对象,如果不行,那就设计成实体吧。

在领域建模时,我们可以将部分对象设计为值对象,保留对象的业务涵义,同时又减少了实体的数量;在数据建模时,我们可以将值对象嵌入实体,减少实体表的数量,简化数据库设计。

这里说一下,DDD思想是提倡从领域设计出发,而不是先设计数据模型。如果按照传统的数据模型设计通常是一个表对应一个实体,一个主表关联多个从表,当实体表太多时,就会使数据库的设计变得无比复杂,但是领域模型就可以通过将重点放在领域模型的设计和领域对象的行为上,帮助开发团队更好地理解和处理复杂的业务问题,并将其分解为可管理的领域对象和领域服务。

聚合

用专业术语来解释的话,聚合(Aggregate)是一种用于组织和管理领域对象的设计模式。聚合将多个对象组合成一个逻辑单元,以便于对领域对象进行操作和维护,同时保证了领域对象之间的一致性和完整性。

你可以这么理解,聚合就是由业务和逻辑紧密关联的实体和值对象组合而成的,它们共同完成一个业务活动或者实现一个业务规则。聚合是数据修改和持久化的基本单元,每一个聚合对应一个仓储,实现数据的持久化。

聚合根

聚合根是聚合中的一个特殊对象,它是聚合的入口点和协调者,负责控制聚合内部的所有对象,并将聚合与外部系统进行交互。聚合根具有唯一的标识符,它代表了整个聚合,是聚合中最重要的对象。

聚合根在聚合中担任着重要的角色,它负责维护聚合内部的所有对象之间的关系,保证聚合的一致性和完整性。聚合根还负责处理聚合内部的业务逻辑,并与外部系统进行交互,比如持久化和查询数据等。

需要注意的是,聚合根并不是聚合内部的所有对象的代表,它只是聚合的入口点。聚合根与聚合内部的其他对象之间的关系是通过引用关系来建立的,不同的聚合内部也可以有相同类型的对象

举例分析

下面我举个例子来说明聚合诞生的完整过程

假设我们正在设计一个电子商务系统,其中有两个核心领域对象:订单(Order)和订单项(OrderItem)。订单对象表示用户下的订单,订单项对象表示订单中的商品项。

  1. 首先,我们需要确定聚合的范围。在这个例子中,我们可以将订单作为聚合,因为订单是一个完整的业务活动单元,包含了订单项等相关信息。

  2. 创建聚合根。我们将创建一个名为Order的聚合根,它具有唯一标识符(例如订单号)和相关属性(如订单状态、支付方式等)。聚合根还负责处理与订单相关的业务逻辑,如计算订单总金额、验证支付状态等。

  3. 添加实体和值对象。在Order聚合内部,我们可以定义实体和值对象,比如OrderItem实体表示订单中的商品项,具有自己的属性(如商品ID、数量、价格等)。值对象可以表示一些不可变的属性,例如订单地址、收货人姓名等。

  4. 确定聚合内部的关系和约束。在这个例子中,订单项是依赖于订单的,因此需要将订单项作为订单的子对象。订单项与订单之间的关系通过引用来建立,在订单中保存订单项的集合。

  5. 定义聚合根的行为。根据业务需求,我们可以在Order聚合根中定义一些方法和操作,例如添加订单项、删除订单项、取消订单等。这些方法负责维护聚合内部对象之间的一致性,并处理相应的业务逻辑。

  6. 注意聚合边界。聚合根应该是聚合中唯一可以直接访问的对象,外部系统通过聚合根来与聚合进行交互。其他聚合内部的对象应该通过聚合根来进行访问和操作,以确保聚合的完整性和一致性。

架构分层

用户接口层

用户接口层是前端应用和微服务之间服务访问和数据交换的桥梁。接口服务位于用户接口层,用于处理用户发送的Restful请求和解析用户输入的配置文件等,并将信息传递给应用层。或获取应用服务的数据后,进行数据组装,向前端提供数据服务。主要服务形态是 Facade 服务。Facade 服务分为接口和实现两个部分。完成服务定向,DO 与 DTO 数据的转换和组装,实现前端与应用层数据的转换和交换

代码示例:用户接口层(User Interface Layer)

// UserController.java
@RestController
@RequestMapping("/users")
public class UserController {
    @Autowired
    private UserService userService;

    @PostMapping("/")
    public ResponseEntity createUser(@RequestBody CreateUserRequest request) {
        UserDTO userDTO = userService.createUser(request);
        return ResponseEntity.ok(userDTO);
    }
}

应用层

应用层是很薄的一层,理论上不应该有业务规则或逻辑,主要面向用例和流程相关的操作。但应用层又位于领域层之上,因为领域层包含多个聚合,所以它可以协调多个聚合的服务和领域对象完成服务编排和组合,协作完成业务操作。除了同步方法调用外,还可以发布或者订阅领域事件,权限校验、事务控制,一个事务对应一个聚合根。

应用层负责不同聚合之间的服务和数据协调,负责微服务之间的事件发布和订阅。通过应用服务对外暴露微服务的内部功能,这样就可以隐藏领域层核心业务逻辑的复杂性以及内部实现机制。应用层的主要服务形态有:应用服务、事件发布和订阅服务。应用服务内用于组合和编排的服务,主要来源于领域服务,也可以是外部微服务的应用服务。

代码示例:应用层(Application Layer)

// UserService.java
@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;

    public UserDTO createUser(CreateUserRequest request) {
        User user = new User(request.getUsername(), request.getEmail);
        userRepository.save(user);
        return new UserDTO(user.getId(), user.getUsername(), user.getEmail());
    }
}

领域层

  • 领域服务位于领域层,为完成领域中跨实体或值对象的操作转换而封装的服务,领域服务以与实体和值对象相同的方式参与实施过程。
  • 领域服务对同一个实体的一个或多个方法进行组合和封装,或对多个不同实体的操作进行组合或编排,对外暴露成领域服务。领域服务封装了核心的业务逻辑。实体自身的行为在实体类内部实现,向上封装成领域服务暴露。
  • 为隐藏领域层的业务逻辑实现,所有领域方法和服务等均须通过领域服务对外暴露。
  • 为实现微服务内聚合之间的解耦,原则上禁止跨聚合的领域服务调用和跨聚合的数据相互关联。

代码示例:领域层(Domain Layer)

// User.java
@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;
    private String email;

    // 构造函数、Getter和Setter等略
}

// UserRepository.java
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}

基础层

  • 也叫基础设施层,基础层是贯穿所有层的,它的作用就是为其它各层提供通用的技术和基础服务,包括第三方工具、驱动、消息中间件、网关、文件、缓存以及数据库等。比较常见的功能还是提供数据库持久化
  • 基础层的服务形态主要是仓储服务。仓储服务包括接口和实现两部分。仓储接口服务供应用层或者领域层服务调用,仓储实现服务,完成领域对象的持久化或数据初始化。

代码示例:基础层(Infrastructure Layer)

// JpaUserRepository.java
@Repository
public class JpaUserRepository implements UserRepository {
    @Autowired
    private JpaUserDao userDao;

    @Override
    public void save(User user) {
        userDao.save(user);
    }

    // 其他持久化操作的实现
}

DDD设计思想

所以前面讲了那么多,那DDD架构分层设计思想到底是什么呢?下面我给出我的理解

领域驱动设计(DDD)是一种软件设计方法,其核心思想是将业务领域的知识和规则贯穿于整个软件开发过程中,以确保软件系统能够更好地反映现实世界的业务需求。DDD 设计思想的关键点包括以下几个方面:

  1. 领域模型:DDD 强调通过领域模型来表达业务领域的复杂性和规则。领域模型是对业务概念、过程和规则的抽象表示,它基于业务专家和开发团队之间的沟通和协作,帮助理解业务需求并将其映射到软件设计中。

  2. 模型驱动设计:在DDD 中,领域模型是设计的核心,它驱动着软件系统的构建。开发团队需要不断地与业务人员合作,深入理解业务需求,并将这些需求转化为可执行的领域模型。

  3. 通用语言:为了促进业务人员和开发人员之间的沟通和理解,DDD 强调采用通用语言(Ubiquitous Language)。通用语言是指在整个软件开发过程中使用的统一的业务术语和概念,以确保所有人对业务领域的理解保持一致。

  4. 分层架构:在DDD 中,通常会采用分层架构来组织软件系统,其中包括领域层、应用层和基础设施层。这种分层设计有助于将业务逻辑与技术实现分离,提高系统的可维护性和可扩展性。

  5. 持续演化:领域驱动设计认识到业务领域的复杂性和不断变化,因此强调软件系统应该是持续演化的。开发团队需要不断地根据业务需求和反馈进行调整和改进,以确保系统能够满足业务的变化和发展。

DDD 设计思想致力于将业务领域的知识和规则融入到软件设计中,以提高软件系统的质量、灵活性和适应能力,从而更好地满足业务需求。

项目实战

项目结构

1c1b31933bc442048d8f185f6e96538b.png

909df8818c7944a99fdab36384c15304.png

具体代码

下面展示重点代码块,需要完整项目的可以下载绑定资源

application/UserService.java | 应用层用户服务,领域层服务做具体实现

package com.kjz.application.service;

import com.kjz.domain.model.aggregates.UserRichInfo;

/**
 * 应用层用户服务
 */
public interface UserService {

    UserRichInfo queryUserInfoById(Long id);

}

domain/repository/IuserRepository.java | 领域层资源库,由基础层实现

package com.kjz.domain.repository;

import com.kjz.infrastructure.po.UserEntity;


public interface IUserRepository {

    void save(UserEntity userEntity);

    UserEntity query(Long id);

}

domain/service/UserServiceImpl.java | 应用层实现类,应用层是很薄的一层可以只做服务编排

package com.kjz.domain.service;

import com.kjz.application.service.UserService;
import com.kjz.domain.model.aggregates.UserRichInfo;
import com.kjz.domain.model.vo.UserInfo;
import com.kjz.domain.model.vo.UserSchool;
import com.kjz.domain.repository.IUserRepository;
import com.kjz.infrastructure.po.UserEntity;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;


@Service("userService")
public class UserServiceImpl implements UserService {

    @Resource(name = "userRepository")
    private IUserRepository userRepository;

    @Override
    public UserRichInfo queryUserInfoById(Long id) {
        
        // 查询资源库
        UserEntity userEntity = userRepository.query(id);

        UserInfo userInfo = new UserInfo();
        userInfo.setName(userEntity.getName());

        // TODO 查询学校信息,外部接口
        UserSchool userSchool_01 = new UserSchool();
        userSchool_01.setSchoolName("株洲市第二中学");

        UserSchool userSchool_02 = new UserSchool();
        userSchool_02.setSchoolName("厂里");

        List<UserSchool> userSchoolList = new ArrayList<>();
        userSchoolList.add(userSchool_01);
        userSchoolList.add(userSchool_02);

        UserRichInfo userRichInfo = new UserRichInfo();
        userRichInfo.setUserInfo(userInfo);
        userRichInfo.setUserSchoolList(userSchoolList);

        return userRichInfo;
    }

}

infrastructure/po/UserEntity.java | 数据库对象类

package com.kjz.infrastructure.po;

/**
 * 数据库实体对象;用户实体
*/
public class UserEntity {

    private Long id;
    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

infrastructrue/repository/UserRepository.java | 领域层定义接口,基础层资源库实现

package com.kjz.infrastructure.repository.mysql;

import com.kjz.domain.repository.IUserRepository;
import com.kjz.infrastructure.dao.UserDao;
import com.kjz.infrastructure.po.UserEntity;
import org.springframework.stereotype.Repository;

import javax.annotation.Resource;


@Repository("userMysqlRepository")
public class UserMysqlRepository implements IUserRepository {

    @Resource
    private UserDao userDao;

    @Override
    public void save(UserEntity userEntity) {
        userDao.save(userEntity);
    }

    @Override
    public UserEntity query(Long id) {
        return userDao.query(id);
    }
}

interfaces/dto/UserInfoDto.java | DTO对象类,隔离数据库类

package com.kjz.interfaces.dto;

public class UserInfoDto {

    private Long id;        // ID

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

}

interfaces/facade/DDDController.java | 门面接口

package com.kjz.interfaces.facade;

import com.kjz.application.service.UserService;
import com.kjz.interfaces.dto.UserInfoDto;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.annotation.Resource;


@Controller
public class DDDController {

    @Resource(name = "userService")
    private UserService userService;

    @RequestMapping("/index")
    public String index(Model model) {
        return "index";
    }

    @RequestMapping("/api/user/queryUserInfo")
    @ResponseBody
    public ResponseEntity queryUserInfo(@RequestBody UserInfoDto request) {
        return new ResponseEntity<>(userService.queryUserInfoById(request.getId()), HttpStatus.OK);
    }

}

综上总结

  • 以上基于DDD一个基本入门的结构演示完成,实际开发可以按照此模式进行调整。
  • 目前这个架构分层还不能很好的进行分离,以及层级关系的引用还不利于扩展。
  • 后续会持续完善以及可以组合搭建RPC框架等,让整个架构更利于互联网开发。

专栏持续更新中,感兴趣的读者大大可以关注我一下哟!谢谢!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/228345.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【ET8】1.ET8入门-运行指南

主要学习网址 论坛地址为&#xff1a;https://et-framework.cn Git地址为&#xff1a;GitHub - egametang/ET: Unity3D Client And C# Server Framework 官方QQ群 : 474643097 项目检出 检出项目切换到release8.0分支 GitHub地址&#xff1a;GitHub - egametang/ET: Unity…

2023-12-08面试

1、自我介绍 面试官你好&#xff0c;我叫平明博&#xff0c;来自河南郑州&#xff0c;19年毕业&#xff0c;所学专业软件工程&#xff0c;之前任职于南京华苏科技&#xff0c;担任开发工程师一职&#xff0c;在职期间主要对省间现货相关项目进行研发&#xff0c;核心就是从多平…

nvidia安装出现7-zip crc error解决办法

解决办法&#xff1a;下载network版本&#xff0c;重新安装。&#xff08;选择自己需要的版本&#xff09; 网址&#xff1a;CUDA Toolkit 12.3 Update 1 Downloads | NVIDIA Developer 分析原因&#xff1a;local版本的安装包可能在下载过程中出现损坏。 本人尝试过全网说的…

crmeb本地开发配置代理

crmeb 是一个开源的商城系统&#xff0c; v5 版本是一个前后端分离的项目&#xff0c; 我们从git仓库中下载下来的是一个文件夹&#xff0c;其结构是这样的 我的系统没有使用docker &#xff0c;使用的是 laragon 的系统 所以首先我们要在 nginx 中配置 之后&#xff0c; 我们…

钒电解液回收提钒树脂

#钒电解液回收提钒树脂 钒是一种重要的战略金属具有硬度大、抗拉强度强、熔点高等优点主要应用于冶金、电池、核材料、航空航天及能源等领域。 钒电池全称全钒氧化还原液流电池具有环境友好、循环寿命长、能量效率较高等优点&#xff0c;钒电解液是钒电池的关键部分由钒离子和硫…

阿里云国际基于CentOS系统镜像快速部署Apache服务

阿里云轻量应用服务器提供了Windows Server系统镜像和主流的Linux系统镜像&#xff0c;您可以通过该类镜像创建纯净、安全、稳定的运行环境。本文以CentOS 7.6系统镜像为例&#xff0c;介绍如何快速配置Apache服务。 背景信息 注意&#xff0c;阿里云国际通过corebyt注册并充…

【小白专用】MySQL入门(详细总结)

3. 创建数据库 使用 create database 数据库名; 创建数据库。 create database MyDB_one; create database DBAliTest; 创建数据库成功后&#xff0c;数据库的数量变成了6个&#xff0c;多了刚才创建的 dbalitest 。 4. 创建数据库时设置字符编码 使用 create database 数据…

(六) python观察者设计模式

6.1行为型模式简介 观察者设计模式是最简单的行为型模式之一,所以我们先简单了解一下行为型模式 创建型模式的工作原理是基于对象的创建机制的。由于这些模式隔离了对象的创建细 节&#xff0c;所以使得代码能够与要创建的对象的类型相互独立。结构型模式用于设计对象和类的结…

echarts折线图的数据显示

一、 echarts让折线图的每个折点都显示y轴的数值 效果如下 // 在 series中添加 itemStyle : { normal: {label : {show: true}}}series: [{name: 买入汇率,data: BuyRate,type: line,itemStyle : { normal: {label : {show: true}}}},{name: 卖出汇率,data: SaleRate,type: lin…

仅需30秒完美复刻任何人的声音 - 最强AI音频11Labs

我的用词一直都挺克制的&#xff0c;基本不会用到“最强”这个字眼。 但是这一次的这个AI应用&#xff0c;是我认为在TTS&#xff08;文字转音频&#xff09;这个领域&#xff0c;当之无愧的“最强”。 ElevenLabs&#xff0c;简称11Labs。 仅需30秒到5分钟左右的极少的数据集…

Qt简介、工程文件分离、创建Qt工程、Qt的帮助文档

QT 简介 core&#xff1a;核心模块&#xff0c;非图形的接口类&#xff0c;为其它模块提供支持 gui&#xff1a;图形用户接口&#xff0c;qt5之前 widgets&#xff1a;图形界面相关的类模块 qt5之后的 database&#xff1a;数据库模块 network&#xff1a;网络模块 QT 特性 开…

土壤水分传感器土壤体积含水率含量监测仪器

产品概述 外型小巧轻便&#xff0c;便于携带和连接。 土壤水分传感器由电源模块、变送模块、漂零及温度补偿模块、数据处理模块等组成。传感器内置信号采样及放大、漂零及温度补偿功能&#xff0c;用户接口简洁、方便。 功能特点 ◆本传感器体积小巧化设计&#xff0c;测量…

Sam Altman当选“TIME时代周刊”2023年度最佳CEO!还有梅西、Taylor Swift当选...

TIME时代周刊昨日在官网公布了2023年最佳CEO—— Sam Altman当选! 此外&#xff0c;Taylor Swift当选年度最佳人物&#xff0c;梅西当选年度最佳运动员。 Sam Altman的当选可谓是实至名归&#xff01;没有谁能比火爆全球的ChatGPT背后&#xff0c;OpenAI的CEO更“成功”了。 …

手把手教你写 Compose 动画 -- 讲的不能再细的 AnimationSpec 动画规范

前面我们聊过 animateDpAsState 和 Animatable 两种动画 API 的用法&#xff0c;但只是简单了解了&#xff0c;这两个函数内部都有一个共同的核心参数&#xff1a;AnimationSpec。 Composable fun animateDpAsState(targetValue: Dp,animationSpec: AnimationSpec<Dp> …

代码随想录算法训练营第45天| 70. 爬楼梯 (进阶) 322. 零钱兑换 279.完全平方数

JAVA代码编写 70. 爬楼梯&#xff08;进阶版) 卡码网&#xff1a;57. 爬楼梯&#xff08;第八期模拟笔试&#xff09; 题目描述 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬至多m (1 < m < n)个台阶。你有多少种不同的方法可以爬到楼顶呢&#xff1f…

卷王开启验证码后无法登陆问题解决

问题描述 使用 docker 部署&#xff0c;后台设置开启验证&#xff0c;重启服务器之后&#xff0c;docker重启&#xff0c;再次访问系统&#xff0c;验证码获取失败&#xff0c;导致无法进行验证&#xff0c;也就无法登陆系统。 如果不了解卷王的&#xff0c;可以去官网看下。…

【K8S】微服务不香了?单体化改造悄然兴起!!

微服务一直以来是服务治理的基本盘之一,落地到云原生上,往往是每个 K8s pods 部署一个服务,独立迭代、独立运维。 但是在快速部署的时候,有时候,我们可能需要一些宏服务的优势。有没有一种方法,能够 “既要又要” 呢?本文基于 tRPC-Go 服务,提出并最终实践了一种经验证…

医学图像数据处理流程以及遇到的问题

数据总目录&#xff1a; /home/bavon/datasets/wsi/hsil /home/bavon/datasets/wsi/lsil 1 规整文件命名以及xml拷贝 data_prepare.py 的 align_xml_svs 方法 if __name__ __main__: file_path "/home/bavon/datasets/wsi/lsil"# align_xml_svs(file_path) # b…

程序员的养生指南(生命诚可贵,一人永流传!珍惜生命,从你我做起)

作为程序员&#xff0c;我们经常需要长时间坐在电脑前工作&#xff0c;这对我们的身体健康造成了很大的影响。为了保持健康&#xff0c;我们需要采取一些养生措施来延寿。下面是我个人的一些养生经验和建议&#xff0c;希望能对大家有所帮助。 1、合理安排工作时间&#xff1a;…

Bert-vits2新版本V2.1英文模型本地训练以及中英文混合推理(mix)

中英文混合输出是文本转语音(TTS)项目中很常见的需求场景&#xff0c;尤其在技术文章或者技术视频领域里&#xff0c;其中文文本中一定会夹杂着海量的英文单词&#xff0c;我们当然不希望AI口播只会念中文&#xff0c;Bert-vits2老版本(2.0以下版本)并不支持英文训练和推理&…