领域驱动设计(五) - 战术设计 - 【3/3】聚合与模型的构建方法

news2024/11/25 6:39:18

这一小章主要阐述下如何组织上述分析后的模型。

使用聚合(Aggergate)进行建模,并且在设计中结合工厂(Factory)和资源库(Repositiory,注意Orm映射出的持久化对象不是领域模型的范围,在后续章节中会详细阐述这两者的区别),这样就能够把生命周期做为一个原子单元进行模型对象的操作。

通常,Aggergate用于划分范围,这个范围内的模型元素在生命周期各个阶段都应维护其固定规则和事务一致性 。Factory和Repositiory在Aggergate基础上进行操作,将特定生命周期转换的复杂性封装起来。下图是一张整体构建思路:

  • 聚合Aggregate:定义清晰的关联项和边界
  • 工厂Factory:创建和重建复杂对象,并用aggergate来封装其内部结构
  • 资源库Repository:提供检索和持久化,可以委托给Factory来创建对象

一、聚合

聚合的识别和创建已在《快速开始-战术设计》章节详细描述过,此处不在累述,在本小节中笔者试图从概念角度讲述这个主题,希望可以加深读者对聚合的理想。首先需要强调的一点,在DDD中聚合是一个可选的模块,实现时需要在代码复杂性和是否应用聚合之间做出权衡。

聚合通过定义清晰的关系避免混乱和保持模型的高内聚。每个聚合都包含一个特定的根实体(聚合根)和边界(内部都有什么属性)。

  • 对外引用:聚合根是唯一允许外部对象保持对聚合引用的对象,通过他来控制外界对内部成员的访问,不能根引用绕过它来修改内部对象。这种设计有利于维护业务规则的完整性,设计时尽量减少非必要的关联,有助于对象间的遍历和复杂性;
  • 聚合内部:在聚合内部对象之间则可以相互引用,不同的对象可以有唯一的标识,但只有在内部才能区分开,对外部是不可见的。

1.1、创建聚合需遵循的原则

  • 在聚合边界内保护业务规则不变性:由业务最终决定,技术处理上要保证事务的一致性;
  • 聚合要设计的小巧:职责单一性,这是一个决策问题;
  • 只能通过标识符引用其它聚合:出于容器的考虑,聚合间最好是外键引用,而不是实体引用,同时尽量消除冗余字段,这也是一个决策问题;
  • 使用最终一致性更新其它聚合:避免复杂性,最常用的是异步和消息机制;

1.2、程序实现时的注意事项

1.2.1、创建行为

  • 迪米特法则:强调了最小知识原则,任何对象的任何方法只能调用:1、对象本身;2、所传入的参数对象;3、它所创建的对象;4、自身包含的对象的方法;
  • 告诉而非询问原则:客户端不应该先询问执行端再决定是否进行某种操作,这个询问应该由执行端来决定,它只需告诉客户端0和1即可;

1.2.2、并发控制

  • 为聚合创建版本号,但一般没有必要,比较复杂。

1.2.3、避免依赖注入

  • 这主要是出于性能考虑,不要在聚合中注意资源库和领域服务;可在调用聚合命令之前先查询此聚合命令执行的前置条件,然后再执行的方式来解决。如果需要其它引用,可把其它聚合以参数的方式传入到聚合中的方法中;

二、工厂

如果创建实体或聚合的过程过于复杂,在程序设计时有必要将创建职责剥离出来,然后分配给一个单独的对象为领域对象减负,该对象本身并不承担领域模型中的职责,但是依然是领域设计的一部分。由此对象提供一个创建对象的方法/接口,该接口封装了所有创建对象的复杂操作过程,一次性地创建整个聚合,并且确保它的不变条件得到满足。此过程还要做到用户无感知。

这个被剥离出来的对象就是工厂,概念上讲工厂不受限界上下文的约束;所以不建议创建单独的Factory类,最好的方式是在领域服务(受限界上下文限制)或聚合中添加工厂方法。另外工厂方法本身是可替换的,所以在工厂方法实现时不建议做防御编程以及任何业务规则 ,这个职责应该由各个领域模型来分担,但被创建对象的完整性检查应该由工厂负责。使用用工厂方法除了能保证安全性等,还能简化客户端调用。通用的工厂设计如下:

2.1、创建工厂方法的方式

这里说的工厂不是软件设计模式中的工厂模式,是一种概念其意义在于解耦,只要满足这个条件的实现都可以认为是工厂,工厂的实现需要满足两个条件:创建方法必须是原子的;创建的东西最好是具体的;工厂一般有三种创建方式:

  • 工厂方法:
  • 抽象工厂:
  • 构建函数:

2.2、聚合中的工厂方法

一般适用于创建被引用的聚合,且创建过程中与其它上下文没有太多关系;下面是一个简单的示例,在Calendar聚合类中创建CaendarEntry聚合。但聚合里不建议访问领域服务;

public class Calendar extends EventSourcedRootEntity {
    public CalendarEntry scheduleCalendarEntry(
            CalendarIdentityService aCalendarIdentityService,
            String aDescription,
            String aLocation,
            Owner anOwner,
            TimeSpan aTimeSpan,
            Repetition aRepetition,
            Alarm anAlarm,
            Set<Participant> anInvitees) {

        CalendarEntry calendarEntry =
                new CalendarEntry(
                        this.tenant(),
                        this.calendarId(),
                        aCalendarIdentityService.nextCalendarEntryId(),//不建议在工厂方法中访问领域服务
                        aDescription,
                        aLocation,
                        anOwner,
                        aTimeSpan,
                        aRepetition,
                        anAlarm,
                        anInvitees);

        return calendarEntry;
    }
}

如果由于特殊原因需要要在聚合中访问service,则建议把上面的代码优化成如下方式。

public class Calendar extends EventSourcedRootEntity {
    public CalendarEntry scheduleCalendarEntry(
            CalendarIdentityService aCalendarIdentityService,
            String aDescription,
            String aLocation,
            Owner anOwner,
            TimeSpan aTimeSpan,
            Repetition aRepetition,
            Alarm anAlarm,
            Set<Participant> anInvitees) {

        CalendarEntry calendarEntry =
                new CalendarEntry(
                        this.tenant(),
                        this.calendarId(),
                        getProductgetProduct(productId),//***
                        aDescription,
                        aLocation,
                        anOwner,
                        aTimeSpan,
                        aRepetition,
                        anAlarm,
                        anInvitees);

        return calendarEntry;
    }

   public Product getProduct(Long productId){
        return new AggregateService().getProduct(productId);
    }
}

2.3、领域服务中的工厂方法

适合创建顶层聚合或是创建过程中需要与其它上下文进行通信。最好的方式是在领域服务(受限定上下文限制)中使用工厂方法。顺理成章的也可以处理防腐层、发布语言、开放主机服务等相关内容。从是否使用工厂方法的维度来分,领域服务也可分为工厂和职能两大类。下面是一个简单的示例:

public class CalendarApplicationService {

    private CalendarRepository calendarRepository;
    private CalendarEntryRepository calendarEntryRepository;
    private CalendarIdentityService calendarIdentityService;
    private CollaboratorService collaboratorService;

    public void scheduleCalendarEntry(
            String aTenantId,
            String aCalendarId,
            String aDescription,
            String aLocation,
            String anOwnerId,
            Date aTimeSpanBegins,
            Date aTimeSpanEnds,
            String aRepeatType,
            Date aRepeatEndsOnDate,
            String anAlarmType,
            int anAlarmUnits,
            Set<String> aParticipantsToInvite,
            CalendarCommandResult aCalendarCommandResult) {

        Tenant tenant = new Tenant(aTenantId);

        Calendar calendar =
                this.calendarRepository()
                        .calendarOfId(
                                tenant,
                                new CalendarId(aCalendarId));

        CalendarEntry calendarEntry =
                calendar.scheduleCalendarEntry(
                        this.calendarIdentityService(),
                        aDescription,
                        aLocation,
                        this.collaboratorService().ownerFrom(tenant, anOwnerId),
                        new TimeSpan(aTimeSpanBegins, aTimeSpanEnds),
                        new Repetition(RepeatType.valueOf(aRepeatType), aRepeatEndsOnDate),
                        new Alarm(AlarmUnitsType.valueOf(anAlarmType), anAlarmUnits),
                        this.inviteesFrom(tenant, aParticipantsToInvite));

        this.calendarEntryRepository().save(calendarEntry);

        aCalendarCommandResult.resultingCalendarId(aCalendarId);
        aCalendarCommandResult.resultingCalendarEntryId(calendarEntry.calendarEntryId().id());
    }
}

