第三章:DDD四种模型详解

上一章:《第二章:Domain Primitive》
在上一章中我们通过一些案例分析了解到了在设计我们的对象和功能时如何去定义我们的DP,本章带着大家系统的了解一下在我们开发时经常遇到的四种领域模型


四种领域模型分别是: 失血模型贫血模型充血模型胀血模型,它们是对领域驱动设计(DDD)中领域对象(Domain Object)结构和职责的不同划分方式,它们主要依据领域对象是否包含以及包含多少业务逻辑来进行区分。

1.失血模型

在失血模型中,领域对象仅作为数据容器存在,只包含属性的定义和对应的 getter/setter 方法

所有业务逻辑、规则验证、数据转换等操作都被移至业务逻辑层(如服务类)或应用层来处理。

失血模型的领域对象就像一个“骨架”,没有任何行为(方法),仅用于数据传递和存储,因此被称为“失血”,因为它剥离了领域对象应有的业务内涵。

上面说了这么多,失血模型的本质:一个对象仅包含数据成员(即属性),而缺乏行为(即方法)

就像下面这段代码:

public class Article implements Serializable {
    private Integer id;
    private String title;
    private Integer classId;
    private Integer authorId;
    private String authorName;
    private String content;
    private Date pubDate;
    //getter/setter/toString
}

public interface ArticleDao {
     public Article getArticleById(Integer id);
     public Article findAll();
     public void updateArticle(Article article);
}

这段代码展示了两个Java类:

  • Article 类作为领域实体(Domain Entity)
  • ArticleDao 接口作为数据访问对象(Data Access Object)

1.Article 类:

  • 它包含了表示文章的基本属性,如 id, title, classId, authorId, authorName, content, pubDate。
  • 类中没有实现任何与这些属性相关的行为(业务逻辑方法),如校验标题的长度、计算文章摘要、处理作者信息等。

2.ArticleDao 接口:

  • 定义了一系列与数据库交互的方法,如通过 id 获取文章 (getArticleById)、获取所有文章 (findAll) 以及更新文章 (updateArticle)。
  • 这些方法负责数据的持久化操作,但不涉及文章实体本身的业务逻辑。

结合上述分析,可以得出以下结论:

失血模型特征:

  • Article 类只包含数据成员(属性),而没有或几乎没有实现与业务逻辑相关的操作(方法)。它主要作为数据载体,对外提供属性的读写访问。
  • 业务逻辑被分离到了其他组件中,如 ArticleDao 接口中定义的数据访问操作,或者可能存在于其他服务、控制器等类中。

这种设计模式符合“失血模型”的典型特点,因为实体类仅作为数据容器,其业务逻辑和行为被转移到了外部组件,导致实体本身缺乏表达领域特性的能力。

在领域驱动设计(DDD)的视角下,这种模型往往被认为不利于捕获和表达领域知识,因为它没有封装领域内的行为,使得领域逻辑散落在应用程序的不同层次或组件中,增加了理解和维护系统的复杂性。

理想情况下,领域实体应该尽可能地包含与其相关的业务逻辑,使其成为具有“生命力”的模型,即所谓的“充血模型”

在这种设计中,Article 类可能会包含诸如校验标题是否合法、计算摘要、关联作者信息等方法,而数据访问逻辑则通常由专门的仓储(Repository)类来负责,而不是直接暴露在实体类的接口中。

2.贫血模型

贫血模型相对于失血模型有所改进,它允许领域对象包含一些简单的业务逻辑,但通常不涉及与数据库或其他外部资源交互的复杂操作。

也就是说,贫血模型的领域对象可能拥有验证属性值计算派生属性执行某些轻量级业务规则的方法

然而,那些依赖于持久层的业务逻辑,如数据持久化、事务管理等,依然保留在服务层或数据访问层实现。虽然比失血模型多了一些业务能力,但由于缺乏关键的业务过程,仍被认为“贫血”,即业务能力不足。

