DDD领域驱动设计

一、贫血模型

贫血模型(Anemic Domain Model)

贫血模型是一种领域模型,其中领域对象包含很少或没有业务逻辑。是一种面向过程的编程模式,它与面向对象设计的基本思想相悖,将数据和过程结合在一起。

因为贫血模型没有逻辑实现,所以逻辑基本上会放到调用贫血模型的service中,这些service类会转换领域对象的状态。

贫血模型中,domain包含了不依赖于持久化的原子领域逻辑,而组合逻辑在Service层。service :组合服务,也叫事务服务。model:除包含get set方法,还包含原子服务(如获得关联model的id)。dao:数据持久化。

贫血模型的优点

贫血模型的系统层次结构清楚,各层之间单向依赖

领域对象几乎只作传输介质之用,不会影响到层次的划分

贫血模型的缺点

对象状态和行为分离(贫血模型中,对象只有属性,get/set方法,业务逻辑在不在对象类内部),所以一个完整的业务逻辑描述不能在一个类中完成,而是一组相互协作的类共同完成的。

不够面向对象,领域对象只是作为保存状态或者传递状态使用,它是没有生命的,只有数据没有行为的对象不是真正的对象。(在service里面处理所有的业务逻辑,对于细粒度的逻辑处理,通过增加一层Facade达到门面包装的效果)

可复用的颗粒度比较小,代码量膨胀很厉害,很重要的一点是业务逻辑的描述能力较差,一个稍微复杂的业务逻辑,就需要太多类和太多代码去表达。

说明

在使用Spring的时候,通常暗示着你使用了贫血模型

二、充血模型

充血模型(Rich Domain Model)

数据和对应的业务逻辑被封装到同一个类中。因此,这种充血模型满足面向对象的封装特性,是典型的面向对象编程风格。

业务逻辑集中在 Service 类中。基于充血模型,Service 层包含 Service 类和 Domain 类两部分。Domain 是基于充血模型开发的,既包含数据,也包含业务逻辑。而 Service 类变得非常单薄。

充血模型中,绝大多业务逻辑都应该被放在domain里面,包括持久化逻辑,而Service层是很薄的一层,仅仅封装事务和少量逻辑,不和DAO层打交道。service :组合服务也叫事务服务;model:除包含get set方法,还包含原子服务和数据持久化的逻辑

优点

对象自治度很高,表达能力强,适合于复杂的企业业务逻辑实现,可复用程度高。

缺点

如何划分业务逻辑,什么样的逻辑应该放在Domain中,什么样的业务逻辑应该放在Service 中,这是很含糊的。

对象自治度高的结果就是不利于大规模团队分工协作。

失血模型

失血模型中,domain只有属性的get set方法的纯数据类,所有的业务逻辑完全由Service来完成的,没有dao,Service直接操作数据库,进行数据持久化。失血模型service层负担太重,一般不会有这种设计。

涨血模型

胀血模型取消了Service层,只剩下domain object和DAO两层,在domain的domain logic上面封装事务。

为什么贫血模型受欢迎

开发的系统业务可能都比较简单。贫血模型就可以应付,不需要充血模型。大部分是基于 SQL 的 CRUD 操作,我们不需要动脑子精心设计充血模型,贫血模型足以应付这种简单业务的开发工作。除此之外,因为业务比较简单,即便我们使用充血模型,那模型本身包含的业务逻辑也并不会很多,设计出来的领域模型也会比较单薄,跟贫血模型差不多,意义不大。

充血模型的设计要比贫血模型更加有难度。因为充血模型是一种面向对象的编程风格。我们从一开始就要设计好针对数据要暴露哪些操作,定义哪些业务逻辑。而不是像贫血模型那样,我们只需要定义数据,之后有什么功能开发需求,我们就在Service 层定义什么操作,不需要事先做太多设计。

思维固化,转型有成本。基于贫血模型的传统开发模式经历了这么多年,已经深得人心、习以为常。如果转向用充血模型、领域驱动设计,有一定的学习成本、转型成本。