三、资源库

资源库通常表示一个安全的存储区域,原则上只为聚合创建资源库,他们之间存在一对一的关系。在设计时要考虑如何向资源库中添加行为、层级资源库的设计、资源库与Dao在概念上的区别等;这只是一种设计原则,具体还是要根据软件的复杂度来评判。概念上资源库不是Dao,但在代码实现上又非常类似,所以资源库的设计实现往往是DDD实践过程中打破领域设计的最大风险。

另外在设计时简单的业务可能只需要简单的存储即可,即Dao可以直接使用领域对象,复杂的可能需要用到按聚合创建资源库这种设计模式。所以在建立资源库时有面向集合(collection-oriented)和面向持久化(persistence-oriented)两种设计方式。

在平衡性能与聚合的大小时,当不易通过遍历的方式来访问某些聚合的时候,就需要使用这资源库,所以资源库经常被设计成用来查找而不是更新。

3.1、资源库设计的注意事项

  1. 资源库是领域的概念;
  2. 不同的聚合有不同的资源库,两个或多个聚合位于同一个对象层级时可以共享同一个资源库
  3. 在第二条成立的前提下,比较合适选用适配器架构 ;
  4. 资源库的设计要体出可替换的原则,程序表现为资源库的接口定义在领域层,实现拿到基础层实现(注意与适配器架构的区别);

3.2、面向集合的资源库设计

一个资源库应该模拟一个SET集合。无论采用什么类型的持久化机制,我们都不应该允许多次添加同一个聚合实例。另外,当从资源库中获取到一个对象并对其进行修改时,我们并不需要重新保存此对象到资源库中。

在一个聚合中不应该存在一个显示的持久化接口调用,而应该用类似Hibernate那种会话机制实现隐式读时复制或隐式写时复制的方式。前者是读时创建一个副本当客户端提交时,对比这两个版本决定是否更新到DB中;后者是利用一个委托对象管理持久化对象,当持久化对象首次被调用时生成一个副本。

public interface GroupRepository {
    public void add(Group aGroup);
    public Collection<Group> allGroups(TenantId aTenantId);
    public Group groupNamed(TenantId aTenantId, String aName);
    public void remove(Group aGroup);
}
public class HibernateGroupRepository
        extends AbstractHibernateSession
        implements GroupRepository {

    public HibernateGroupRepository() {
        super();
    }

    @Override
    public void add(Group aGroup) {
        try {
            this.session().saveOrUpdate(aGroup);
        } catch (ConstraintViolationException e) {
            throw new IllegalStateException("Group is not unique.", e);
        }
    }
}

3.3、面向持久化的资源库设计

如果持久化技术框架不支持对象变化的跟踪,无论是显式还是隐匿的,那么采用面向集全资源库便不合适了,此时最好采用基于操作的资源库—面向持久化的资源库,这是现阶段普遍采用的方式,实现技术多为mybatis。在向数据存储中添加新建对象或修改时,必须显示的调用save方法,

和面向集合的设计的一个区别就是,是否需要显示调用save方法。

3.4、资源库设计时的事务的管理

相对来说领域模型和领域层的操作比较细粒度,事务的管理应该是以粗粒度的方式放在应用层中比较合适,实现时可以硬编码也可以采用声明的方式,但是在分布式系统中,不建议过多的使用事务,而是采用最终一致性设计,示例如下:

public class AccessApplicationService {
    @Autowired
    private GroupRepository groupRepository;
    @Autowired
    private UserRepository userRepository;
    public AccessApplicationService() {
        super();
    }
    @Transactional
    public void assignUserToRole(AssignUserToRoleCommand aCommand) {
        //领域模型
        TenantId tenantId = new TenantId(aCommand.getTenantId());
        User user =
                this.userRepository().userWithUsername(
                                tenantId,
                                aCommand.getUsername());
        if (user != null) {
            Role role = this.roleRepository().roleNamed(
                                    tenantId,
                                    aCommand.getRoleName());

            if (role != null) {
                role.assignUser(user);
            }
        }
    }