简单来说,就是 Domain Object 包含了不依赖于持久化的领域逻辑,而那些依赖持久化的领域逻辑被分离到 Service 层

public class Article implements Serializable {
    private Integer id;
    private String title;
    private Integer classId;
    private Integer authorId;
    private String authorName;
    private String content;
    private Date pubDate;
    //getter/setter/toString
    //判断是否是热门分类(假设等于57或102的类别的文章就是热门分类的文章)
    public boolean isHotClass(Article article){
        return Stream.of(57,102)
            .anyMatch(classId -> classId.equals(article.getClassId()));
    }
    //更新分类,但未持久化,这里不能依赖Dao去操作实体化
    public Article changeClass(Article article, ArticleClass ac){
        return article.setClassId(ac.getId());
    }
}

@Repository("articleDao")
public class ArticleDaoImpl implements ArticleDao{
    @Resource
    private ArticleDao articleDao;
    public void changeClass(Article article, ArticleClass ac){
        article.changeClass(article, ac);
        articleDao.update(article)
    }
}

这段代码被描述为“贫血模型设计”主要是因为它体现了以下几个特点,符合贫血模型(Anemic Domain Model)的概念:

  • 缺乏业务逻辑: 在Article类中,虽然定义了与文章相关的属性,如标题、分类ID、作者ID等,但其方法仅限于简单的数据获取(getter)和设置(setter),以及一些辅助性质的方法(如isHotClass()用于判断是否属于热门分类,changeClass()仅更新分类ID)。这些方法并没有体现复杂的业务规则或逻辑。真正的业务逻辑,如将更改后的文章状态持久化到数据库(update()操作),是在外部的ArticleDaoImpl类中实现的。

  • 关注数据而非行为: Article类更像一个纯粹的数据容器,它的主要职责是存储和提供文章相关的属性值。
    尽管有isHotClass()和changeClass()方法,但它们的功能相对简单,没有涉及业务流程控制、规则校验等核心业务逻辑。这类模型更侧重于数据本身,而非围绕数据的行为和交互。

  • 依赖外部服务进行数据处理: 当需要对文章数据进行实质性修改(如更新分类)时,实际的处理工作是在ArticleDaoImpl这个数据访问对象(DAO)中完成的。
    changeClass()方法仅在Article对象上进行局部状态变更,而将变更持久化的责任交给了外部的articleDao.update(article)调用。

这意味着业务逻辑分散在了领域模型(Article)之外,没有内聚在领域对象内部

这段代码中的Article类作为领域模型,只包含了数据属性和少量辅助方法,缺乏丰富的业务逻辑,其行为主要依赖于外部的ArticleDaoImpl类来实现。

这种设计模式遵循了贫血模型的特点:领域对象专注于数据表示,而业务逻辑则存在于单独的服务或数据访问层中

这样做的优缺点

  • 优点:各层单向依赖,结构清晰。
  • 缺点:Domain Object 的部分比较紧密依赖的持久化 Domain Logic 被分离到 Service 层,显得不够 OO,Service 层过于厚重

3.充血模型

充血模型强调将完整的业务逻辑封装在领域对象内部,使其成为业务行为的主要执行者

在这种模型下,领域对象不仅包含属性,还拥有丰富的业务方法来表达领域行为,如复杂的验证逻辑、业务规则的执行、与其他领域对象的协作等。

充血模型的领域对象是自主的、有生命力的实体,能够代表领域内的一个具体概念并完全负责与其相关的所有业务操作。充血模型鼓励将业务逻辑紧密地与数据结合,从而形成高度内聚的业务组件,有助于保持领域模型的清晰性和完整性。

充血模型和第二种模型差不多,区别在于业务逻辑划分,将绝大多数业务逻辑放到 Domain 中,Service 是很薄的一层,封装少量业务逻辑,并且不和 DAO 打交道

Service (事务封装) —> Domain Object <—> DAO

我看很多人给出以下代码所示的充血模型改进方案

