系列文章

围绕DDDABP Framework两个核心技术,后面还会陆续发布核心构件实现综合案例实现系列文章,敬请关注!

ABP Framework 研习社(QQ群:726299208)

ABP Framework 学习及实施DDD经验分享;示例源码、电子书共享,欢迎加入!

数据传输对象

DTO 是简单对象,用于在应用层和展示层传递状态数据。所以,应用服务方法返回 DTO。

DTO原则和最佳实践:

  • DTO应该可序列化,因为大多数时候,需要网络传输。
  • 应该有一个无参构造函数
  • 不能包含任何业务逻辑
  • 不能继承或引用实体

输入DTO输出DTO在本质上不同:一个用于给应用服务方法传递参数,一个作为应用服务方法的返回值,根据业务需要区别对待。

输入DTO最佳实践

不要在输入DTO中定义不使用的属性

只定义需要用的属性,否则,无用的属性只会让客户端在使用应用服务方法时感到困惑。当然可以定义可选属性,但是确保当客户端在使用时,不应该影响到用例的工作方式。

这条规则看起来没什么必要,谁会为方法仓储(输入DTO)添加不使用的属性呢?但是,它经常发生,尤其是当你想重用输入DTO对象时,会将多个DTO属性放在一个DTO对象中。

不要重用输入DTO

为每个用例(应用服务方法)定义特定的输入DTO,否则,在某些情况下不会添加一些不被使用的属性,这就违反了上面定义的规则。

有时候,在两个不同的用例中使用相同的DTO似乎很有吸引力,因为他们如此相似。甚至,当前是一模一样,可能后面随着业务变化才会有可能不同,此时也应该不要重用输入DTO。因为和用例间的耦合相比,代码复制可能是更好的做法。

重用DTO的另一种方式是:DTO继承,这同样会产生上面描述的问题.。

示例:用户应用服务

public interface IUserAppService:IApplicationService
{
Task CreateAsync(UserDto input);
Task UpdateAsync(UserDto input);
Task ChangePasswordAsync(UserDto input);
}

IUserAppService 在所有方法(用例)使用 UserDto 作为输入DTO,UserDto定义如下:

public class UserDto
{
public Guid Id{get;set;}
public string UserName{get;set;}
public string Email{get;set;}
public string Password{get;set;}
public DateTime CreationTime{get;set;}
}
  • Id 在 Create 方法中不被使用,因为 Id 由服务器生成。
  • Password 在 Update 方法中不使用,因为有修改密码的单独方法。
  • CreationTime 未被使用,且不应该由客户端发送给服务端,应该在服务端设置创建时间。

正确的实现,如下:

public interface IUserAppService:IApplicationService
{
Task CreateAsync(UserCreationDto input);
Task UpdateAsync(UserUpdateDto input);
Task ChangePasswordAsync(UserChangePasswordDto input);
}

然后定义对应的DTO类:

public class UserCreationDto
{
public string UserName {get;set;}
public string Email{get;set;}
public string Password{get;set;}
} public class UserUpdateDto
{
public Guid Id{get;set;}
public string UserName{get;set;}
public string Email{get;set;}
} public class UserChangePasswordDto
{
public Guid Id{get;set;}
public string Password{get;set;}
}

尽管需要编写更多的代码,但是这是一种更易维护的方法。

特殊情况:举个例子,如果你有一个报表页,页面中有多个过滤条件,对应多个应用服务方法(显示报表、导出Excel、导出CSV),此时应该使用相同的输入DTO参数,返回不同的结果。因为当页面过滤条件改变时,修改一个DTO而对整个页面对应的应用服务方法参数生效。

输入DTO中验证逻辑

  • 仅在DTO内部执行简单验证,使用数据注解特性或实现 IValidatableObject 接口
  • 不要执行领域验证,举个例子,不要在DTO中检测用户名是否唯一的验证。

示例:使用数据注解特性

using System.ComponentModel.DataAnnotations;

namespace IssueTracking.Users
{
public class UserCreationDto
{
[Required]
[StringLength(UserConsts.MaxUserNameLength)]
public string UserName {get;set;} [Required]
[EmailAddress]
[StringLength(UserConsts.MaxEmailLength)]
public string Email{get;set;}
[Required]
[StringLength(UserConsts.MaxEmailLength,MinimumLength=UserConsts.MinPasswordLength)]
public string Password{get;set;}
}
}

ABP框架自动验证输入DTO,验证失败则抛出AbpValidationException异常,返回 400 HTTP 状态码。

某些开发者认为将验证规则和DTO类分离可能会更好。我们认为声明式(数据注解)是实用的,不会导致任何设计问题。当然,ABP支持 FluentValidation集成。

输出DTO最佳实践

  • 保持输出DTO数量最小,尽可能重用,但是不能将输入DTO作为输出DTO使用。
  • 输出DTO可以包含比用例需要的更多属性
  • CreateUpdate 方法中返回DTO