    @Transactional(readOnly=true)
    public boolean isUserInRole(
            String aTenantId,
            String aUsername,
            String aRoleName) {

        User user = this.userInRole(aTenantId, aUsername, aRoleName);

        return user != null;
    }
}

3.5、资源库VS数据访问对象(DAO)

虽然Dao和面向集合持久化的资源库在代码实现上有可能是是一样的,但一定要弄清它们之间的概念区别,因为它会影响我们的程序设计:

  • Dao是针对数据的,它只是对DB的一层封装。而资源库更加面向对象;
  • 资源库是领域模型的一部分,而Dao并不领域范畴内的东西;
  • 以上定位会影响包结构的设计以及调用关系。比如Dao通常会把接口和实现单独管理,而资源库则会用领域层来管理资源库;

END,至此DDD中相关的概念和基础知识全部讲解了,后续笔者会侧重实践落地,分专题讲述下笔者在实践DDD中关于架构设计、考核、流程制定等相关的内容。

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

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

相关文章

深入理解ClickHouse跳数索引教程

跳数索引 影响ClickHouse查询性能的因素很多。在大多数场景中&#xff0c;关键因素是ClickHouse在计算查询WHERE子句条件时是否可以使用主键。因此&#xff0c;选择适用于最常见查询模式的主键对于表的设计至关重要。 然而&#xff0c;无论如何仔细地调优主键&#xff0c;不可…

Java导出数据到Excel