public class Article implements Serializable {
    @Resource
    private static ArticleDao articleDao;
    private Integer id;
    private String title;
    private Integer classId;
    private Integer authorId;
    private String authorName;
    private String content;
    private Date pubDate;
    //getter/setter/toString
    //使用articleDao进行持久化交互
    public List<Article> findAll(){
        return articleDao.findAll();
    }
    //判断是否是热门分类(假设等于57或102的类别的文章就是热门分类的文章)
    public boolean isHotClass(Article article){
        return Stream.of(57,102)
            .anyMatch(classId -> classId.equals(article.getClassId()));
    }
    //更新分类,但未持久化,这里不能依赖Dao去操作实体化
    public Article changeClass(Article article, ArticleClass ac){
        return article.setClassId(ac.getId());
    }
}

实际上,上面这段代码仍然不符合充血模型(Rich Domain Model)的设计原则。原因如下:

依赖注入不适用于静态成员: 在Article类中,尝试通过@Resource注解为articleDao静态成员变量进行依赖注入。

然而,依赖注入框架通常不会处理静态字段的注入,因为它们属于类级别,而非实例级别。

此外,将数据访问对象(DAO)作为领域模型(如Article)的静态成员违背了面向对象设计的原则,使得领域模型与数据访问细节耦合在一起。

业务逻辑与数据访问混杂: 即使忽略上述静态注入的问题,Article类中包含的findAll()方法直接调用了articleDao进行数据查询。这将数据访问逻辑(通常属于服务层或数据访问层)与领域模型(Article)紧密耦合,破坏了职责分离。

充血模型中,领域对象应专注于封装业务逻辑和数据,而不直接负责数据的持久化操作。

业务逻辑依然匮乏: 类似于之前的代码片段,Article类中的isHotClass()和changeClass()方法同样仅提供了简单的数据检查和属性修改,并未体现复杂的业务规则或逻辑。这表明该领域模型依然偏向于数据的承载,而非业务行为的封装。

综上所述,这段代码不仅存在技术实现上的问题(如无效的依赖注入),而且在设计层面依然不符合充血模型的要求。

领域对象(Article)既没有清晰地封装丰富的业务逻辑,又试图直接承担数据访问的责任。因此,它不能被视为充血模型设计。 若要实现充血模型,应将业务逻辑充分内聚在Article类中,避免直接依赖数据访问层,并确保其方法能够反映领域内的复杂行为和规则。

现在对上面这段代码进行修改:

import java.util.Date;
import java.util.List;

public class Article implements Serializable {

    private Integer id;
    private String title;
    private ArticleClass classification;
    private Author author;
    private String content;
    private Date publicationDate;

    // Getter and Setter methods...

    // Constructor(s)...

    // Business logic methods:

    /**
     * Checks if the article belongs to a hot category.
     * A hot category is assumed to have an ID of 57 or 102.
     */
    public boolean isHotCategory() {
        return classification.getId().equals(57) || classification.getId().equals(102);
    }

    /**
     * Changes the article's classification, applying any necessary business rules.
     * This method assumes that the new classification is already validated.
     */
    public void changeClassification(ArticleClass newClassification) {
        this.classification = newClassification;
        // Optionally, apply additional business rules or trigger events here.
    }

    /**
     * Validates the content based on specific criteria (e.g., length, keywords).
     * Returns true if the content is valid, false otherwise.
     */
    public boolean validateContent() {
        // Implement your content validation logic here...
    }

    // Other business logic methods as needed...

    // Utility methods (optional):

    /**
     * Retrieves all articles from the repository.
     * Note: This method is typically not part of a domain model,
     * but included here for demonstration purposes.
     */
    public static List<Article> findAllArticles(ArticleRepository repository) {
        return repository.findAll();
    }
}