以上建议的主要原因是:

  • 使客户端代码易于开发和扩展

    • 在客户端端处理不同但相似的DTO容易混淆
    • 输入DTO中的更多属性可能未来会在UI/客户端中被使用,返回实体的所有属性(已经考虑过安全性和特殊情况)使客户端代码易于改进,而不需要修改后端代码。
    • 如果是通过API暴露给第三方客户端,避免不同需求返回不同DTO
  • 使服务端代码易于开发和扩展
    • 更少的类,易于理解和维护
    • 可以重用实体到DTO(AutoMapper)的对象映射代码
    • 不同方法返回相同类型,使添加新方法变得简单明了。

示例:从不同方法返回不同DTO

public interface IUserAppService:IApplicationService
{
UserDto Get(Guid id);
List<UserNameAndEmailDto> GetUserNameAndEmail(Guid id);
List<string> GetRoles(Guid id);
List<UserListDto> GetList();
UserCreateResultDto Create(UserCreationDto input);
UserUpdateResultDto Update(UserUpdateDto input);
}

示例中没有使用异步方法,在实际开发时应该是异步方法。

上面的示例代码中,为每个方法返回不同DTO类型,这样会导致我们需要处理非常多的数据查询,映射实体到DTO的重复代码。

按照以下方式定义就简单多了:

public interface IUserAppService:IApplicationService
{
UserDto Get(Guid id);
List<UserDto> GetList();
UserDto Create(UserCreationDto input);
UserDto Update(UserUpdateDto input);
}

使用一个输出DTO:

public class UserDto
{
public Guid Id{get;set;}
public string UserName{get;set;}
public string Email{get;set;}
public DateTiem CreationTime{get;set;}
public List<string> Roles{get;set;}
}
  • 移除 GetUserNameAndEmailGetRoles 方法,因为 Get 方法已经返回足够需要的信息。
  • GetList 返回对象与 Get 相同
  • CreateUpdate 同样返回 UserDto

由此可见,返回相同DTO更加简洁。

为什么创建或更新之后要返回DTO? 想象一个用例场景,在页面中显示表格数据,当更新之后,获取返回对象,并对表格数据源进行更新,这样就不需要再次调用 GetList 方法,这是我们建议在 CreateUpdate 方法中返回 DTO 的原因。

讨论

以上关于输出DTO的建议,并不适用所有场景。

出于性能考虑,这些建议可以被忽略,特别是当存在大型数据集返回结果时,或者用户界面需要发起很多并发请求时,此时应该创建特定的输出DTO,只包含尽可能少的信息。

可维护性和性能,需要开发者权衡,上面的建议适用于性能损失可忽略不计的应用。

对象映射

自动对象映射是一个非常有用的工具,两个对象的属性相同或相似,将一个对象的值复制给另一个对象。

DTO和实体类通常具有相同或相似的属性,通常需要根据实体和业务需求来创建DTO对象。ABP框架对象映射基于 AutoMapper,相比手动赋值,效率更高。

  • 仅对实体到输出DTO使用自动对象映射。
  • 输入DTO到实体不适用自动对象映射。

不使用输入DTO到实体自动映射的原因:

  1. 实体类通常有构造函数,接收参数并在创建时,进行参数验证。自动对象映射操作通常需要无参构造函数创建对象。
  2. 实体属性设置器大多是私有的,应该使用方法设置属性值
  3. 通常需要仔细验证和处理用户/客户端输入,而不是盲目地映射到实体属性。

虽然其中一些问题可以通过映射配置来解决(例如,AutoMapper允许定义自定义映射规则),但它使你的业务逻辑隐含/隐藏,并与基础设施紧密耦合。我们认为业务代码应该是明确的、清晰的、容易理解的。

学习帮助

围绕DDDABP Framework两个核心技术,后面还会陆续发布核心构件实现综合案例实现系列文章,敬请关注!

ABP Framework 研习社(QQ群:726299208)

专注 ABP Framework 学习及DDD实施经验分享;示例源码、电子书共享,欢迎加入!

