DTO 命名约定、建模和继承

50

我们正在使用AngularJS,C#,ASP.Net Web API和Fluent NHibernate构建Web应用程序。 我们决定使用DTO将数据传输到表示层(角度视图)。 我对DTO的一般结构和命名有些疑惑。 这里有一个示例来说明我的情况。 假设我有一个名为Customer的域实体:

public class Customer
    {
        public virtual int Id { get; set; }
        public virtual string Name { get; set; }
        public virtual Address Address { get; set; }
        public virtual ICollection<Account> Accounts { get; set; }
    }

现在,在我的视图/表示层中,我需要检索不同类型的客户,例如:

1)仅ID和名称     2)ID,名称和地址     3)ID,名称,地址和账户

我已经创建了一组DTO来实现这一点:

public class CustomerEntry
{
    public  int Id { get; set; }
    public  string Name { get; set; }
}

public class CustomerWithAddress : CustomerEntry
{
    public AddressDetails Address { get; set; }
}

public class CustomerWithAddressAndAccounts : CustomerWithAddress
{
    public ICollection<AccountDetails> Accounts { get; set; }
}

AddressDetails和AccountDetails是数据传输对象(DTO),它们具有相应领域实体的所有属性。

这对于查询和数据检索很有效;问题在于我该使用什么进行插入和更新。在创建新客户记录时,名称和地址是必填项,而账户则是可选的……因此,换句话说,我需要一个具有所有客户属性的对象。因此,存在以下困惑:

1)我用什么进行插入和更新? CustomerWithAddressAndAccounts DTO中包含了一切,但其名称似乎有点笨重,不太适合用于插入/更新。

2)我是否应创建另一个DTO?如果这样做,那不是重复了吗?因为新的DTO将与CustomerWithAddressAndAccounts完全相同。

3)最后但并非最不重要的,上述DTO继承结构是否适合此需求?是否有其他建模方法?

我已经研究过关于此主题的其他帖子,但没有取得太大进展。我学到的一个东西是避免在类名中使用“DTO”后缀。我认为这似乎有点多余。

希望听听您的想法。

谢谢

3个回答

25
建议每个实体只拥有一个DTO类,以DTO为后缀,例如,对于Customer实体,使用CustomerEntryDTO(但是根据选择和需求,可以使用继承层次结构)。此外,添加一个抽象的DTOBase基类或接口;不要为每个Address、Account和其他属性在子DTO中使用如此深的继承层次结构。相反,如果可能的话,将这些属性包含在同一个CustomerEntryDTO类中,如下所示:
[Serializable]
public class CustomerEntryDTO : DTOBase, IAddressDetails, IAccountDetails
{
    public  int Id { get; set; }
    public  string Name { get; set; }
    public AddressDetails Address { get; set; } //Can remain null for some Customers
    public ICollection<AccountDetails> Accounts { get; set; } //Can remain null for some Customemer
}

此外,您的DTO应该是可序列化的,以便在进程边界传递。关于DTO模式的更多信息,请参阅以下文章:数据传输对象 MSDN

编辑: 如果你不想将某些属性发送到网络上(我知道你需要有条件地这样做,因此需要进一步探索),你可以使用类似于NonSerialized的属性将它们从序列化机制中排除在外(但它仅适用于字段而不是属性,请参阅使用属性的解决方法文章:NonSerialized on property)。 你也可以创建自己的自定义属性,例如ExcludeFromSerializationAttribute,并将其应用于你不想根据某些规则/条件每次都要发送的属性。另请参见:Conditional xml serialization

编辑2: 使用接口将一个CustomerEntryDTO类中的不同属性分离出来。在Google或MSDN上查看接口隔离原则。我稍后会尝试提供一个示例说明。


感谢您的回复。使用多个DTO类的动机之一是在定义DTO用途时更具表现力。此外,如果您只需要Id和Name,那么是否应该将完整的CustomerEntryDTO(如您所描述)发送到网络上? - Sennin
3
回答不错,但我认为在DTO上应用接口是没有必要的。那有什么用呢? 接口隔离原则对于具有实际逻辑的对象而言非常有效,而不是针对简单数据结构。 - Arman
4
在.NET世界中,在类名后缀上加DTO是愚蠢的,没有必要。我们倾向于使用“实体”来表示存储库对象,“模型”来表示API返回对象,而对于服务对象通常不使用任何前缀。公平地说,所有这些层都使用DTO,所以我们应该开始将实体命名为XptoDtoEntities吗?!放弃那个DTO名称,它很丑。 - MeTitus
2
@Marco,当你将视图模型映射到实体时,后缀非常有用,因为在此过程中导入两个层的类时不希望出现名称冲突。 - Felix K.
3
我只是引用了你的话并证明了你的错误。讨论结束。 - Felix K.
显示剩余9条评论

2

我应该使用什么来进行插入和更新操作?

  1. 服务操作通常与业务操作密切相关。业务语言不会使用“插入”和“更新”的术语,服务也是如此。

  2. 客户管理服务可能会有一些注册(Register)操作,需要提供客户名称和其他可选参数。

我需要创建另一个DTO吗?

是的,您应该创建另一个DTO。

有时服务操作协定可能已经足够了,没有必要为特定操作定义单独的DTO:

function Register(UserName as String, Address as Maybe(of String)) as Response

但大多数情况下,即使只有一个服务操作,定义一个单独的DTO类也是更好的选择:

class RegisterCommand
    public UserName as String
    public Address as Maybe(of String)
end class

function Register(Command as RegisterCommand) as Response
RegisterCommand数据传输对象(DTO)与CustomerWithAddress DTO非常相似,因为它们具有相同的字段,但实际上这两个DTO具有非常不同的含义,不能相互替代。
例如,CustomerWithAddress包含AddressDetails,而一个简单的String地址表示可能足以注册客户。
为每个服务操作使用单独的DTO编写需要更多时间,但更易于维护。

-3
根据您的第一项,对于插入和更新操作最好使用命令模式。根据CQRS,您不需要DTO。考虑以下架构: CQRS — basic patterns 通过blogs.msdn.com

7
对于查询方面,您仍需要一个DTO,因此这不是一个很好的答案。 - Steven Liekens
5
这就像建议某人购买摩托车,而他想要的是汽车。 - yakya
1
@sotn 摩托车非常有趣,嗯。 - Vitaly Chirkov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接