什么场景下使用充血模型

我们平时的开发,大部分都是 SQL 驱动(SQL-Driven)的开发模式。接到一个后端接口的开发需求的时候,就去看接口需要的数据对应到数据库中,需要哪张表或者哪几张表,然后思考如何编写 SQL 语句来获取数据。之后就是定义 Entity、BO、VO,然后模板式地往对应的Repository、Service、Controller 类中添加代码。

业务逻辑包裹在一个大的 SQL 语句中,而 Service 层可以做的事情很少。SQL 都是针对特定的业务功能编写的,复用性差。当我要开发另一个业务功能的时候,只能重新写个满足新需求的 SQL 语句。

对于简单业务系统来说,这种开发方式问题不大。但对于复杂业务系统的开发来说,这样的开发方式会让代码越来越混乱,最终导致无法维护。

在这种开发模式下,我们需要事先理清楚所有的业务,定义领域模型所包含的属性和方法。领域模型相当于可复用的业务中间层。新功能需求的开发,都基于之前定义好的这些领域模型来完成。

————————————————

原文链接:https://blog.csdn.net/sdjnwy/article/details/114866748


三、领域模型


原文链接:https://blog.csdn.net/m0_37583655/article/details/117565641?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522165707513316781683988993%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=165707513316781683988993&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduend~default-4-117565641-null-null.142^v31^control,185^v2^control&utm_term=%E9%A2%86%E5%9F%9F%E9%A9%B1%E5%8A%A8%E8%AE%BE%E8%AE%A1&spm=1018.2226.3001.4187


领域建模模式

他们表述实现与模型间的关系,将分析模型绑定到代码实现模型。主要用于在代码中表述模型元素的模式。

1.实体

实体表述的是领域中的概念,它是由身份而不是属性来定义的。

实体的身份标识在生命周期中保持不变,但其属性会发生变化。实体以身份标识作为唯一凭证,沿着时间轴,记录了实体所有变更事件。

实体的一个实例是产品,一旦产品被生成好,其唯一身份就不会发生变化,但是其描述信息、价格等可以被多次修改。

2.值对象

值对象代表仅通过数据区分的领域元素和概念。用作模型中元素的描述,它不具有唯一标识。

值对象不需要唯一标识,是因为它总是与另一个对象相关联,是在一个特定上下文中被解析的。通常,其生命周期会依附于它的关联对象(在这里,主要是实体对象)。

值对象会当做不变对象来设计,在完成创建后,其状态就不能改变了。

值对象比较好的例子就是现金,你无需关系货币的身份,只关心它的价值。如果有人用一张五美元钞票交换你的五张一美元钞票,也不会改变五美元本身。

3.领域服务

在模型中,领域服务封装了不能自然建模为值对象和实体的逻辑、流程和概念。

它本身不具有身份和状态。它的职责是使用实体和值对象编排业务逻辑。

领域服务的一个好例子是运输成本计算器,只要给出一组拖运货物和重量,它就能计算运输成本。

4.模块

模块主要用于组织和封装相关概念(实体、值对象、领域服务、领域事件等),这样可以简化对较大模型的理解。

应用模块可以在领域模型中促成低耦合和搞内聚的设计。

模块作用于单个领域,用于分解模型规模。子域用于限定领域模型适用范围(有界上下文)。

4.4 对象生命周期模式

相对来说,之前提到的模式重点在于表达领域概念。而对象生命周期模式,有点侧重于技术,用于表示领域对象的创建和持久化。

1.聚合

实体和值对象会相互协作,形成复杂的关联关系。我们需要在满足不变条件的前提下,将其拆分为一个个概念上的整体。通常,面对复杂的对象关系,在执行领域对象操作时,难以保证一致性和并发性。领域驱动设计由聚合模式来确保操作的一致性和事务的并发边界。大模型会通过不变性条件来划分,并组成概念化整体的实体和对象组,这个概念化整体便是聚合。

聚合根之间的关系应该通过保持对另一个聚合根 ID 的引用,而非对对象本身的引用来实现。这一原则有助于保持聚合之间的边界并避免加载不必要的对象。