基于ABP落地领域驱动设计-05.实体创建和更新最佳实践的更多相关文章

  1. 基于ABP落地领域驱动设计-00.目录和小结

    <实现领域驱动设计> -- 基于 ABP Framework 实现领域驱动设计实用指南 翻译缘由 自 ABP vNext 1.0 开始学习和使用该框架,被其优雅的设计和实现吸引,适逢 AB ...

  2. 基于ABP落地领域驱动设计-04.领域服务和应用服务的最佳实践和原则

    目录 系列文章 领域服务 应用服务 学习帮助 系列文章 基于ABP落地领域驱动设计-00.目录和前言 基于ABP落地领域驱动设计-01.全景图 基于ABP落地领域驱动设计-02.聚合和聚合根的最佳实践 ...

  3. 基于ABP落地领域驱动设计-06.正确区分领域逻辑和应用逻辑

    目录 系列文章 领域逻辑和应用逻辑 多应用层 示例:正确区分应用逻辑和领域逻辑 学习帮助 系列文章 基于ABP落地领域驱动设计-00.目录和前言 基于ABP落地领域驱动设计-01.全景图 基于ABP落 ...

  4. 基于ABP落地领域驱动设计-02.聚合和聚合根的最佳实践和原则

    目录 前言 聚合 聚合和聚合根原则 包含业务原则 单个单元原则 事务边界原则 可序列化原则 聚合和聚合根最佳实践 只通过ID引用其他聚合 用于 EF Core 和 关系型数据库 保持聚合根足够小 聚合 ...

  5. 基于ABP落地领域驱动设计-03.仓储和规约最佳实践和原则

    目录 系列文章 仓储 仓储的通用原则 仓储中不包含领域逻辑 规约 在实体中使用规约 在仓储中使用规约 组合规约 学习帮助 围绕DDD和ABP Framework两个核心技术,后面还会陆续发布核心构件实 ...

  6. 基于ABP落地领域驱动设计-01.全景图

    什么是领域驱动设计? 领域驱动设计(简称:DDD)是一种针对复杂需求的软件开发方法.将软件实现与不断发展的模型联系起来,专注于核心领域逻辑,而不是基础设施细节.DDD适用于复杂领域和大规模应用,而不是 ...

  7. [.NET领域驱动设计实战系列]专题七:DDD实践案例:引入事件驱动与中间件机制来实现后台管理功能

    一.引言 在当前的电子商务平台中,用户下完订单之后,然后店家会在后台看到客户下的订单,然后店家可以对客户的订单进行发货操作.此时客户会在自己的订单状态看到店家已经发货.从上面的业务逻辑可以看出,当用户 ...

  8. [.NET领域驱动设计实战系列]专题六:DDD实践案例:网上书店订单功能的实现

    一.引言 上一专题已经为网上书店实现了购物车的功能了,在这一专题中,将继续对网上书店案例进行完善,本专题将对网上书店订单功能的实现进行介绍,现在废话不多说了,让我们来一起看看订单功能是如何实现的吧. ...

  9. 从0开发3D引擎(补充):介绍领域驱动设计

    我们使用领域驱动设计(英文缩写为DDD)的方法来设计引擎,在引擎开发的过程中,领域模型会不断地演化. 本文介绍本系列使用的领域驱动设计思想的相关概念和知识点,给出了相关的资料. 上一篇博文 从0开发3 ...

随机推荐

  1. QFNU 10-16 training

    7-9.小字辈 思路:建立一个类,并且类中存有其父节点,其地位,其儿子节点(因为儿子节点有很多,所以要用vector进行存储),通过-1这个祖先节点进行查找.首先找到-1这个祖先节点,并且读入其他位置 ...

  2. Django(17)orm查询操作

    前言 查找是数据库操作中一个非常重要的技术.查询一般就是使用filter.exclude以及get三个方法来实现.我们可以在调用这些方法的时候传递不同的参数来实现查询需求.在ORM层面,这些查询条件都 ...

  3. git远端账号问题

    1. gitlab密码修改后git因保存密码无法下载,403 在.git/config添加如下 [credential] helper = store 添加了此项配置之后, github的账号信息就会 ...

  4. 阿里云上安装 OpenStack 是什么体验

    阿里云上跑火车(安装 OpenStack Train 版本),猜猜最终花了多少钱? 前言 前面给大家提供了用虚拟机安装 OpenStack 的镜像,虽然已经很简便了,但还是略显笨重.一来镜像文件比较大 ...

  5. 解决 Ubuntu 无法使用 root 用户进行 ssh 远程登陆

    解决 Ubuntu 无法使用 root 用户进行 ssh 远程登陆 操作系统 Ubuntu 20.04.2 LTS 一.修改sshd配置文件 //打开 /etc/ssh/sshd_config 配置文 ...

  6. centos 7编译32位软件的问题

    centos 7默认安装的gcc 为64位,所以在编译32位软件时需要用到32位的gcc库,于是可以增加glibc 32位库: sudo yum -y install glibc-bevel.i386

  7. 回归(regression)与分类(classification)的区别

    术语监督学习,意指给出一个算法,需要部分数据集已经有正确的答案. " 分类和回归的区别在于输出变量的类型. 定量输出称为回归,或者说是连续变量预测:定性输出称为分类,或者说是离散变量预测. ...

  8. java和kotlin的可见性修饰符对比

    private 意味着只在这个类内部(包含其所有成员)可见: protected-- 和 private一样 + 在子类中可见. internal -- 能见到类声明的 本模块内 的任何客户端都可见其 ...

  9. 为Go项目编写Makefile

    为Go项目编写Makefile 借助Makefile我们在编译过程中不再需要每次手动输入编译的命令和编译的参数,可以极大简化项目编译过程. make介绍 make是一个构建自动化工具,会在当前目录下寻 ...

  10. ELK技术栈之-Logstash详解

    ELK技术栈之-Logstash详解   前言 在第九章节中,我们已经安装好Logstash组件了,并且启动实例测试它的数据输入和输出,但是用的是最简单的控制台标准输入和标准输出,那这节我们就来深入的 ...