1-1-1 ベストプラクティス

すべての proto 定義は、ファイルごとに1つのトップレベル要素とビルドターゲットを持つべきです。

1-1-1 ベストプラクティスは、すべての proto_library および .proto ファイルを合理的な範囲で可能な限り小さく保つことであり、理想は以下の通りです。

  • 1つの proto_library ビルドルール
  • 1つのソース .proto ファイル
  • 1つのトップレベルエンティティ (メッセージ、enum、または拡張)

メッセージ、enum、拡張、サービスの数を合理的に可能な限り少なくすることで、リファクタリングが容易になります。分離されているファイルを移動する方が、他のメッセージが含まれるファイルからメッセージを抽出するよりもはるかに簡単です。

このプラクティスに従うことで、実際に推移的依存関係のサイズを減らし、ビルド時間とバイナリサイズを改善できます。あるコードが1つの enum のみを使用する必要がある場合、1-1-1 設計ではその enum を定義する .proto ファイルのみに依存することで、同じファイルで定義されている別のメッセージによってのみ使用される可能性のある大量の推移的依存関係を偶発的に引き込むことを避けることができます。

1-1-1 の理想が不可能な場合(循環依存)、理想的ではない場合(概念的に密接に結合しており、共存することで可読性が向上するメッセージ)、あるいは一部のデメリットが当てはまらない場合(.proto ファイルにインポートがない場合、推移的依存関係のサイズに関する技術的な懸念がない)もあります。他のベストプラクティスと同様に、ガイドラインから逸脱すべきかどうかは適切な判断力を使用してください。

proto スキーマファイルのモジュール性が重要となる点の1つは、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);
}

サービス定義と各メッセージ定義はそれぞれ自身のファイルにあり、他のスキーマファイルからメッセージにアクセスするためにインクルードを使用します。

この例では、StudentStudentId、および FullName は、リクエストとレスポンス間で再利用可能なドメイン型です。トップレベルのリクエストおよびレスポンスのプロトは、各サービスとメソッドに固有です。

後で FullName メッセージに middle_name フィールドを追加する必要がある場合でも、個々のトップレベルメッセージすべてをその新しいフィールドで更新する必要はありません。同様に、Student をより多くの情報で更新する必要がある場合、すべてのリクエストとレスポンスが更新されます。さらに、StudentId は複数パーツの ID に更新されるかもしれません。

最後に、StudentId のような単純な型でさえもメッセージとしてラップすることで、セマンティクスと統合されたドキュメントを持つ型を作成したことになります。FullName のようなものについては、この PII がどこにログされるかに注意する必要があります。これは、これらのフィールドを複数のトップレベルメッセージで繰り返さないもう一つの利点です。これらのフィールドを一箇所で機密情報としてタグ付けし、ログから除外することができます。