学习领域驱动设计(笔记)

183 阅读12分钟

应用架构

贫血模型和充血模型 经典贫血三层架构以及DDD常见的应用架构。不管哪种架构,都是对系统的描述,可能在模块、代码组织上有区别。本质上描述同一个事物

常见问题

  • 领域模型: 在DDD中,领域模型是对业务领域中概念和规则的抽象,它直接反映了业务的本质。这个模型是基于领域专家的知识和经验,作为团队理解业务并进行软件设计的基础。它负责表达业务概念和规则,是软件系统的核心。
  • 与领域专家的合作: 强调与领域专家合作是为了充分了解业务需求和规则。这种合作有助于确保软件模型真实地反映业务需求,从而创建更符合业务目标的系统。领域专家的经验和洞察力有助于更好地塑造领域模型。
  • 聚合和聚合根: 聚合是一组关联实体和值对象,它们一起完成某种业务功能。聚合根是聚合的主要实体,它负责确保聚合内的所有元素保持一致性。聚合根对外部对象可见,外部对象只能通过聚合根来访问聚合内的内容,从而保证了数据的完整性和领域规则的实施。
  • 与外部系统集成: 防腐层是在与外部系统集成时的一种策略,用于隔离和转换外部系统与领域模型之间的交互。这一层的存在确保了领域模型不会受到外部系统变化的影响,同时也确保了数据交互的一致性。
  • 适合采用DDD的项目: DDD最适合处理业务逻辑复杂、经常变化,并需要建立可持续发展、易于维护和扩展的系统的项目。在这样的情况下,DDD提供了一种有组织、结构化的方法来管理复杂性。但对于简单的项目,DDD可能会带来不必要的复杂性,因此需要权衡利弊。

模式对比

贫血模式:

  • 方便性:相对简单,方便使用。在Java等编程语言中,使用ORM框架从数据库获取数据、在Service层进行业务逻辑的操作很常见
  • 业务逻辑的封装问题:贫血模型中业务知识散落到Service层各个方法
  • 业务逻辑与技术实现的分离问题:贫血模型实现的Service层通常难以对业务逻辑和技术实现进行有效的分离,导致代码的可维护性和清晰性下降
  • 难以维护的问题:贫血模型的系统难以持续演化和维护,特别是多次迭代之后,Service层方法可能变得臃肿
image.png

充血模式:

  • 业务逻辑的集中:在充血模型中,业务逻辑不仅仅局限于Service业务行为,而是直接存在于领域对象中。业务规则和操作在领域对象内部进行封装,形成更加集中的自洽业务逻辑
  • 更好的封装性:提高了代码的模块性和可维护性
  • 领域对象的自我管理:能够自我管理自身的状态和行为。确保自身处于一致的状态,并执行特定业务规则,不需要依赖外部Service层的操作
  • 减少重复代码:可以减少在Service层中重复执行相似业务逻辑的情况
  • 更直观的代码:充血模型是的代码更加直观,业务逻辑和领域对象的行为直接反映了实际业务的需求,而不是简单数据容器

DDD 常见应用架构

经典四层架构

image.png

用户接口层(User Interface)

  • 责任: 负责与用户进行交互,接收用户输入,展示系统输出。
  • 组成: 包括各种图形界面、Web 界面、移动端应用等。
  • 特点: 不包含业务逻辑,只负责将用户请求传递给下一层处理,并按照协议返回执行结果。

应用层(Application)

  • 责任: 协调领域模型和基础设施层,完成业务操作。
  • 组成: 包含应用服务,负责加载领域模型中的聚合根,执行业务操作,返回处理结果。
  • 特点: 不包含具体业务逻辑,专注于调度领域模型的操作。 领域模型层(Domain Model)
  • 责任: 描述业务领域中的概念和规则,包括实体、值对象、领域服务等。
  • 组成: 包含业务逻辑和规则,以及实体、值对象、领域服务等。
  • 特点: 是软件系统的核心,业务逻辑和规则在这一层得到实现,但不包含任何与具体技术相关的代码。

基础设施层(Infrastructure)

  • 责任: 提供基础设施支持,如数据库访问、缓存、消息队列、日志等。
  • 组成: 包括与具体技术相关的代码,但不包含业务逻辑。
  • 特点: 为上层层次提供支持,是系统的最底层,专注于处理技术细节。

