系列文章

围绕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. static 静态文件配置

  2. input type

    input的type有: text 文本输入 password密码输入 file选择文件 radio单选按钮 checkbox复选按钮 submit对应form的action按钮 button 普通按 ...

  3. Java语言的三大特性

    Java语言的三大特性 1.封装 1.1  概念: 封装也成为信息异常,隐藏对象的属性和实现细节,仅对外公开接口(外部不可以直接看到对象的属性细节并且不能直接更改属性的值,只能通过方法来读取或更改属性 ...

  4. 使用基于centos7 dbus问题总结

    Authorization not available. Check if polkit Authorization not available. Check if polkit service is ...

  5. 使用alpine为基础镜像Q&A

    作为go应用存在二进制文件却不能执行 明明镜像中有对应的二进制文件,但是执行时却提示 not found 或 no such file 或 standard_init_linux.go:211: ex ...

  6. macos查看端口状况

    Mac OS netstat命令与CentOS 略有出入 在Mac上正确使用的方法是:即-f需要加上地址族,-p需要加上协议TCP或者UDP等 如果需要查询inet:netstat -anvf ine ...

  7. ruby基础(四)

    ruby基础知识 模块 模块是ruby的特色功能之一.如果说类是事物的实体以及行为,那么模块表现的 就是事物的行为部分,模块和类有以下两点不同: 模块不能拥有实例 模块不能被继承 模块的使用方法 mo ...

  8. PyCharm和JDK安装与配置(windows)

    原创 PyCharm和JDK安装与配置(windows) mb5cd21e691f31a关注0人评论2024人阅读2020-03-20 21:08:41   一.PyCharm安装与配置 PyChar ...

  9. 马哥Linux SysAdmin学习笔记(三)

    CentOS 5和6的启动流程: Linux:kernel+rootfs kernel:进程管理,内存管理,网络管理,驱动程序,文件系统,安全功能 rootfs: glibc 库:函数集合,funct ...

  10. Msf--控制 Android手机

    |>>>中华人民共和国网络安全法<<<|警告:请勿用于非法用途,后果自负! 0.环境 虚拟机 KaliLinux 手机 Redmi 6A 同一局域网下 1.查看控制 ...