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

news2024/11/19 1:30:11

引言

最近准备给自己之前写的项目做重构,这是一个单体架构的小项目,后端采用的是最常见的三层架构。因为项目比较简单,其实采用三层架构就完全够了。但是呢,小编最近在做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框架等,让整个架构更利于互联网开发。

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

解决Eslint和Prettier关于三元运算符的冲突问题

三元运算符Prettier的格式化 三元运算符Eslint的格式要求 解决办法 // eslint加入配置&#xff0c;屏蔽标红报错indent: [error, 2, { ignoredNodes: [ConditionalExpression] }]效果

HCIP —— BGP 基础 (上)

BGP --- 边界网关协议 &#xff08;路径矢量协议&#xff09; IGP --- 内部网关协议 --- OSPF RIP ISIS EGP --- 外部网关协议 --- EGP BGP AS --- 自治系统 由单一的组织或者机构独立维护的网络设备以及网络资源的集合。 因 网络范围太大 需 自治 。 为区分不同的AS&#…

智能优化算法应用:基于浣熊算法无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于浣熊算法无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于浣熊算法无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.浣熊算法4.实验参数设定5.算法结果6.参考文献7.MATLAB…

1 接口测试介绍

在软件测试工作中&#xff0c;接口测试是必不可少的。接口测试一般是发生在单元测试之后&#xff0c;系统测试之前。当开发人员输出API文档后&#xff0c;测试人员就可以开始编写接口测试用例了。接口测试可以让测试人员更早的介入&#xff0c;不需要等待前后端联调完成才开始测…

信息化,数字化,智能化三者是同一概念么?

引言 在当今科技和商业领域&#xff0c;信息化、数字化和智能化是三个极为关键的概念。信息化强调信息的获取、传递和应用&#xff0c;数字化则是将物理实体转化为数字形式&#xff0c;而智能化则赋予系统更高级的智能和自主性。这些概念的交汇与融合塑造着我们的现实&#xf…

Windows下安装Nginx

Nginx简介 Nginx 是一个高性能的HTTP和反向代理web服务器&#xff0c;其特点是占有内存少&#xff0c;并发能力强&#xff0c;其并发能力在同类型的网页服务器中表现较好。 Nginx安装 下载地址 安装稳定版本 下载完成后进行解压 可以双击nginx.exe 启动nginx 也可以打开cm…

基于POSIX标准的Linux进程间通信

文章目录 1 管道&#xff08;匿名管道&#xff09;1.1 管道抽象1.2 接口——pipe1.3 管道的特征1.4 管道的四种情况1.5 匿名管道用例 2 命名管道2.1 创建一个命名管道——mkfifo2.2 关闭一个管道文件——unlink2.3 管道和命名管道的补充2.4 命名管道用例 3 共享内存3.1 原理3.2…

中国特供阉割版 RTX 4090 曝光,老黄这操作绝了

到了现在大伙儿应该发现&#xff1a;国内禁售 NVIDIA RTX 4090 显卡这事儿基本实锤了。 实际上根据老美规定&#xff0c;从上个月 17 号开始&#xff0c;凡是公司主体在中国的显卡品牌&#xff0c;就已经不能生产和销售 RTX 4090。 以后厂商想要卖 4090 只能以整机形式出售&am…

第一启富金:新添澳大利亚(ASIC)牌照

第一启富金&#xff1a;澳大利亞證券及投資委員會&#xff08;ASIC&#xff09; GOLDWELL GLOBAL PTY LTD 是 WHOLESALE INVESTOR SERVICES PTY LTD&#xff08;CAR 編號 001304943&#xff09;的企業授權代表開發澳大利亞客戶&#xff0c;WHOLESALE INVESTOR SERVICES PTY LT…

二维码智慧门牌管理系统升级:提升社会管理和公共服务水平

文章目录 前言一、升级的意义二、升级方案三、升级后的好处 前言 随着科技不断进步&#xff0c;二维码智慧门牌管理系统在公共管理和服务领域扮演关键角色。随着需求的增长&#xff0c;现有系统已难以满足各方需求。因此&#xff0c;系统升级成为紧迫任务。 一、升级的意义 升…