端口和适配器架构(六边形架构)

  1. 外部世界(External World): 表示系统外部的各种组件、服务、用户界面等。
  2. 进站端口(Inbound Ports): 与外部世界进行交互的接口,定义系统对外提供的服务。
  3. 出站端口(Outbound Ports): 用于执行业务逻辑过程中需要依赖外部服务的接口。
  4. 驱动适配器(Driving Adapters): 将系统内部的业务逻辑通过端口暴露给外部,可以是 RESTful 接口、RPC 服务等。
  5. 业务逻辑(Business Logic): 系统的核心业务逻辑,独立于具体的技术实现。
  6. 被驱动适配器(Driven Adapters): 实现业务逻辑执行过程中需要使用的端口,与外部服务或数据进行适配。
/** 
 * 主动适配器,将创建文章的Port暴露为Http服务 
*/ 
@RestController 
public class ArticleController { 
    @Resource 
    private ArticleService service;
    @RequestMapping("/create") 
    public void create(DTO dto) { 
        service.create(dto); 
    } 
}
public interface ArticleService {
    /**
     * 端口和适配器架构中的Port,提供创建文章的能力
     * 这是一个进站端口
     * @param dto
     */
    void create(DTO dto);
}

public interface AuthorServiceGateway {
    /**
     *  端口和适配器架构中的Port,查询作者信息
     *  这是一个出站端口
     * @param authorId
     * @return
     */
    AuthorDto queryAuthor(String authorId);
}
/**
 * 被动适配器
 */
public interface AuthorServiceGatewayImpl implements AuthorServiceGateway {

    /**
     * 作家RPC服务
     */
    @Resource
    private AuthorServiceRpc rpc;

    AuthorDto queryAuthor(String authorId) {
        //拼装报文
        AuthorRequest req = this.createRequest(authorId);
        //执行RPC查询
        AuthorResponse res = rpc.queryAuthor();
        //解析查询结果并返回
        return this.handleAuthorResponse(res);
    }
}

应用架构演化

第一步 数据模型与 DAO 层合并

  1. 一一对应的映射: 数据模型通常与数据库表的列一一对应,而DAO层负责数据库的读写操作。将它们合并可以简化数据模型与数据库之间的映射关系,减少重复的代码,提高开发效率。
  2. 领域模型的封装: 数据模型作为贫血模型,主要用于持久层,其属性与数据库列一致,而业务逻辑通常在领域模型中。将数据模型与DAO层合并可以更方便地封装领域模型的业务逻辑,使其更贴近业务需求。
  3. 简化持久化操作: 数据模型通常带有ORM框架的注解,用于指导数据库持久化操作。将数据模型与DAO层合并可以更好地组织这些注解,简化持久化操作的配置和管理。
  4. 业务逻辑与持久层的耦合度: 数据模型通常在业务逻辑中起到承载数据的作用,与数据库交互。将数据模型与DAO层合并有助于降低业务逻辑与持久层之间的耦合度,使业务逻辑更专注于领域模型的操作。
  5. 一体化的设计: 数据模型和DAO层往往紧密相关,将它们合并可以使整体系统的设计更一体化,更容易理解和维护

第一步 Service 层抽取业务逻辑

  1. 原始 Service 层:

    • 业务逻辑和基础设施调用混合在一个 Service 方法中,导致方法臃肿,不易维护。
    • 存在大量的条件判断和基础设施调用。
  2. 抽取业务逻辑形成领域模型:

    • 通过将业务逻辑从 Service 方法中抽取出来,形成领域模型,使得业务逻辑更为清晰。
    • Service 方法变得更加简洁,专注于领域模型的调用和协调。
  3. 领域模型执行业务逻辑:

    • 领域模型内部封装了具体的业务规则和逻辑,Service 方法不再直接操作业务逻辑,而是调用领域模型的方法。
    • 模型自身去执行业务逻辑,Service 方法不再关心具体业务规则。
  4. 简化 Service 方法:

    • Service 方法变得更为简洁,只需要调用领域模型的方法,不再包含复杂的条件判断和详细的业务逻辑。
    • Service 方法主要负责协调领域模型的调用、协同基础设施完成其余操作。
  5. 支持单元测试:

    • 由于业务逻辑被抽取到领域模型中,单元测试变得更为容易,可以专注于测试领域模型的方法,而无需关心 Service 方法中的复杂逻辑和基础设施调用。

image.png