不变性,是在领域模型中强制实现一致性的规则。无论何时对实体或聚合进行变更都要应用该业务规则。聚合外部的对象只能引用另一个聚合的聚合根,聚合中对象的任何变更都需要通过聚合根来完成。聚合根封装聚合数据并公开行为以对其进行修改。

2.工厂

如果实体或值对象的创建过程非常复杂,可以将其委托给工厂。工厂会确保在领域对象使用之前就满足所有的不变条件。

如果领域对象很简单并且不具有特殊的不变条件,可以使用构造函数代替工厂。当从持久化存储中重建领域对象时,也可以使用工厂。


3.仓库

仓库主要用于持久化一个聚合。将聚合作为原子单元进行处理,因此,仓库操作的最小单元就是聚合,每个聚合会对应一个仓库。

仓库是用来检索和存储聚合的机制,是对基础框架的一种抽象。


4.4 其他模式

1.领域事件

领域事件表示问题空间中发生了一些业务人员关心的事情。主要用于表示领域概念。

使用领域事件主要有以下两种场景:

1.记录模型的变更历史;

2.作为跨聚合通信方式。

2.事件溯源

传统的仅快照式持久化的一个替代项便是事件溯源。作为实体状态存储的替代,可以存储引发该状态的系列事件。存储所有的事件会提高业务的分析能力,不仅可以得知实体当前状态,还可以得知过去任意时点的状态。


4.5 总结

实体

        由唯一标识符定义

        标识符在整个生命周期保存不变

        基于标识符进行相等性检查

        通过方法对属性进行更新

值对象

        描述问题域中的概念和特征

        不具备身份

        不变对象

领域服务

        处理无法放置在实体或值对象中的领域逻辑

        无唯一标识

        无状态服务

模块

        分解、组织和提高领域模型的可读性

        命名空间,降低耦合,提供模型高内聚性

        定义领域对象组间的边界

        封装比较独立的概念,是比聚合、实体等更高层次的抽象

聚合

        将大对象图分解成小的领域对象群,降低技术实现的复杂性

        表示领域概念,不仅仅是领域对象集合

        确定领域一致性边界,确保领域的可靠性

        控制并发边界


工厂

        将对象的使用和构造分离

        封装复杂实体和值对象的创建逻辑

        保障复杂实体和值对象的业务完整性


仓库

        是聚合根在内存中的集合接口

        提供聚合根的检索和持久化需求

        将领域层与基础实施层解耦

        通常不用于报告需求


领域事件

        业务人员所关心的事件,是通用语言的一部分

        记录聚合根的所有变更

        处理跨聚合的通信需求


事件溯源

        使用历史事件记录替换快照存储

        提供对历史状态的查询

5. 领域驱动架构模型

5.1 领域驱动基本架构

5.1.1 分层架构


5.1.2 六边形理论


5.1.3 CQRS架构设计


5.2 领域驱动分层架构


1.用户接口层:面向前端用户提供服务和数据适配。这一层聚集了接口和数据适配相关的功能。

2.应用层:实现服务组合与编排,主要适应业务流程快速变化的需求。这一层聚集了应用服务和时间订阅相关的功能。

3.领域层:实现领域模型的核心业务逻辑。这一层聚集了领域模型的聚合、聚合根、实体、值对象、领域服务和领域事件,通过个领域对象的协同和组合形成领域模型的核心业务能力。

4.基础设施层:它贯穿所有层,为各层提供基础资源服务。这一层聚集了各种底层资源相关的服务和能力。

5.3 服务调用


5.4 服务封装与组合


5.5 领域架构对应关系


6. 领域驱动落地框架

6.1 leave-sample

中台架构与实现 DDD和微服务,清晰地提供了从战略设计到战术设计以及代码落地。

leave-sample地址:https://gitee.com/serpmelon/leave-sample

6.2 dddbook

阿里技术专家详解DDD系列,例子精炼,项目代码结构与rdfa相似,极具参考价值。

