1-1-1 最佳实践

所有 proto 定义都应该在一个文件内包含一个顶级元素和一个构建目标。

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

  • 一个 proto_library 构建规则
  • 一个源 .proto 文件
  • 一个顶级实体(message、enum 或 extension)

尽可能合理地减少 message、enum、extension 和 service 的数量,可以使重构更容易。将文件分离后移动它们比从包含其他 message 的文件中提取 message 要容易得多。

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

在某些情况下,1-1-1 的理想状态不可行(循环依赖),或不理想(概念上紧密耦合的 message,放在一起有利于可读性),或者某些缺点不适用(当一个 .proto 文件没有导入时,就不存在传递依赖大小的技术问题)。与任何最佳实践一样,应根据实际情况判断何时偏离此准则。

在创建 gRPC 定义时,proto 模式文件的模块化非常重要。以下 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);
}

服务定义和每个 message 定义都位于各自的文件中,并使用 include 来访问其他模式文件中的 message。

在此示例中,StudentStudentIdFullName 是可在请求和响应中重复使用的领域类型。顶层请求和响应 proto 对于每个 service+method 是唯一的。

如果以后需要向 FullName message 添加 middle_name 字段,则无需更新每个单独的顶层 message 来包含该新字段。同样,如果需要更新 Student 以包含更多信息,所有请求和响应都会获取更新。此外,StudentId 可能更新为一个多部分 ID。

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