了解最新技术文章
我们从一开始就使用 Protobuf 不仅仅是一种序列化机制。它提供了一种语言来建模领域。这种能力在基于领域驱动设计方法(又名 DDD)的项目中特别有用。
领域模型的定义特征是通用语言——一种在整个领域中使用和认可的语言:领域专家、程序员和代码中。域模型由许多组件组成,但语言为所有组件奠定了基础。值对象有助于将语言转换为代码。
值对象是一种简单的类型,表示任何类型的逻辑上不可分割的域信息,例如EmailAddress
、PhoneNumber
、LocalDateTime
、Money
等。与实体不同,值对象仅由值本身标识,即它们没有指定的 ID。这些对象是不可变的,通常不会太大。除了对数据本身的封装外,这些类型还可以包含验证逻辑、对数据的基本操作、字符串表示等。例如,该类型LocalDateTime
可以提供诸如add(Duration)
在给定持续时间后计算本地日期和时间等方法通过。结果是 的另一个实例LocalDateTime
,而原始对象保持不变。
如前所述,值对象主要用于将领域语言集成到应用程序的类型系统中。将值对象视为虚拟数据结构是一种常见的误解。在实践中,值对象是逃避所有与数据结构相关的问题的方法,例如数据不一致和贫血模型。
在 Protobuf 中创建值对象很方便,因为:
让我们看看如何在 Protobuf 中实现 Value Object。
message EmailAddress {
字符串值 = 1 [
(必需)= true,
(模式).regex = ".+@.+\..+"
];
}
根据该声明,Protobuf 编译器将生成一个类型,其实例由 value 字段进行比较。例如,在 Java 中,该类将具有生成的equals()
方法。
请注意,(required)
和(pattern)
选项是验证库(它是Spine 事件引擎框架的一部分)提供的对 Protobuf 的扩展。
此外,在某些目标语言中,例如 Java,生成的 Protobuf 类型默认是不可变的。不幸的是,其他一些语言,例如 JavaScript 和 Dart,只支持可变类型。然而,对于 Dart 来说,一个社区驱动的解决方案似乎即将到来。该immutable_proto
包实现了 Protobuf 中不可变类型的代码生成。我们还没有尝试过,因为该库仍处于最早的形式,但其他工程师感受到我们的痛苦并尝试对此做些什么的想法让我们感到温暖。
有关 Protobuf 的不变性的进一步阅读,请参阅我们之前的文章。
上面声明的EmailAddress
类型有一个字符串字段,通过正则表达式进行简单验证。此外,应填写值字段。这个验证 API 是我们努力改进 Protobuf 代码生成的一部分。现在,我们为 Java 和 Dart 生成验证代码,以覆盖后端和前端。稍后,其他目标语言可能会加入该俱乐部。
验证规则是根据选项确定的,例如(required)
和(pattern)
。在构建时,我们添加了额外的代码,用于根据这些规则验证消息值。构建消息时会自动触发代码。没有更多容易忘记的验证步骤!
我们将在本系列的下一篇文章中更详细地讨论我们的验证机制的功能和内部结构。
值对象的一个重要部分是它的行为。OOP 极大地影响了程序员的思维方式,同时为每件小事创建实用程序类和方法的需求使编写和理解代码感到烦恼和复杂化。快速创建域类型的能力很好,但我们也希望它们便于在代码中“讨论”。取而代之的是user.getAddress().getCountry()
,我们希望能够编写user.country()
。
在 Java 中,Protobuf 生成不可扩展的类,这使得向它们添加行为变得困难。我们通过定义选项 (is) 解决了这个问题。它采用我们想要标记 Protobuf 消息的 Java 接口的名称。此类接口可能包括默认方法,将行为添加到值对象。我们的自定义 Protobuf 编译器插件修改生成的代码,以便 Proto-types 实现分配的接口。这是它的工作原理。
消息用户 {
option (is).java_type = "UserMixin";
UserId id = 1 [(必需) = true];
// 主要帐单地址。
地址address = 2 [(必填) = true];
...
}
这是UserMixin
声明:
public interface UserMixin extends UserOrBuilder {
/**
* 获取该用户的居住国。
*/
默认国家 country() {
return getAddress().getCountry();
}
...
}
我们的 Protobuf Compiler 插件看到该(is).java_type
选项并将指定的接口添加到生成的类的已实现接口列表中。注意 mixin 接口OrBuilder
默认扩展了 Protobuf 编译器生成的接口。这个小技巧允许我们使用为消息字段生成的 getter 方法,例如getAddress()
.
值对象的一个特例是标识符。
领域驱动设计专家建议为每个实体类型使用单独的 ID 类型。这在 DDD 的现代响应式版本中尤为重要,其中实体处理消息(命令或事件)。如果它们的类型相同,使用原始类型或字符串作为标识符可能会导致与参数混淆。例如,如果我们有customerId
, orderId
, userId
, 并且它们都是long
or String
,很容易机械地弄乱它们的顺序:
completeOrder(String userId, String customerId, String orderId);
此外,对 ID 使用原始类型可能会带来一系列令人不快的副作用。例如,long
ID 在用于快速增长的实体类型(想想领域事件、用户会话等)时可能会溢出。
类型安全的 ID 为这些问题提供了解决方案。首先,类型安全的 ID 不能被意外混淆,因为编译器很容易捕捉到这样的错误。除此之外,使用类型标识符的代码更紧凑。它更容易阅读和理解。例如,让我们将后者与相同的调用进行比较,但使用了类型化的 ID:
completeOrder(UserId 用户, CustomerId 客户, OrderId 订单);
在类型名称中说过Id
一次之后,我们可以轻松地Id
从每个参数名称中删除前缀。我们甚至可以这样写:
completeOrder(UserId u, CustomerId c, OrderId o);
类型安全标识符的另一个好处在于它们的结构。在基本情况下,实体的 ID 类型如下所示:
消息客户 ID {
字符串 uuid = 1;
}
但是,通过隐藏标识符实现的类型,我们可以在必要时对其进行扩展。例如,整合不同厂商的数据。我们可以通过oneof
构造来做到这一点:
消息 CustomerId { oneof
kind {
uint64 代码 = 1;
电子邮件地址电子邮件 = 2;
字符串电话 = 3;
}
}
由于 Protobuf 专门设计为允许对类型进行附加更改而无需任何迁移,更改 ID 结构可能就像向 ID 类型添加额外字段一样简单。
值对象作为一个整体是一个很棒的概念。它通过形成强类型模型而不是基于原语的模型来帮助开发人员将无处不在的语言带入代码中并避免错误。
Protobuf 有助于简化值对象的创建和维护。将域清晰度引入代码的简单值对象已经是一个很大的改进。再加上类型化的标识符,它们带来了额外的好处。多亏了 Protobuf,此类类型可以声明一次并分布在整个系统中,从而弥合不同组件之间的语言障碍。
通过 Java 接口添加额外的定制代码生成层,包括验证规则、行为和类型分组,我们得到了一个强大的机制,它具有能够维护简单域不变量的强类型模型。
最后,结合领域驱动的软件架构方法,我们得到了一个系统,它从头开始帮助开发人员一次只解决一个问题,从简单的机械问题,价值对象级别,到复杂的业务更高层次的要求。
上一篇:Protobuf - 序列化和超越。第 2 部分:不变性
下一篇:没有了!