dddbook地址:https://developer.aliyun.com/article/719251

6.3 Xtoon

xtoon-boot是基于领域驱动设计(DDD)并支持SaaS平台的单体应用开发脚手架。重点研究如何应用。xtoon-boot提供了完整落地方案和企业级手脚架;

gitee地址:https://gitee.com/xtoon/xtoon-boot

github地址:https://github.com/xtoon/xtoon-boot

6.4 DDD Lite

DDD 领域驱动设计微服务简化版,简洁、高效、值得重点研究,主要问题是持久化采用的JPA,担心技术人员不熟悉,理论篇。

gitee地址:https://gitee.com/litao851025/geekhalo-ddd

快速入门:https://segmentfault.com/a/1190000018464713

快速构建新闻系统:https://segmentfault.com/a/1190000018254111

6.5 ruoyi_cloud

若依快速开发平台,以该项目建立对阳光智采和rdfa的技术框架基准线。

gitee地址:https://gitee.com/y_project/RuoYi-Cloud

6.6 Cola框架

cola框架是阿里大佬张建飞(Frank) 基于DDD构建的平台应用框架。“让COLA真正成为应用架构的最佳实践,帮助广大的业务技术同学,脱离酱缸代码的泥潭!”

csdn地址:https://blog.csdn.net/significantfrank/article/details/110934799

6.7 Axon Framework

Axon Framework 是用来帮助开发人员构建基于命令查询责任分类(Command Query Responsibility Segregation: CQRS)设计模式的可伸缩、可扩展和可维护应用程序的框架。你只需要把工作重心放在业务逻辑的设计上。通过一些 Annotation ,Axon 使得你的代码和测试分离。

https://www.oschina.net/p/axon

https://www.jianshu.com/p/15484ed1fbde

7. 领域驱动实践

7.1 贫血模型和充血模型

1.贫血模型概念

贫血模型,所谓的贫血模型是在定义对象时,指定以对象的属性信息,却没有对象的行为信息,比如,定义Employee对象会包含id,name,age,sex,role,phone等信息,最后再通过添加一些对象属性的get/set方法来赋值取值操作。

这些贫血对象在设计之初就被定义为只能包含数据,不能加入领域逻辑;所有的业务逻辑是放在所谓的业务层(xxxService, xxxManager对象中),需要使用这些模型来传递数据。

2.充血模型概念

充血模型,在定义对象时不但包含对象的属性信息,还包括对象的行为信息。所以充血模型是一种有行为的模型,模型中状态的改变只能通过模型上的行为来触发,同时所有的约束及业务逻辑都收敛在模型上。

3.贫血模型和充血模型的区别

贫血模型是事务脚本模式,贫血模型相对简单,模型上只有数据没有行为,业务逻辑由xxxService、xxxManger等类来承载,相对来说比较直接,针对简单的业务,贫血模型可以快速的完成交付,但后期的维护成本比较高,很容易变成我们所说的面条代码。

充血模型是领域模型模式,充血模型的实现相对比较复杂,但所有逻辑都由各自的对象来负责,职责比较清晰,方便后期的迭代与维护。充血模型更加符合现实中的对象,因为一个员工在现实世界里不只有姓名,年龄,电话等,还可以工作,吃饭,睡觉等行为,只有属性信息的对象不是一个完整的对象。

面向对象设计主张将数据和行为绑定在一起也就是充血模型,而贫血领域模型则更像是一种面向过程设计,很多人认为这些贫血领域对象是真正的对象,从而彻底误解了面向对象设计的涵义。

贫血领域模型的根本问题是,它引入了领域模型设计的所有成本,却没有带来任何好处。最主要的成本是将对象映射到数据库中,从而产生了一个O/R(对象关系)映射层。只有当你充分使用了面向对象设计来组织复杂的业务逻辑后,这一成本才能够被抵消。如果将所有行为都写入到Service对象,那最终你会得到一组事务处理脚本,从而错过了领域模型带来的好处。而且当业务足够复杂时, 你将会得到一堆爆炸的事务处理脚本。

