【问题标题】:Multiple Dtos for same entity同一实体的多个 Dto
【发布时间】:2018-11-06 16:19:02
【问题描述】:

对不同 API 端点中的同一实体使用多个 DTO 是否是一种好习惯。例如: 我有一个接受以下 Dto 的 api 端点:

public class AddressDto
{
    public string City { get; set; }
    public string Country { get; set; }
    public string Contact { get; set; }
    public string Street1 { get; set; }
    public string Street2 { get; set; }
    public string State { get; set; }
    public string Zip { get; set; }
}

现在有第二个 Api 接受相同的 dto,但在那个 api 调用中我只使用 Streer1, Street2, Contact 所有其他的都被忽略了。

我是否应该为第二个api endpoint 再写一个DTO,比如:

public class AddressDtoForSecondAPI
{
    public string Contact { get; set; }
    public string Street1 { get; set; }
    public string Street2 { get; set; }
}

【问题讨论】:

  • SOLID 中的inheritance 和/或interface segregation principle 怎么样?
  • 两个地址是否都在同一个数据库表中?
  • 同意@S.Akbari 也考虑数据完整性。虽然我不确定这是否可能。不管是什么原因,但看起来有风险(如果可能的话)。
  • @S.Akbari,你是对的。但我最终会为同一个实体提供多个类。这是一个好习惯吗?我应该为单个实体创建 3-4 个类吗?
  • @danish,是的,它最终会出现在同一个数据库表中。

标签: c# .net asp.net-web-api2 api-design


【解决方案1】:

简而言之,是的,可以接受。


但是,正如您在 cmets 和其他答案中看到的那样,并非所有人都同意这里。所以让我解释一下我的答案。

论据 1 - 误导消费者

现在有第二个 Api 接受相同的 dto,但在那个 api 调用中我只使用 Streer1, Street2, Contact 所有其他的都被忽略了。

这里的问题之一是明确你的意图。如果您允许消费者向您发送完整的AddressDTO,但随后只使用属性的子集,那么您就是在误导您的消费者。你让他们认为其他属性是相关的。

这实际上与以下内容相同:

public int AddNumbersTogether(int a, int b, int c, int d)
{
    return a + c + d; //we ignore b
}

b 没有理由存在。当AddNumbersTogether(1,2,3,4) 返回值8 时,任何使用此方法的人都会摸不着头脑。语法与行为相矛盾。

是的,省略一个未使用的方法参数比开发第二个 DTO 更容易。但是您需要在这里保持一致并坚持相同的原则:不误导消费者

论点 2 - DTO 不是实体

您的消费者与您的 API 的交互需要在消费者不知道您的数据库记录结构的情况下进行。

这就是为什么您一开始就使用 DTO 而不是实体类的原因!您在执行操作和存储该操作的数据之间提供了逻辑分离。

消费者并不关心数据存储在哪里。无论您是将街道存储在与地址相同的表中,还是完全存储在不同的表(或数据库)中,在调用 API 方法的消费者范围内都无关紧要

论点 3 - 反击 S.Akbari

继承和/或SOLID中的接口隔离原则呢? – S.Akbari

对于这种特殊情况,这些不是有效的参数。

继承是一种有缺陷的方法。是的,您可以技术上在发布的示例代码中执行AddressDto : AddressDtoForSecondAPI 之类的操作,但这是一种巨大的代码气味。
当需要第三个 DTO 时会发生什么,例如一个只使用邮政编码和城市名称的地方?你不能让AddressDto从多个来源继承,AddressDtoForSecondAPI和新创建的AddressDtoForThirdAPI之间没有逻辑重叠。

接口不是这里的解决方案。是的,从技术上讲,您可以使用适当的字段创建IAddressDtoForSecondAPIIAddressDtoForThirdAPI 接口,然后执行AddressDto : IAddressDtoForSecondAPI, IAddressDtoForThirdAPI 之类的操作。但是,这又是相同的大量代码气味。

如果第二个和第三个变体有一些共享属性和一些单独的属性,会发生什么?如果你应用接口隔离,那么重叠的属性需要自己抽象到一个接口中。
如果出现第四个变体,它具有一些与第二个变体相同的属性,一些与第三个变体,一些与第二个和第三个变体,以及一些单独的属性,那么你将需要创建更多接口!

给定同一实体的足够多的变体并重复应用界面隔离原则;你最终会得到一个实体的每个属性的接口;这需要大量的样板化。你最终会得到类似的东西:

public class AddressDto : IAddressCity, IAddressCountry, IAddressContact, IAddressStreet1, IAddressStreet2, IAddressState, IAddressZip
{
    public string City { get; set; }
    public string Country { get; set; }
    public string Contact { get; set; }
    public string Street1 { get; set; }
    public string Street2 { get; set; }
    public string State { get; set; }
    public string Zip { get; set; }
}

想象一下,所有课程都必须这样做;因为相同的原则适用于 API 使用的每个 DTO。

参数 4 - DRY 不适用于此处

我有点明白您为什么担心创建两个类。很可能,您的脑海中出现了 DRY/WET 错误标志。

避免 WET 是一种很好的反射;但你不能总是听它。因为如果您要真正避免重复,那么您实际上也不应该创建单独的实体和 DTO 类,因为它们通常是相互复制/粘贴的。

DRY 不是绝对的。以实体/DTO 为例,这里有一个平衡的考虑:

  • 您想不惜一切代价避免重复吗? (= 干)
  • 您想将您的 DAL 与您的 API 逻辑分开吗? (= 关注点分离)

在这种情况下,后者通常会胜出。

同样的论点适用于你的情况。在 DRY 之后的论点反对(这是我刚刚列出的论点)在这种情况下超过了遵循 DRY 的好处。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-01-07
    • 2021-06-16
    • 1970-01-01
    • 1970-01-01
    • 2016-12-06
    相关资源
    最近更新 更多