本文将介绍DDD分层架构中广泛使用的数据传输对象Dto,并且与领域实体Entity,查询实体QueryObject,视图实体ViewModel等几种实体进行比较。

领域实体为何不能一统江湖?

  当你阅读我或其它博主提供的示例代码时,会发现几种类型的实体,这几种实体初步看上去区别不大,只是名称不同,特别在这些示例非常简单的情况下更是如此。你可能会疑惑为何要搞得这么复杂,采用一种实体不是更好?

  在最理想的情况下,我们只想采用领域实体Entity进行所有的操作。

  领域实体是领域层的核心,是业务逻辑的主要放置场所。换句话说,领域实体中包含了大量业务逻辑方法。

领域实体在表现层进行模型绑定时可能遇到障碍

  如果领域实体中的属性都包含getter和setter,并且所有属性都是public的,那么,使用这个Entity的程序员可能会绕过业务方法,直接操作属性进行赋值。

  为属性直接赋值,是面向数据的过程式思维,而调用方法是面向对象的方式,这也是领域模型的核心所在。

  所以为了强制实施业务规则,必须把业务方法操作过的属性的setter访问器隐藏起来,否则这个方法不会有人调用。

  当领域实体某些属性的setter被隐藏后,直接在表现层操作领域实体将变得困难,因为Mvc或Wpf的模型绑定只能操作public的属性。

序列化领域实体可能遇到障碍

  哪怕你的系统没有使用分布式,比如只是一个Mvc网站,但由于前端要求越来越高,客户端很多时候需要通过ajax与服务端进行交流,一般采用json格式传递数据,这就要求你的实体能够序列化。

  对领域实体进行序列化,首先需要考虑的问题是,可能序列化一个较大的对象图,从而导致不必要的开销。

  领域实体一般包含导航属性指向其它领域实体,其它的领域实体可能包含更多导航属性,从而组成一个对象图。如果采用Serializable特性进行序列化,并且没有指定其它序列化选项,可能导致把一个庞大的对象图序列化并进行网络传输。

  另一个问题是,复杂的领域实体可能包含循环引用,从而导致序列化失败。

  对于序列化,一个更好的选择是采用DataContract特性,被DataContract修饰过的类成员,不会被自动序列化,必须在成员上明确指定DataMember特性。

  DataMember在一定程度上可以缓解上述问题,比如减少需要序列化的数据,不序列化循环引用的对象等,但无法从根本上解决问题。

领域实体无法应对多客户端应用需求

  对于不同的客户端,可能需要的数据和格式不同,这属于应用层需求,而领域实体只有一个,在领域实体上通过标记DataMember进行序列化费力不讨好,无法满足复杂的应用需求。

  哪怕你只有一个Mvc网站,如果页面上需要显示一些领域实体不存在的数据,你根据这个需求,直接在领域实体上增加属性是非常糟糕的做法,会严重污染你的领域模型,将大大降低领域实体的复用能力。

  从以上可以看出,对于一个比较复杂的系统,单凭领域实体很难完成任务,将太多的职责强加到领域实体上,会导致领域实体严重变形。

数据传输对象介绍

  数据传输对象,即Data Transfer Object,简称DTO。

  一个为了减少方法调用次数而在进程间传输数据的对象,《企业应用架构模式》如是说。

  可以看出,DTO用于分布式环境,主要用来解决分布式调用的性能问题。同一进程内的对象调用,速度是非常快的,但跨进程调用,甚至跨网络调用,性能下降N个数量级。为了提升性能,需要减少调用次数,这就要求把多次调用的结果打包成一个对象,在一次调用中返回尽量多的数据。

  上面是DTO的原始含义,下面来看看我的山寨用法。

  虽然我也取名为DTO,但我的动机并不完全是一次打包更多数据来提升性能,而是解决上面提到的几个问题,当然它们之间有一定关系,可以看作一种变种用法。

DTO的长相

  DTO是一个贫血对象,也就是它里面基本没有方法,只有一堆属性,并且所有属性都具有public的getter和setter访问器。

  DTO拥有public的setter访问器,方便的解决了表现层的模型绑定问题。

  由于DTO不执行业务操作,仅用于传递数据,所以不应该定义非常复杂的对象引用关系,这样就避免了循环引用,解决了对象序列化的问题。

