1-1-1 最佳实践

所有 proto 定义应在每个文件中具有一个顶级元素和一个构建目标。

“1-1-1”最佳实践主张将定义组织为每个 .proto 文件一个顶级实体(消息、枚举或扩展),对应一个 proto_library 构建规则。这种方法促进了小型、模块化的 proto 定义。主要好处包括简化重构,可能缩短构建时间,以及由于最小化传递依赖而减小二进制文件大小。

原理

1-1-1 最佳实践是尽可能合理地保持每个 proto_library 和 .proto 文件小巧,理想情况是

  • 一个 proto_library 构建规则
  • 一个源 .proto 文件
  • 一个顶级实体(消息、枚举或扩展)

拥有尽可能少的消息、枚举、扩展和服务,可以使重构更容易。当文件分离时,移动文件比从一个包含其他消息的文件中提取消息要容易得多。

遵循此实践可以通过实际上减少传递依赖的大小来帮助缩短构建时间和减小二进制文件大小:当某些代码只需要使用一个枚举时,在 1-1-1 设计下,它只依赖于定义该枚举的 .proto 文件,并避免意外地引入可能仅由同一文件中定义的另一个消息使用的大量传递依赖。

在某些情况下,1-1-1 的理想不可行(循环依赖)、不理想(概念上极度耦合的消息,通过共置具有可读性优势),或者某些缺点不适用(当 .proto 文件没有导入时,没有关于传递依赖大小的技术问题)。与任何最佳实践一样,请在何时偏离指南时做出良好判断。

proto 模式文件的模块化在创建 gRPC 定义时很重要。以下 proto 文件集显示了模块化结构。

student_id.proto

edition = "2023";

package my.package;

message StudentId {
  string value = 1;
}

full_name.proto

edition = "2023";

package my.package;

message FullName {
  string family_name = 1;
  string given_name = 2;
}

student.proto

edition = "2023";

package my.package;

import "student_id.proto";
import "full_name.proto";

message Student {
  StudentId id = 1;
  FullName name = 2;
}

create_student_request.proto

edition = "2023";

package my.package;

import "full_name.proto";

message CreateStudentRequest {
  FullName name = 1;
}

create_student_response.proto

edition = "2023";

package my.package;

import "student.proto";

message CreateStudentResponse {
  Student student = 1;
}

get_student_request.proto

edition = "2023";

package my.package;

import "student_id.proto";

message GetStudentRequest {
  StudentId id = 1;
}

get_student_response.proto

edition = "2023";

package my.package;

import "student.proto";

message GetStudentResponse {
  Student student = 1;
}

student_service.proto

edition = "2023";

package my.package;

import "create_student_request.proto";
import "create_student_response.proto";
import "get_student_request.proto";
import "get_student_response.proto";

service StudentService {
  rpc CreateStudent(CreateStudentRequest) returns (CreateStudentResponse);
  rpc GetStudent(GetStudentRequest) returns (GetStudentResponse);
}

服务定义和每个消息定义都各自在一个文件中,您可以使用 include 来访问来自其他模式文件的消息。

在此示例中,StudentStudentIdFullName 是可在请求和响应中重复使用的领域类型。顶级请求和响应 protos 对于每个服务+方法都是唯一的。

如果以后您需要在 FullName 消息中添加一个 middle_name 字段,则无需使用该新字段更新每个单独的顶级消息。同样,如果您需要使用更多信息更新 Student,则所有请求和响应都会获得更新。此外,StudentId 可能会更新为多部分 ID。

最后,将即使是简单的类型(如 StudentId)包装为消息,意味着您创建了一个具有语义和统一文档的类型。对于像 FullName 这样的内容,您需要小心此 PII 的日志记录位置;这是不在多个顶级消息中重复这些字段的另一个优点。您可以将这些字段在一个地方标记为敏感并将其从日志记录中排除。