静态HTTP和动态HTTP的混合使用:最佳实践

在当今的互联网环境中&#xff0c;静态HTTP和动态HTTP各有其优势和局限。静态HTTP具有速度快、安全性高和易于维护的特点&#xff0c;而动态HTTP则能够实现动态交互和处理大量动态数据。为了充分利用两者的优势&#xff0c;越来越多的网站开始采用静态HTTP和动态HTTP混合使用的…

存储成本降71%,怪兽充电历史库迁移OceanBase

怪兽充电作为共享充电宝第一股&#xff0c;业务增长迅速&#xff0c;以至于业务架构不停地增加组件。在验证 OceanBase 可以简化架构并带来更大的业务价值后&#xff0c;首次尝试在历史库中使用 OceanBase 替代 MySQL&#xff0c;存储成本降低 71%。本文为怪兽充电运维架构部王…

性能优化,单台4核8G机器支撑5万QPS

前言 这篇文章的主题是记录一次Python程序的性能优化&#xff0c;在优化的过程中遇到的问题&#xff0c;以及如何去解决的。为大家提供一个优化的思路&#xff0c;首先要声明的一点是&#xff0c;我的方式不是唯一的&#xff0c;大家在性能优化之路上遇到的问题都绝对不止一个…

Facebook广告报告指标CPC

在Facebook广告中&#xff0c;CPC可以作为一个关键指标来评估广告效果和投资回报。较低的CPC意味着广告主能以更低的价格获得更多的点击量&#xff0c;从而降低广告投放成本。而较高的CPC可能暗示着广告主需要更大的预算才能获得相同数量的点击。本文小编将讲讲Facebook广告报告…

剧本杀小程序搭建:打造线上剧本杀新体验

剧本杀是一款以角色扮演为主的游戏&#xff0c;一度成为了年轻人的最喜爱的社交游戏。在剧本杀市场需求下&#xff0c;剧本杀规模也迅速上升。今年第一季度&#xff0c;剧本杀市场规模环比增长47%&#xff0c;市场整体消费水平逐渐呈上升趋势。 随着剧本杀的不断发展&#xff…

富士通LPK240标签打印机维修案例

故障描述: 一台送修的富士通LPK240标签打印机,故障为通电不开机,打开机器后面的电源开关后电源灯不亮,按机器上面的测试按钮后红灯闪烁,无法正常工作; 速印机(理想、荣大等)、复印机(夏普、东芝、理光、佳能、震旦等全系列)、打印机、扫描仪、传真机、多媒体教学一体…

制作一个RISC-V的操作系统三-编译与链接

文章目录 GCCGCC简介GCC的命令格式gcc -Egcc -cgcc -Sgcc -ggcc -vGCC的主要执行步骤GCC涉及的文件类型针对多个源文件的处理 ELFELF介绍ELF文件格式ELF文件处理相关工具&#xff1a;Binutils&#xff08;binary utility&#xff09;readlelf -hreadelf -S或readelf -SW&#x…

MIT_线性代数笔记: 复习一

目录 问题一问题二问题三问题四 本讲为考前复习课&#xff0c;考试范围就是 Axb 这个单元&#xff0c;重点是长方形矩阵&#xff0c;与此相关的概念包括零空间、左零空间、秩、向量空间、子空间&#xff0c;特别是四个基本子空间。当矩阵为可逆的方阵时&#xff0c;很多性质是一…

【开源】基于Vue.js的停车场收费系统

文末获取源码&#xff0c;项目编号&#xff1a; S 076 。 \color{red}{文末获取源码&#xff0c;项目编号&#xff1a;S076。} 文末获取源码&#xff0c;项目编号&#xff1a;S076。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 停车位模块2.2 车辆模块2.3 停车收费…

谷达冠楠:抖音新手开店在哪里进货

随着抖音平台的日益火爆&#xff0c;越来越多的新手商家选择在抖音上开设自己的店铺。然而&#xff0c;开店的第一步就是货源问题&#xff0c;那么抖音新手开店应该在哪里进货呢? 首先&#xff0c;我们可以选择线上批发市场。例如阿里巴巴、拼多多等大型电商平台&#xff0c;这…