最佳实践 - 多层架构和数据传输对象(DTOs)

18

在阅读了Stackoverflow上一些Q&A之后,我仍然对如何在我的Web应用程序中正确实现DTO感到困惑。我的当前实现是基于Java EE的多层架构(包括持久化、服务和表示层),但具有被所有层使用的“通用”包,其中包含(除其他外)域对象。在这种情况下,这些层不能真正被视为独立的。

在重新设计中,我计划逐步删除“common”包。

问题

在重新设计过程中,我遇到了各种挑战/问题:

  1. 假设持久层将使用一个名为 myproject.persistence.domain.UserEntity (基于JPA的实体)的类来存储和加载数据到/从数据库中。为了在视图中显示数据,我会提供另一个类myproject.service.domain.User。在哪里进行转换?用户服务是否负责在两个类之间进行转换?这样做是否真的有助于改善耦合度?
  2. User类应该长成什么样子?它应该只包含getter以保持不变吗?对于视图编辑现有用户(创建新的User,使用现有User对象的getter等),这是否会很麻烦?
  3. 我应该使用相同的DTO类(User)发送请求到服务以修改现有用户/创建新用户,还是应该实现其他类?
  4. 通过使用myproject.service.domain中的所有DTO,表示层不会非常依赖于服务层吗?
  5. 如何处理自己的异常?我的当前方法重新抛出大多数“严重”异常,直到它们被表示层处理(通常记录下来并通知用户出现了问题)。一方面,我又面临着再次共享包的问题。另一方面,我仍然不确定这是否可以被认为是“最佳做法”。有什么想法吗?

我投票关闭此问题,因为它是一个架构问题而不是编程问题。 - BetaRide
2个回答

19

在不同层之间具有某些包是很常见的,但通常只用于横切关注点,例如日志记录。您的模型不应由不同的层共享,否则对模型的更改将需要更改所有这些层。通常,您的模型是较低的层,靠近数据层(上面,下面或交织在一起,具体取决于方法)。

数据传输对象(DTO)如其名称所示,是用于传输数据的简单类。因此,它们通常用于在层之间通信,特别是当您有通过消息而不是对象进行通信的SOA架构时。 DTO应该是不可变的,因为它们仅存在于传输信息的目的,而不是改变信息。

您的域对象是一件事情,您的DTO是另一件事情,您需要在演示层中使用的对象是另一件事情。然而,在小型项目中,实施所有这些不同集合并在它们之间进行转换可能不值得那份努力。这取决于您的要求。

您正在设计Web应用程序,但询问自己“我是否可以通过桌面应用程序切换我的Web应用程序?我的服务层是否真正不知道我的演示逻辑?”可能有益于您的设计。以这种方式思考将指导您走向更好的架构。

回答您的问题

  1. 假设持久化层使用类 myproject.persistence.domain.UserEntity (基于JPA实体)来存储和加载数据到/从数据库。要在视图中显示数据,我会提供另一个类myproject.service.domain.User。我应该在哪里进行转换?用户服务负责在两个类之间转换吗?这真的有助于改善耦合吗?

服务层知道其类(DTO)和其下面的层(假设是持久层)。因此,是的,该服务负责在持久性和自身之间进行翻译。

  1. User类应该长什么样?它只应包含getter以保持不可变性吗?对于查看编辑现有用户(创建新用户,使用现有用户对象的getter等),这是否会很麻烦?
DTO的理念是仅在传输时使用它们,因此不需要创建新用户等操作。对此,您需要不同的对象。
应该使用相同的DTO类(User)发送请求到服务来修改现有用户/创建新用户还是应该实现其他类?
服务方法可能表达操作,DTO只包含数据作为其参数。另一种选择是使用命令来表示操作,并且还包含DTO。这在SOA架构中很受欢迎,其中您的服务可能仅是一个命令处理器,例如具有一个单独的Execute操作,以ICommand接口作为参数(与每个命令都有一个操作相反)。
如果在myproject.service.domain中使用所有DTO,那么展示层是否会非常依赖于服务层?
是的,覆盖服务层的层将依赖于它。这就是想法。好处是只有该层依赖于它,上下层都不会受影响(与从每个层使用域类发生的情况不同)。
如何处理我的自定义异常?我的当前方法重新抛出大多数“严重”的异常,直到它们由演示层处理(通常记录并告知用户出现了问题)。一方面,我有一个共享的包的问题。另一方面,我仍然不确定这是否可以被认为是“最佳实践”。有什么想法?每个层级都可以有自己的异常。它们从一个层级流动到另一个层级中,封装在下一种异常中。有时,它们将由一个层级处理,该层级将执行某些操作(例如日志记录),然后可能抛出一个不同的异常,上一层必须处理。其他时候,它们可能会被处理并解决问题。例如,考虑连接到数据库时出现的问题。它将引发异常。您可以处理它,并决定在1秒钟后重试,然后可能成功,因此异常不会向上流动。如果重试也失败,异常将被重新抛出,可能一直流到呈现层,您可以优雅地通知用户并要求他重试该层。

非常感谢您详细的回答。我认为这对我进一步设计架构非常有帮助。 - nils
这部分仍然困扰着我: “服务层知道它的类(DTO)和它下面的层(比如持久化)。所以,是的,服务层负责在持久化和自身之间进行翻译。”在“服务”中会有多个层:DAO、web/rest/service资源、业务逻辑层。具体应该在哪里进行翻译? - Sean
1
@SeanCoetzee DTO应该是服务层的职责,因为它们只存在于轻量级数据传输中。因此,翻译应该由该层完成。DAO(或任何持久性机制)和特别是业务层都不应该知道它们的存在,因为它们是较低的层。 - jnovo
在多层架构中,如果表示层从服务层获取实体,这是否是非法的?我的意思是,这会使表示层依赖于持久化层吗? - Arash

5
松散耦合确实是推荐的方式,这意味着您将最终在业务逻辑中编写巨大、无聊且难以维护的转换器。是的,它们属于业务逻辑:DAO和视图之间的层。因此,业务层最终将依赖于DAO DTO和视图DTO,并且将充满转换器类,淡化您对实际业务逻辑的看法...
如果您可以使用不可变的视图DTO,那就太好了。您使用的序列化库可能需要它们具有setter方法。或者,如果它们具有setter方法,则可能更容易构建它们。
我已经成功地使用相同的DTO类用于视图和DAO。这很糟糕,但老实说,如果没有这种紧密耦合,我并没有感到系统更加解耦,因为业务逻辑,即最重要的部分,必须依赖于任何东西。这种紧密耦合提供了很好的简洁性,并使同步视图和DAO层更容易。我仍然可以使用组合来使某些内容仅针对其中一个层而不在另一个层中看到。
最后,关于异常。这是最外层,即视图层(如果您使用Spring,则是控制器)的责任,捕获从内部层传播的错误,无论是使用异常还是使用特殊的DTO字段。然后,这个最外层需要决定是否通知客户端错误以及如何通知。事实是,直到最内层,您都需要区分外层将需要处理的不同类型的错误。例如,如果在DAO层发生了什么事情,并且视图层需要知道是否返回400还是500,则DAO层将需要向视图层提供所需的信息来决定使用哪一个,并且此信息将需要通过所有中间级别传递,它们应该能够添加自己的错误和错误类型。将IOException或SQLException传播到最外层是不够的,内层还需要告诉外层这是否是预期的错误。悲伤但真实。

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