4.贫血模型与充血模型案例验证

员工贫血模型

@Data

public class Person {

    /**

    * 姓名

    */

    private String name;

    /**

    * 年龄

    */

    private Integer age;

    /**

    * 生日

    */

    private Date birthday;

    /**

    * 当前状态

    */

    private Stauts stauts;

}

public class PersonServiceImpl implements PersonService {

    public void sleep(Person person) {

        person.setStauts(SleepStatus.get());

    }

public void setAgeByBirth(Person person) {

        Date birthday = person.getBirthday();

        if (currentDate.before(birthday)) {

            throw new IllegalArgumentException("The birthday is before Now,It's unbelievable");

        }

        int yearNow = cal.get(Calendar.YEAR);

        int dayBirth = bir.get(Calendar.DAY_OF_MONTH);

        /*大概计算, 忽略月份等,年龄是当前年减去出生年*/

        int age = yearNow - yearBirth;

        person.setAge(age);

    }

}

public class WorkServiceImpl implements WorkService{

    public void code(Person person) {

        person.setStauts(CodeStatus.get());

    }

}

这一段代码就是贫血对象的处理过程,Person类, 通过PersonService、WorkingService去控制Person的行为,第一眼看起来像是没什么问题,但是真正去思考整个流程。WorkingService, PersonService到底是什么样的存在?与真实世界逻辑相比, 过于抽象。基于贫血模型的传统开发模式,将数据与业务逻辑分离,违反了 OOP 的封装特性,实际上是一种面向过程的编程风格。但是,现在几乎所有的 Web 项目,都是基于这种贫血模型的开发模式,甚至连 Java Spring 框架的官方 demo,都是按照这种开发模式来编写的。

面向过程编程风格有种种弊端,比如,数据和操作分离之后,数据本身的操作就不受限制了。任何代码都可以随意修改数据。

员工充血模型

@Data

public class Person extends Entity {

    /**

    * 姓名

    */

    private String name;

    /**

    * 年龄

    */

    private Integer age;

    /**

    * 生日

    */

    private Date birthday;

    /**

    * 当前状态

    */

    private Stauts stauts;

    public void code() {

        this.setStauts(CodeStatus.get());

    }

    public void sleep() {

        this.setStauts(SleepStatus.get());

    }

    public void setAgeByBirth() {

        Date birthday = this.getBirthday();

        Calendar currentDate = Calendar.getInstance();

        if (currentDate.before(birthday)) {

            throw new IllegalArgumentException("The birthday is before Now,It's unbelievable");

        }

        int yearNow = currentDate.get(Calendar.YEAR);

        int yearBirth = birthday.getYear();

        /*粗略计算, 忽略月份等,年龄是当前年减去出生年*/

        int age = yearNow - yearBirth;

        this.setAge(age);

    }     

}

贫血模型和充血模型的区别

/**

* 贫血模型

*/

public class Client {

    @Resource

    private PersonService personService;

    @Resource

    private WorkService workService;

    public void test() {

        Person person = new Person();

        personService.setAgeByBirth(person);

        workService.code(person);

        personService.sleep(person);

    }

}

/**

* 充血模型

*/

public class Client {

    public void test() {

        Person person = new Person();

        person.setAgeByBirth();

        person.code();

        person.sleep();

    }

}

上面两段代码很明显第二段的认知成本更低, 这在满是Service,Manage 的系统下更为明显,Person的行为交由自己去管理, 而不是交给各种Service去管理。

7.2 DDD实现银行转账案例

银行转账事务脚本实现在事务脚本的实现中,关于在两个账号之间转账的领域业务逻辑都被写在了MoneyTransferService的实现里面了,而Account仅仅是getters和setters的数据结构,也就是我们说的贫血模型:

public class MoneyTransferServiceTransactionScriptImpl implements MoneyTransferService {

    private AccountDao accountDao;

    private BankingTransactionRepository bankingTransactionRepository;

    //...

    @Override