主要改进点如下:

  • 引入关联对象: 使用ArticleClass和Author对象代替原先的类ID和作者ID,这样可以直接操作关联对象,更好地封装领域知识。同时,这有助于减少领域模型与外部数据访问层的耦合。
  • 添加业务逻辑方法: 添加了如isHotCategory()、changeClassification()、validateContent()等方法,它们体现了领域内的业务规则和逻辑。这些方法封装在Article类内部,使领域模型具备了处理业务需求的能力。
  • 移除数据访问逻辑: 原始代码中的findAll()方法直接涉及数据访问,不符合充血模型的原则。
    在这里,我将其改为静态方法findAllArticles(ArticleRepository repository),并接收一个ArticleRepository参数。这种方法演示了如何通过依赖注入(传递ArticleRepository)来实现数据访问,而不是将数据访问逻辑硬编码在领域模型中。实际上,在真实应用中,这样的查询方法通常应位于服务层或使用查询对象(Query Object)模式实现。

通过以上调整,Article类现在不仅包含了与文章相关的属性,还封装了丰富的业务逻辑,符合充血模型的设计要求。请注意,实际应用中可能还需要根据具体业务需求进一步完善领域模型,添加更多业务逻辑方法。

读到此处想必大家似曾相识,因为在上一章的DP设计原则中也进行一样的改进

优点:

  • 更加符合 OO 的原则;
  • Service 层很薄,只充当 Facade 的角色,不和 DAO 打交道。

缺点:

  • DAO 和 Domain Object 形成了双向依赖,复杂的双向依赖会导致很多潜在的问题。
  • 如何划分 Service 层逻辑和 Domain 层逻辑是非常含混的,在实际项目中,由于设计和开发人员的水平差异,可能 导致整个结构的混乱无序。

4.胀血模型

胀血模型这一术语相对较少见,且在文献中可能存在不同的理解和表述。

一种可能的解释是,胀血模型描述的是当领域对象过度承载业务逻辑,以至于变得庞大、复杂、难以理解和维护的情况。

在这种模型中,领域对象可能承担了过多的职责,包括但不限于复杂的业务流程、数据访问逻辑、甚至是跨领域的操作。

由于包含了过多的上下文无关或过于具体的细节,胀血模型的领域对象可能会违反“单一职责原则”,导致代码的耦合度高、可测试性差、扩展困难。胀血模型通常被视为一种需要避免的设计反模式。

这种模型的设计基于充血模型的第三个缺点,有同学提出,干脆取消 Service 层,只剩下 Domain Object 和 DAO 两层,在 Domain Object 的 Domain Logic 上面封装事务。

Domain Object (事务封装,业务逻辑) <—> DAO

似乎 Ruby on rails 就是这种模型,它甚至把 Domain Object 和 DAO 都合并了。

这样做的优缺点:

  • 简化了分层
  • 也算符合 OO

该模型缺点:

  • 很多不是 Domain Logic 的 Service 逻辑也被强行放入 Domain Object ,引起了 Domain Object 模型的不稳定;
  • Domain Object 暴露给 Web 层过多的信息,可能引起意想不到的副作用。

综上所述,失血模型、贫血模型、充血模型分别代表了领域对象职责从少到多的递增,而胀血模型则警示了过度封装业务逻辑可能导致的负面效应。

