服务层应该接受来自控制器的 DTO 或自定义请求对象吗?

Posted

技术标签:

【中文标题】服务层应该接受来自控制器的 DTO 或自定义请求对象吗?【英文标题】:Should service layer accept a DTO or a custom request object from the controller? 【发布时间】:2019-05-26 17:53:06 【问题描述】:

正如标题所示,设计服务层时的最佳做法是什么?我确实理解服务层应该始终返回一个 DTO,以便将域(实体)对象保留在服务层中。但是控制器的服务层输入应该是什么?

下面我提出了三个我自己的建议:

方法一: 在这种方法中,域对象(Item)被保存在服务层中。

class Controller

    @Autowired
    private ItemService service;

    public ItemDTO createItem(IntemDTO dto)
    
        // service layer returns a DTO object and accepts a DTO object
        return service.createItem(dto);
    

方法二: 这是服务层接收自定义请求对象的地方。我在 AWS Java SDK 和 Google Cloud Java API 中广泛看到了这种模式

class Controller

    @Autowired
    private ItemService service;

    public ItemDTO createItem(CreateItemRequest request)
    
        // service layer returns a DTO object and accepts a custom request object
        return service.createItem(request);
    

方法三: 服务层接受 DTO 并返回域对象。我不喜欢这种方法。但它在我的工作场所被广泛使用。

class Controller

    @Autowired
    private ItemService service;

    public ItemDTO createItem(CreateItemRequest request)
    
        // service layer returns a DTO object and accepts a DTO object
        Item item = service.createItem(request);
        return ItemDTO.fromEntity(item);
    

如果上述三种方法都不正确或不是最好的方法,请告诉我最佳做法。

【问题讨论】:

我不确定这个问题是否与domain-driven-design 有关。您能否详细说明 DDD 是如何出现在这里的。 对不起。我已经从标签列表中删除了 DDD 【参考方案1】:

我来自C# 背景,但这里的概念保持不变。

在这种情况下,我们必须将参数/状态从应用层传递到服务层,然后从服务层返回结果,我倾向于遵循关注点分离。服务层不需要知道你的应用层/控制器的Request参数。同样,您从服务层返回的内容不应与您从控制器返回的内容相耦合。这些是不同的层次、不同的要求、不同的关注点。我们应该避免紧耦合。

对于上面的例子,我会这样做:

class Controller

     @Autowired
     private ItemService service;

     public ItemResponse createItem(CreateItemRequest request)
     
        var creatItemDto = GetDTo(request);
        var itemDto = service.createItem(createItemDto);
        return GetItemResponse(itemDto);
    

这可能感觉需要做更多的工作,因为现在您需要编写额外的代码来转换不同的对象。但是,这为您提供了极大的灵活性,并使代码更易于维护。例如:与CreateItemRequest 相比,CreateItemDto 可能具有额外的/计算字段。在这种情况下,您不需要在 Request 对象中公开这些字段。您只向客户公开您的Data Contract,仅此而已。同样,您只将相关字段返回给客户端,而不是您从服务层返回的内容。

如果你想避免在DtoRequest objects 之间手动映射,C# 有像AutoMapper 这样的库。在java世界中,我相信应该有一个等价物。可能是ModelMapper 可以帮忙。

【讨论】:

我想你可能误会了。 createItemDto 必须是 service.createItem() 的输入,而不是 request @Vino,这是一个错字。已经修好了。谢谢! +1。在作为前端 API 的典型后端中,响应由两部分组成:HTTP 状态代码和响应标头(如果有),以及以 JSON 或 XML 格式传递的值。输出数据是在 Serializer 的帮助下得出的,用于该特定服务。【参考方案2】:

从概念上讲,您希望能够跨表示层和通过不同的访问端口重用服务/应用程序层(例如,控制台应用程序通过网络套接字与您的应用程序通信)。此外,您不希望每个域更改都冒泡到应用层之上的层中。

控制器在概念上属于表示层。因此,您不希望应用程序层与在定义控制器的同一概念层中定义的合同耦合。您也不希望控制器依赖于域,或者当域时它可能必须更改变化。

您需要一个解决方案,其中应用层方法协定(参数和返回类型)以任何 Java 本机类型或服务层边界中定义的类型表示。

如果我们从 Vaughn Vernon 那里获取an IDDD sample,我们可以看到他的应用程序服务方法契约是在 Java 原生类型中定义的。鉴于他使用了 CQRS,他的应用程序服务命令方法也不会产生任何结果,但我们可以看到 query methods 确实返回了应用程序/服务层包中定义的 DTO。

在上面列出的3种方法中,哪些是正确的/错误的?

#1 和 #2 都非常相似,从依赖关系的角度来看可能是正确的,只要在应用层包中定义了 ItemDtoCreateItemRequest,但我更喜欢 #2,因为输入数据类型是根据用例命名的,而不仅仅是它处理的实体类型:entity-naming-focus 更适合 CRUD,因此您可能会发现很难为其他用例方法的输入数据类型找到好的名称对同一种实体进行操作。 #2 也通过 CQRS(命令通常发送到命令总线)得到普及,但不是 CQRS 独有的。 Vaughn Vernon 在IDDD samples 中也使用了这种方法。请注意,您所说的 request 通常称为 command

但是,#3 并不理想,因为它将控制器(表示层)与域耦合。

例如,某些方法接收 4 或 5 个参数。根据 Eric Evans 在 Clean Code 中的说法,必须避免使用此类方法。

这是一个很好的指导方针,我并不是说无法改进示例,但请记住,在 DDD 中,重点是根据通用语言 (UL) 命名事物并将其作为尽可能密切。因此,仅仅为了将论点组合在一起而将新概念强加到设计中可能是有害的。具有讽刺意味的是,尝试这样做的过程仍然可能提供一些很好的见解,并允许发现可以丰富 UL 的被忽视和有用的领域概念。

PS:Robert C. Martin 写的是 Clean Code,而不是 Eric Evans,后者以蓝皮书闻名。

【讨论】:

所以我在理解您的答案时遇到了问题。在上面列出的 3 种方法中,哪些是正确的/错误的?我还查看了您链接的代码示例。我不完全确定其中使用的一些做法。例如,某些方法接收 4 或 5 个参数。根据 Eric Evans 在 Clean Code 中的说法,必须避免使用此类方法。 @Vino FYI - 自从您发表评论后,我更新了答案。 感谢您的洞察力。是的,我把作者的名字弄混了。 @Vino 好吧,我的回答是否涵盖了您的问题?您还有其他问题吗?

以上是关于服务层应该接受来自控制器的 DTO 或自定义请求对象吗?的主要内容,如果未能解决你的问题,请参考以下文章

根据 DTO、实体模型或其他东西验证服务层中的数据?

我应该在哪里进行转换:域对象<-> DTO?

C# 数据层和 Dto

我应该用Serializable标记spring mvc DTO吗? [复制]

ASP.NET MVC 服务层输入输出数据

应该使用哪个层从域对象转换为 DTO