如何识别DTO和价值对象?

247 阅读4分钟

什么是DTO,你如何识别它?

DTO是一个持有原始数据的对象(字符串、布尔值、浮点数、空值、这些东西的数组)。它通过明确声明字段的名称和它们的类型来定义这些数据的模式。它只能保证所有的数据都在那里,仅仅依靠编程语言的严格性:如果一个构造函数有一个类型为string 的必要参数,你必须传递一个字符串,否则你甚至不能实例化这个对象。然而,DTO并没有提供任何保证,从商业角度来看,这些值确实有意义。字符串可能是空的,整数可能是负的,等等。

DTO的类设计有不同的风格。

/**
 * @object-type DTO
 *
 * Using a constructor and public readonly properties:
 */
final class AnExample
{
    public function __construct(
        public readonly string $field,
        // ...
    ) {
    }
}

/**
 * @object-type DTO
 *
 * Using a constructor with private readonly properties
 *  and public getters:
 */
final class AnotherExample
{
    public function __construct(
        private readonly string $field,
        // ...
    ) {
    }

    public function field(): string
    {
        return $this->field;
    }
}

关于DTO的命名:我建议不要在名字中加入 "DTO"。如果你想让它明确是什么类型,可以添加一个注释,或者一个发明的注解(或属性),如@object-type 。这对那些不了解这些对象类型的开发者来说将非常有用。它可能会触发他们去查找关于它的含义的文章(这篇文章,也许:))。

什么是价值对象,如何识别它?

一个值对象是一个包裹一个或多个值或值对象的对象。它保证所有的数据都在那里,同时也保证这些值从领域的角度来看是有意义的。字符串将不再是空的,数字将被验证为在正确的范围内。一个值对象可以通过在构造函数中抛出异常来提供这些保证,构造函数是私有的,迫使客户端使用一个静态的、命名的构造函数。这使得值对象很容易被识别,并与DTO明确区分开来。

final class AnExample
{
    private function __construct(
        private string $value
    ) {
    }

    public static function fromValue(
        string $value
    ): self {
        /*
         * Throw an exception when the value doesn't 
         * match all the expectations.
         */

         return new self($value);
    }
}

DTO只是为你持有一些数据,并为这些数据提供一个清晰的模式,而价值对象也持有一些数据,但提供证据证明这些数据符合预期。当值对象的类被用作参数、属性或返回类型时,你知道你正在处理一个正确的值。

我们应该如何使用这些对象类型?

意义是由使用定义的。如果我们以错误的方式使用 "DTO "和 "价值对象",它们的名字最终会得到不同的含义。这可能就是这两个术语之间的混淆首先产生的原因。

DTOs

DTO只应该在两个地方使用:数据进入应用程序的地方或数据离开应用程序的地方。一些例子。

  1. 当一个控制器收到一个HTTP POST请求时,请求数据可能有任何形状。我们需要从无形状的数据到有模式的数据(经过验证的键和类型)。我们可以使用一个DTO来实现。一个表单库可以根据提交的表单数据来填充这个DTO,或者我们可以使用一个序列化器来将纯文本的请求体转换为一个填充的DTO。
  2. 当我们向网络服务发出HTTP POST请求时,我们可以先在DTO中收集输入数据,然后将其序列化为HTTP客户端可以发送给服务的请求体。
  3. 对于查询来说,情况也类似。这里我们可以使用一个DTO来表示查询结果。作为一个例子,我们可以将一个DTO传递给一个模板,在其基础上渲染一个视图。我们可以使用一个DTO,将其序列化为JSON,并将其作为一个API响应发送回来。
  4. 当我们向网络服务发送HTTP GET请求时,我们可以先将API响应反序列化为一个DTO,这样我们就可以对其应用一个已知的模式,而不是仅仅访问数组键并猜测类型。API客户端包通常为请求和响应提供DTO。

值对象

在我们想验证一个值是否符合我们的期望,并且我们不想再次验证它的时候,就会用到值对象。我们也用它来积累与某个特定值相关的行为。例如,如果我们有一个EmailAddress 值对象,我们知道这个值已经被验证为一个有效的电子邮件地址,所以我们不需要在其他地方再次检查它。我们还可以给对象添加一些方法,例如从电子邮件地址中提取用户名或主机名。

价值对象经常被用于领域模型,因为保证或不变量是其业务的一个重要部分。但它们也可以用在应用程序的任何地方,因为应用程序的每一部分都需要一些方法来集中一些规则,提供正确性的证据,并积累相关行为。

总结

关于价值对象还有很多可说的,但这不是本文的重点(如果你想读更多,可以看看我的《对象设计风格指南》,或者Vaughn Vernon的《实现领域驱动设计》)。目的是为了最清楚地展示DTO和价值对象之间的区别,我希望他们不再被混淆。这里有一个总结表。

一个DTO。

  • 声明并执行数据的模式:名称和类型
  • 对值的正确性不提供任何保证

一个值对象。

  • 包裹了一个或多个值或值对象
  • 提供这些值的正确性的证据