DTO的粒度

  DTO可以根据应用需求定义成不同的粒度,在一般情况下,DTO是聚合粒度,也就是说,一个领域层的聚合对应一个DTO,这样做的一个好处是方便对CRUD操作进行抽象以及代码生成。

  界面如果想保持简单,应该尽量一个界面操作一个聚合,将聚合的数据映射到DTO后,传给视图展示。

  对于更加复杂的界面,需要在一个界面操作多个聚合,这种情况下,把需要的全部数据打包到DTO进行操作。

  从以上介绍中,你应该了解DTO不能理解为单表操作,它可以包含你需要的全部数据。

DTO的位置

  DTO处于应用层,在表现层与领域层之间传递数据。

  DTO由应用层服务使用,应用层服务从仓储中获得聚合,并调用DTO转换器将聚合映射为DTO,再将DTO传递给表现层。

  关于应用层服务,后续再专门介绍。

DTO的映射

  聚合与DTO的转换,看上去是一个简单问题,在聚合与DTO几乎完全一致的情况下,采用映射组件将非常省力。很多人采用AutoMapper,但它的性能稍微差了点,EmitMapper是更好的选择,性能接近硬编码。

  当DTO与聚合显著不同时,我发现手工编码更加清晰高效。我采用代码生成器创建出一个代码基础,在有个性化需求时,手工修改映射代码。

  我总是采用一个静态类来扩展DTO和聚合,为它们添加相关的转换方法。

using Biz.Security.Domains.Models;
using Util; namespace Biz.Security.Services.Dtos {
/// <summary>
/// 应用程序数据传输对象扩展
/// </summary>
public static class ApplicationDtoExtension {
/// <summary>
/// 转换为应用程序实体
/// </summary>
/// <param name="dto">应用程序数据传输对象</param>
public static Application ToEntity( this ApplicationDto dto ) {
return new Application( dto.Id.ToGuid() ) {
Code = dto.Code,
Name = dto.Name,
Note = dto.Note,
Enabled = dto.Enabled,
CreateTime = dto.CreateTime,
Version = dto.Version,
};
} /// <summary>
/// 转换为应用程序数据传输对象
/// </summary>
/// <param name="entity">应用程序实体</param>
public static ApplicationDto ToDto( this Application entity ) {
return new ApplicationDto {
Id = entity.Id.ToString(),
Code = entity.Code,
Name = entity.Name,
Note = entity.Note,
Enabled = entity.Enabled,
CreateTime = entity.CreateTime,
Version = entity.Version,
};
}
}
}

DTO 与 ViewModel比较

  ViewModel是为特定视图专门定义的实体对象,专为该视图服务。

  对于WPF,ViewModel是必须的,用来支持MVVM模式进行双向绑定。

  那么MVC呢,一定需要它吗?

  由于采用了DTO,在一般情况下,我都把这个DTO当作ViewModel来使用。如果界面上需要某个属性,我会直接添加到DTO上。

  一个例外是,如果MVC的界面非常复杂,我感觉把大量的垃圾属性加到DTO上不合适,就会创建专门的ViewModel。

查询实体介绍

  查询实体这个说法,是我乱取的,估计你在其它地方也没有听说过。使用它的原因,是用来配合我的查询组件一起工作。

  我前面已经介绍过查询相关的内容,核心思想是通过判断一个可空属性,自动完成空值判断,这是一个强大的特性,帮助你免于编写大量杂乱无章的判断

  查询实体的基本特征就是所有属性必须可空,并且它足够简单,不会拥有集合那样的子对象,所有属性都是扁平化的。

  通过传递查询实体,表现层可以做到尽量简单,由于表现层支持模型绑定,甚至不需要代码,省力是我搭建框架的一个基本出发点。

  当然查询实体只支持简单查询,不支持灵活的动态查询,比如让客户设置查询运算符等,暂时没有这方面的需求,如果后续有需求,会扩展一个出来。

  查询实体示例:

