你的位置:首页 > 软件开发 > ASP.net > 应用程序框架实战三十四:数据传输对象(DTO)介绍及各类型实体比较

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

发布时间:2015-03-31 04:00:23
本文将介绍DDD分层架构中广泛使用的数据传输对象Dto,并且与领域实体Entity,查询实体QueryObject,视图实体ViewModel等几种实体进行比较。领域实体为何不能一统江湖?  当你阅读我或其它博主提供的示例代码时,会发现几种类型的实体,这几种实体初步看上去区别不大 ...

  本文将介绍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)介绍及各类型实体比较

关键词:

*特别声明:以上内容来自于网络收集,著作权属原作者所有,如有侵权,请联系我们: admin#shaoqun.com (#换成@)。

可能感兴趣文章

我的浏览记录