第三步、维护领域对象生命周期

  1. 领域对象的生命周期:

    • 在领域驱动设计(DDD)中,领域对象的生命周期管理是重要的概念。领域对象的生命周期通常包括创建、修改、删除等阶段,需要通过一些手段来保证领域对象在不同阶段的一致性和有效性。
    • 为了维护领域对象的生命周期,通常引入了 Repository(领域仓储)的概念。Repository 负责加载、保存领域对象,并将领域对象的生命周期管理起来。
  2. DomainRepository 接口的定义:

    • DomainRepository 是一个领域仓储的接口,定义了保存和加载聚合根(Aggregate Root)的方法。聚合根是领域模型中负责维护一致性边界的根实体,通常是整个聚合的入口。
    • save 方法用于保存聚合根,load 方法用于加载聚合根。
  3. Repository 的实现:

    • 为了实现 DomainRepository 接口,需要在 DAO 层引入 Domain 包,并在 DAO 层提供 DomainRepository 的实现。
    • 在 Repository 实现中,可以通过调用底层的 Mapper 查询出数据模型,并将其封装为领域模型,然后返回给上层的 Service。
  4. DAO 层的调整:

    • 由于领域模型的引入,DAO 层不再向 Service 返回数据模型,而是返回领域模型。
    • DAO 层也被更名为 Repository,强调其职责是与底层数据存储打交道,并提供领域模型的访问接口。
  5. 隐藏数据库交互细节:

    • 将 Repository 的实现移到 DAO 层,有助于隐藏底层数据库交互的细节,使得 Service 层不需要直接操作数据模型。
    • Service 层专注于领域模型的调用和业务逻辑,而不必关心数据模型和数据库的具体实现。
public interface DomainRepository {

    void save(AggregateRoot root);

    AggregateRoot load(EntityId id);
}

image.png

第四步、泛化抽象

  1. Infrastructure 层的细化:

    • Repository 层被更名为 infrastructure-persistence,即基础设施层持久化包。这一层负责领域模型的加载和持久化,与数据库等底层设施打交道。
    • 考虑到可能引入其他基础设施,比如缓存,所以将其命名为 infrastructure-XXX 的格式,便于区分不同的基础设施支持。
  2. User Interface 层的细化:

    • Controller 层被更名为 user-interface-web,即用户接口层的 Web 包。这一层负责与用户进行交互,包括 RESTful 接口和一系列 Web 相关的拦截器。
    • 考虑到可能有不同协议的用户接口,引入 user-interface-provider 包,用于存放对外提供的 RPC 服务的实现类。
    • 引入 user-interface-subscriber 包,用于存放消费 Kafka 消息并调用 Service 层执行业务逻辑的部分。
  3. Application 层的引入:

    • Service 层被更名为 Application 层或者 Application Service 层。这一层主要负责提供应用服务,协调领域模型和基础设施层完成业务逻辑。
    • 通过这种命名,强调了 Service 层不再包含业务逻辑,而是提供应用服务的调度和协同。
  4. 防腐层的引入:

    • 引入了防腐层的概念,通过 infrastructure-gateway 包来封装对外部系统或资源的访问,以防止外部模型污染本地上下文的领域模型。

image.png

第五步、完整的项目结构

  1. 包的整理:

    • 项目结构经过整理,包括 infrastructure-persistenceuser-interface-webuser-interface-provideruser-interface-subscriberinfrastructure-gatewayApplication 等包,使得不同层次的代码能够清晰地组织在一起。
    • 引入了 infrastructure-XXX 的格式,便于区分不同的基础设施支持。
  2. 启动包的考虑:

    • 考虑到有多个 User Interface,不适合将启动类放在其中。由于 Application 层也不合适,因此决定将启动类放在单独的模块中。
    • 为了明确启动模块的用途,命名为 launcher 模块,这个模块负责提供启动类和项目运行所需的配置文件。
  3. 多个 Launcher 的支持:

    • 引入了 launcher 模块,允许项目存在多个启动类,可以按需引用不同的 User Interface。这样的设计使得项目更加灵活,可以根据需求选择不同的启动方式。
  4. 项目的模块化:

    • 通过引入不同的模块,将不同层次的代码进行模块化,使得每个模块专注于特定的职责。这样的模块化设计有助于提高代码的可维护性和可扩展性。

领域对象的生命周期

image.png

查询过程的类型转换

image.png