using System.ComponentModel.DataAnnotations;
using Util;
using Util.Domains.Repositories; namespace Biz.Security.Domains.Queries {
/// <summary>
/// 应用程序查询实体
/// </summary>
public class ApplicationQuery : Pager {
/// <summary>
/// 应用程序编号
/// </summary>
[Display( Name = "应用程序编号" )]
public System.Guid? ApplicationId { get; set; } private string _code = string.Empty;
/// <summary>
/// 应用程序编码
/// </summary>
[Display( Name = "应用程序编码" )]
public string Code {
get { return _code == null ? string.Empty : _code.Trim(); }
set { _code = value; }
} private string _name = string.Empty;
/// <summary>
/// 应用程序名称
/// </summary>
[Display( Name = "应用程序名称" )]
public string Name {
get { return _name == null ? string.Empty : _name.Trim(); }
set { _name = value; }
} private string _note = string.Empty;
/// <summary>
/// 备注
/// </summary>
[Display( Name = "备注" )]
public string Note {
get { return _note == null ? string.Empty : _note.Trim(); }
set { _note = value; }
} /// <summary>
/// 启用
/// </summary>
[Display( Name = "启用" )]
public bool? Enabled { get; set; } /// <summary>
/// 起始创建时间
/// </summary>
[Display( Name = "起始创建时间" )]
public System.DateTime? BeginCreateTime { get; set; } /// <summary>
/// 结束创建时间
/// </summary>
[Display( Name = "结束创建时间" )]
public System.DateTime? EndCreateTime { get; set; } /// <summary>
/// 添加描述
/// </summary>
protected override void AddDescriptions() {
base.AddDescriptions();
AddDescription( "应用程序编号", ApplicationId );
AddDescription( "应用程序编码", Code );
AddDescription( "应用程序名称", Name );
AddDescription( "备注", Note );
AddDescription( "启用", Enabled.Description() );
AddDescription( "起始创建时间", BeginCreateTime );
AddDescription( "结束创建时间", EndCreateTime );
}
}
}

总结

最后来总结一下:

  1. 领域实体是系统的中心,是业务逻辑的主要放置场所,应该尽量关闭业务逻辑操作的属性,以避免有人能绕过你的方法直接操作数据。

  2. DTO是数据传输对象,原义是用来在分布式系统中一次传输更多数据,以减少调用次数,提升性能。

  3. 我的DTO用法离原义相去甚远,只是借用了DTO的名词,属于变种。DTO为我解决了如下几个问题:

  • 领域实体在表现层进行模型绑定时可能失败
  • 序列化领域实体可能失败
  • 领域实体无法应对多客户端应用需求,通过创建多套DTO甚至应用层,可以为不同的应用提供服务,而领域层不变,它是系统的中心。

  4. DTO是包含大量属性,没有方法的贫血实体,所有属性都开放getter和setter,以方便模型绑定和序列化。

  5. DTO一般情况下是聚合去除方法后的模样,主要好处是方便抽象CRUD及代码生成。

  6. DTO位于应用层,由应用层服务操作它。

  7. DTO的映射可以采用映射组件,也可以代码生成方便随时修改,以你觉得方便为主。

  8. 仅在WPF环境下才需要为每个视图创建一个对应的ViewModel,MVC一般使用DTO即可,仅为复杂界面创建ViewModel。

  9. 查询实体是为了配合查询组件引入的构造,目的是帮助查询组件完成空值判断,并且简化表现层的调用。

  本文分享了我在几个构造类型上的认识和经验,希望大家积极讨论,更希望高手能指正我的不足,帮助我与大家一起进步。

  .Net应用程序框架交流QQ群: 386092459,欢迎有兴趣的朋友加入讨论。

  .Net Easyui开发交流QQ群(本群仅限Easyui开发者,非Easyui开发者勿进):157809322

  谢谢大家的持续关注,我的博客地址:http://www.cnblogs.com/xiadao521/