在实际的领域驱动设计实践中,通常倾向于采用充血模型以保持领域模型的清晰性和业务逻辑的一致性,同时要警惕防止演化成胀血模型,确保领域对象的职责适当、内聚且易于管理。

  • 20
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
DDD(领域驱动设计)是一种软件开发方法论,它将业务领域作为软件开发的核心,重视业务领域中的实体、值对象、聚合根等概念的建模。 在DDD中,数据领域模型是一个重要的概念,它是指将业务领域中的实体、值对象、聚合根等概念转化为数据库中的表、字段、关系等概念的过程。为了方便管理和维护数据领域模型,我们需要为其定义一个项目目录结构。 以下是DDD数据领域模型项目目录结构的详细解释: ``` data-model/ |-- src/ | |-- main/ | | |-- java/ | | | |-- com/ | | | | |-- yourcompany/ | | | | | |-- datamodel/ | | | | | | |-- entity/ | | | | | | | |-- YourEntity.java | | | | | | |-- value/ | | | | | | | |-- YourValueObject.java | | | | | | |-- repository/ | | | | | | | |-- YourRepository.java | | | | | | |-- service/ | | | | | | | |-- YourService.java | | | | | | |-- event/ | | | | | | | |-- YourEvent.java | | | | | | |-- exception/ | | | | | | | |-- YourException.java | | | | | | |-- factory/ | | | | | | | |-- YourFactory.java | | | | | | |-- mapper/ | | | | | | | |-- YourMapper.java | | | | | | |-- specification/ | | | | | | | |-- YourSpecification.java | | | | | | |-- eventlistener/ | | | | | | | |-- YourEventListener.java | | | | | | |-- util/ | | | | | | | |-- YourUtil.java | | | | | |-- config/ | | | | | | |-- DataSourceConfig.java | | | | | | |-- MybatisConfig.java | | | | | | |-- EventListenerConfig.java | | | | | | |-- SpringConfig.java | | | | | | |-- SwaggerConfig.java | | | | | |-- DatamodelApplication.java | | |-- resources/ | | | |-- db/ | | | | |-- migration/ | | | | | |-- V1__create_table.sql | | | |-- application.yml | |-- test/ | | |-- java/ | | | |-- com/ | | | | |-- yourcompany/ | | | | | |-- datamodel/ | | | | | | |-- YourTest.java | | |-- resources/ | | | |-- application.yml |-- README.md |-- LICENSE ``` 解释如下: - `data-model`:数据领域模型项目的根目录。 - `src`:源代码目录。 - `main`:主目录,包含了项目的主要代码和资源文件。 - `java`:Java代码目录。 - `com`:公司或组织的根包名。 - `yourcompany`:公司或组织的名称,根据实际情况修改。 - `datamodel`:数据领域模型的包名。 - `entity`:实体类包名。 - `YourEntity.java`:实体类文件,根据实际情况修改。 - `value`:值对象包名。 - `YourValueObject.java`:值对象文件,根据实际情况修改。 - `repository`:仓储接口包名。 - `YourRepository.java`:仓储接口文件,根据实际情况修改。 - `service`:服务类包名。 - `YourService.java`:服务类文件,根据实际情况修改。 - `event`:事件类包名。 - `YourEvent.java`:事件类文件,根据实际情况修改。 - `exception`:异常类包名。 - `YourException.java`:异常类文件,根据实际情况修改。 - `factory`:工厂类包名。 - `YourFactory.java`:工厂类文件,根据实际情况修改。 - `mapper`:映射器包名。 - `YourMapper.java`:映射器文件,根据实际情况修改。 - `specification`:规约包名。 - `YourSpecification.java`:规约文件,根据实际情况修改。 - `eventlistener`:事件监听器包名。 - `YourEventListener.java`:事件监听器文件,根据实际情况修改。 - `util`:工具类包名。 - `YourUtil.java`:工具类文件,根据实际情况修改。 - `config`:配置文件目录。 - `DataSourceConfig.java`:数据源配置文件,根据实际情况修改。 - `MybatisConfig.java`:Mybatis配置文件,根据实际情况修改。 - `EventListenerConfig.java`:事件监听器配置文件,根据实际情况修改。 - `SpringConfig.java`:Spring配置文件,根据实际情况修改。 - `SwaggerConfig.java`:Swagger配置文件,根据实际情况修改。 - `DatamodelApplication.java`:数据领域模型项目的启动类文件,根据实际情况修改。 - `resources`:资源文件目录。 - `db`:数据库脚本目录。 - `migration`:数据库迁移脚本目录。 - `V1__create_table.sql`:创建表的SQL脚本文件,根据实际情况修改。 - `application.yml`:应用程序配置文件,根据实际情况修改。 - `test`:测试代码目录。 - `YourTest.java`:测试类文件,根据实际情况修改。 - `README.md`:说明文件。 - `LICENSE`:许可证文件。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ZNineSun

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值