    public BankingTransaction transfer(

            String fromAccountId, String toAccountId, double amount) {

        Account fromAccount = accountDao.findById(fromAccountId);

        Account toAccount = accountDao.findById(toAccountId);

        //. . .

        double newBalance = fromAccount.getBalance() - amount;

        switch (fromAccount.getOverdraftPolicy()) {

            case NEVER:

                if (newBalance < 0) {

                    throw new DebitException("Insufficient funds");

                }

                break;

            case ALLOWED:

                if (newBalance < -limit) {

                    throw new DebitException(

                            "Overdraft limit (of " + limit + ") exceeded: " + newBalance);

                }

                break;

        }

        fromAccount.setBalance(newBalance);

        toAccount.setBalance(toAccount.getBalance() + amount);

        BankingTransaction moneyTransferTransaction =

                new MoneyTranferTransaction(fromAccountId, toAccountId, amount);

        bankingTransactionRepository.addTransaction(moneyTransferTransaction);

        return moneyTransferTransaction;

    }

}

上面的代码大家看起来应该比较眼熟,因为目前大部分系统都是这么写的。其实我们是有办法做的更优雅的,这种优雅的方式就是领域建模,唯有掌握了这种优雅你才能实现从工程师向应用架构的转型。同样的业务逻辑,接下来就让我们看一下用DDD是怎么做的。银行转账领域模型实现如果用DDD的方式实现,Account实体除了账号属性之外,还包含了行为和业务逻辑,比如debit( )和credit( )方法。

// @Entity

public class Account {

    // @Id

    private String id;

    private double balance;

    private OverdraftPolicy overdraftPolicy;

    //. . .

    public double balance() {

        return balance;

    }

    public void debit(double amount) {

        this.overdraftPolicy.preDebit(this, amount);

        this.balance = this.balance - amount;

        this.overdraftPolicy.postDebit(this, amount);

    }

    public void credit(double amount) {

        this.balance = this.balance + amount;

    }

}

而且透支策略OverdraftPolicy也不仅仅是一个Enum了,而是被抽象成包含了业务规则并采用了策略模式的对象.

public interface OverdraftPolicy {

    void preDebit(Account account, double amount);

    void postDebit(Account account, double amount);

}

public class NoOverdraftAllowed implements OverdraftPolicy {

    public void preDebit(Account account, double amount) {

        double newBalance = account.balance() - amount;

        if (newBalance < 0) {

            throw new DebitException("Insufficient funds");

        }

    }

    public void postDebit(Account account, double amount) {

    }

}

public class LimitedOverdraft implements OverdraftPolicy {

    private double limit;

    //...

    public void preDebit(Account account, double amount) {

        double newBalance = account.balance() - amount;

        if (newBalance < -limit) {

            throw new DebitException(

                    "Overdraft limit (of " + limit + ") exceeded: " + newBalance);

        }

    }

    public void postDebit(Account account, double amount) {

    }

}

//而Domain Service只需要调用Domain Entity对象完成业务逻辑即可。

public class MoneyTransferServiceDomainModelImpl implements MoneyTransferService {

    private AccountRepository accountRepository;

    private BankingTransactionRepository bankingTransactionRepository;

    //...

    @Override

    public BankingTransaction transfer(

            String fromAccountId, String toAccountId, double amount) {

        Account fromAccount = accountRepository.findById(fromAccountId);

        Account toAccount = accountRepository.findById(toAccountId);

        //. . .

        fromAccount.debit(amount);

        toAccount.credit(amount);

        BankingTransaction moneyTransferTransaction =

                new MoneyTranferTransaction(fromAccountId, toAccountId, amount);

        bankingTransactionRepository.addTransaction(moneyTransferTransaction);

        return moneyTransferTransaction;

    }

}

通过上面的DDD重构后,原来在事务脚本中的逻辑,被分散到Domain Service,Domain Entity和OverdraftPolicy三个满足SOLID的对象中。

————————————————

版权声明:本文为CSDN博主「靖节先生」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/m0_37583655/article/details/117565641

你可能感兴趣的:(DDD领域驱动设计)