应用程序框架实战三十四:数据传输对象(DTO)介绍及各类型实体比较的更多相关文章

  1. 数据传输对象(DTO)介绍及各类型实体比较

    数据传输对象(DTO)介绍及各类型实体比较 本文将介绍DDD分层架构中广泛使用的数据传输对象Dto,并且与领域实体Entity,查询实体QueryObject,视图实体ViewModel等几种实体进行 ...

  2. 应用程序框架实战三十六:CRUD实战演练介绍

    从本篇开始,本系列将进入实战演练阶段. 前面主要介绍了一些应用程序框架的概念和基类,本来想把所有概念介绍完,再把框架内部实现都讲完了,再进入实战,这样可以让初学者基础牢靠.不过我的精力很有限,文章进度 ...

  3. 应用程序框架实战三十八:项目示例VS解决方案的创建(一)

    进行项目开发的第一步,是创建出适合自己团队习惯的VS解决方案,虽然我已经提供了项目示例,但毕竟是我创建的,你直接使用可能并不合适,另外你如果尝试模仿重新创建该示例,中间可能碰到各种障碍,特别是项目间的 ...

  4. 应用程序框架实战三十:表现层及ASP.NET MVC介绍(一)

    本文将介绍表现层及ASP.NET MVC的一些要点,特别是ASP.NET MVC的一些抽象和封装技巧,如果你对MVC还不了解,可以参考<ASP.NET MVC4 高级编程>,作者Jon G ...

  5. 微信小程序把玩(三十四)Audio API

    原文:微信小程序把玩(三十四)Audio API 没啥可值得太注意的地方 重要属性: 1. wx.getBackgroundAudioPlayerState(object) 获取播放状态 2.wx.p ...

  6. 应用程序框架实战二十二 : DDD分层架构之仓储(层超类型基础篇)

    前一篇介绍了仓储的基本概念,并谈了我对仓储的一些认识,本文将实现仓储的基本功能. 仓储代表聚合在内存中的集合,所以仓储的接口需要模拟得像一个集合.仓储中有很多操作都是可以通用的,可以把这部分操作抽取到 ...

  7. 【WePY小程序框架实战三】-组件传值

    [WePY小程序框架实战一]-创建项目 [WePY小程序框架实战二]-页面结构 父子组件传值 静态传值 静态传值为父组件向子组件传递常量数据,因此只能传递String字符串类型. 父组件 (paren ...

  8. 应用程序框架实战二十九:Util Demo介绍

    上文介绍了我选择EasyUi作为前端框架的原因,并发放了最新Demo.本文将对这个Demo进行一些介绍,以方便你能够顺利运行起来. 这个Demo运行起来以后,是EasyUi的一个简单CRUD操作,数据 ...

  9. 应用程序框架实战三十七:Util最新代码更新说明

    离上一篇又过去了一个月,时间比较紧,后续估计会更紧,所以这次将放出更多公共操作类及配套的CodeSmith模板,本篇将简要介绍新放出的重要功能,供有兴趣的同学参考. 重要更新 这一次对两个VS解决方案 ...

随机推荐

  1. dede 调用原图的路径

    步骤:1修改include/extend.func.php 添加如下代码: //取原图地址function bigimg($str_pic){$str_houzhi=substr($str_pic,- ...

  2. HTTP协议入门要点

    应用层协议.基于tcp HTTP/0.9 命令 GET 特点 服务器只能回应HTML字符串 服务器发送完毕后就关闭tcp连接 HTTP/1.0 命令 GET POST HEAD 特点 每次通信都必须包 ...

  3. STM32之待机唤醒

    前段时间我稍微涉及节能减排大赛..倡导节能的社会..没错了.你真是太聪明了..知道了我今天要讲关于STM32节能方面的模块..没错..这标题已经告诉你了是吧..哦,对,标题有写..所以..言归正传.至 ...

  4. QT 文件对话框(QFileDialog)

    1.选择文件(上传.打开...) QString QFileDialog::getOpenFileName( QWidget *parent = , //parent,用于指定父组件.注意,很多Qt组 ...

  5. 工作总结_js

    工作至今已经有7个月了,虽然有进步,但是总感觉还是什么都不知道.可能这其中很大一部分还是与自己有关系,遇到自己不知道,问了人,或者百度到了,但是自己没有用心记.平时要用的时候,打开上一个项目,复制粘贴 ...

  6. centos执行yum出现Could not retrieve mirrorlist错误

    具体错误见截图 刚开始以为是DNS配置错误,经检查发现DNS与物理机的DNS配置是一样的,物理机可以解析DNS 搜索资料发现是/etc/nsswitch.conf这个文件的问题 这个文件hosts标签 ...

  7. Principles of measurement of sound intensity

    Introduction In accordance with the definition of instantaneous sound intensity as the product of th ...

  8. 浅谈Android应用保护(一):Android应用逆向的基本方法

    对于未进行保护的Android应用,有很多方法和思路对其进行逆向分析和攻击.使用一些基本的方法,就可以打破对应用安全非常重要的机密性和完整性,实现获取其内部代码.数据,修改其代码逻辑和机制等操作.这篇 ...

  9. 走向面试之数据库基础:一、你必知必会的SQL语句练习-Part 1

    本文是在Cat Qi的参考原帖的基础之上经本人一题一题练习后编辑而成,非原创,仅润色而已.另外,本文所列题目的解法并非只有一种,本文只是给出比较普通的一种而已,也希望各位园友能够自由发挥. 一.三点一 ...

  10. .Net开发笔记(二十一) 反射在.net中的应用

    反射概念在网上到处都有,但是讲到的具体的应用很少,一个重要的原因是现实中真的很少用得到它.引用msdn上对“反射”的解释: "通过 System.Reflection 命名空间中的类以及 S ...