Java导出数据到Excel分3步处理 1、构建Workbook 数据 2、设置Workbook 格式 3、导出到Excel 1、构建Workbook 数据 public static void buildData(Workbook wb, List<Person> list) {Sheet sheetName wb.createSheet("sheetName");Row row sheetName.creat…

macOS 环境变量加载探究

使用 macOS 安装环境&#xff0c;见到过很数种环境变量配置方法&#xff0c;每次也都是按照别人的代码&#xff0c;人家配置在哪 我就配置在哪&#xff0c;其实不太清楚有什么区别&#xff0c;决定记录下。 本机 macOS 13.3&#xff0c;从 macOS Catalina(10.15) 开始&#xf…

软件测试入门基础知识

目录 1.软件测试的定义 2.软件测试的生命周期 3.如何描述一个bug 4.bug的级别如何定义 5.bug生命周期 6.软件测试策略 7.软件测试模型 7.1传统瀑布模型 7.2V模型 7.3W模型&#xff08;双V模型&#xff09; 7.4敏捷模型 7.5X模型 1.软件测试的定义 首先要明确测试的定义…

systemVerilog基础9——类的继承

1、 描述子类继承父类的关键词&#xff1a;extends 之前定义过的类Packet&#xff0c;可以进一步扩展构成一个它的子类LinkedPacket。类Packet的定义如下&#xff1a; class Packet ;//class定义类 类名 packet//类 packet的成员//数据或类属性bit [3:0] command; bit [40:0] …

【css】背景图片附着

属性&#xff1a;background-attachment 属性指定背景图像是应该滚动还是固定的&#xff08;不会随页面的其余部分一起滚动&#xff09;。 background-attachment: fixed&#xff1a;为固定&#xff1b; background-attachment: scroll为滚动 代码&#xff1a; <!DOCTYPE h…

TypeScript基础学习

目录 一、安装 1、下载国内镜像 2、安装 3、查看安装情况 4、使用例子 二、变量声明 1、规则 2、声明的四种方式 3、注意 4、类型断言 5、类型推断 6、变量作用域 三、基础类型&#xff08;共11种&#xff09; 1、Any 类型 2、Null 和 Undefined 3、never 类型…

医药化工企业洁净厂房改造消防防爆安全的重要性

设计 【摘要】&#xff1a;近年来&#xff0c;我国医药化工企业规模不断扩大。医药化工企业的情况复杂&#xff0c;稍有不慎将发生火灾或者爆炸&#xff0c;对人员生命以及财产安全造成巨大的损害&#xff0c;酿成悲剧。所以&#xff0c;“三同时”原则的落实&#xff0c;如何…

伊语IM即时通讯源码/im商城系统/纯源码IM通讯系统安卓+IOS前端纯原生源码

伊语IM即时通讯源码/im商城系统/纯源码IM通讯系统安卓IOS前端纯原生源码&#xff0c; 后端是java源码。

tinkerCAD案例:30. 冰球挑战赛

tinkerCAD案例&#xff1a;30. 冰球挑战赛 这些简单易学、循序渐进的 Tinkercad 课程将指导你设计出属于自己的超棒曲棍球冰球&#xff1b;这些设计将性能和风格推向了极限&#xff01; 本课有两个目标利用科学方法提高曲棍球球的性能。通过在冰球上添加图案&#xff0c;发挥设…

rust怎么搞的,这么简单的代码也报“borrow of moved value“?

奇了怪了&#xff0c;这么简单的代码也编译不过&#xff1f; let hello String::from("hello");let world hello;hello.push_str("world"); // error[E0382]: borrow of moved value: hello看下完整报错 error[E0382]: borrow of moved value: hello--&…

picgo 图床 七牛云的设置

网站 PicGo github Molunerfinn/PicGo: &#x1f680;A simple & beautiful tool for pictures uploading built by vue-cli-electron-builder PicGo/Awesome-PicGo: A collection of awesome projects using PicGo. 设置 AcccessKey (AK) SecretKey(SK) 在个人中心&…

HarmonyOS学习路之方舟开发框架—学习ArkTS语言(状态管理 三)

Link装饰器&#xff1a;父子双向同步 子组件中被Link装饰的变量与其父组件中对应的数据源建立双向数据绑定。 概述 Link装饰的变量与其父组件中的数据源共享相同的值。 装饰器使用规则说明 Link变量装饰器 说明 装饰器参数 无 同步类型 双向同步。 父组件中State, Stor…

无法访问Microsoft Store

可以先试试把代理工具关了 如果不行 winr, 输入inetcpl.cpl 勾上TLS 1.1,TSL1.2 重启&#xff0c;再试试

react学习笔记——1. hello react

包含的包一共有4个&#xff0c;分别的作用如下&#xff1a; babel.min.js&#xff1a;可以进行ES6到ES5的语法转换&#xff1b;可以用于import&#xff1b;可以用于将jsx转换为js。注意&#xff0c;在开发的时候&#xff0c;这个转换&#xff08;jsx转换js&#xff09;不在线上…

杨辉三角,给定一个非负整数 numRows,生成「杨辉三角」的前 numRows 行。

题记&#xff1a; 给定一个非负整数 numRows&#xff0c;生成「杨辉三角」的前 numRows 行。 在「杨辉三角」中&#xff0c;每个数是它左上方和右上方的数的和。 示例 1: 输入: numRows 5 输出: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]] 示例 2: 输入: numRows 1 输出: …

千元内初学者吉他买什么好?SAGA萨伽SF700和VEAZEN费森VZ200评测对比,哪一款更出众适合新手入门选购!

大部分了解过相关吉他知识或者有点音乐意识的初学者&#xff0c;都会摒弃烧火棍去选择买一把千元单板&#xff0c;亦或者少数资金充足的琴友会选择更贵的吉他。所以翻阅很多有关吉他推荐的帖子&#xff0c;我们可以看到在千元的价位里&#xff0c; **VEAZEN费森VZ200和 SAGA萨伽…

ZeRO Memory Optimizations Toward Training Trillion Parameter Models

1总述 Zero Redundancy Optimizer: 提高了可以被有效训练的模型的大小&#xff0c;极大提高了模型训练的速度。 保持了较小的通信量保持较高的计算粒度 数据并行DP 并没有减少每个device上的内存占用当模型有1.4B 参数时&#xff0c;就会超过GPU的32GB显存通过PP, MP&#…

P4D编程遇到乱码?别急,这里有解决方案!

P4D简介&#xff1a; P4D&#xff08;Python for Delphi&#xff09;是一种用于在Delphi开发环境中嵌入Python的技术&#xff0c;它允许开发人员使用Python语言编写脚本和模块&#xff0c;然后将其集成到Delphi应用程序中。这使得开发人员可以利用Python的强大功能来扩展和增强…

JavaWeb(7)——前端综合案例1(面向对象)

一、需求 JS进阶-day3-184-综合案例-模态框构造函数写法 二、 实现 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta http-equiv"X-UA-Compatible" content"IEedge" /><meta…