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

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

「1-1-1」のベストプラクティスは、.protoファイルごとに1つのトップレベルエンティティ(メッセージ、enum、または拡張機能)を持つように定義を構造化することを提唱しており、これは単一のproto_libraryビルドルールに対応します。このアプローチは、小さくモジュール化されたproto定義を促進します。主な利点には、リファクタリングの簡素化、ビルド時間の潜在的な改善、推移的依存関係の最小化によるバイナリサイズの縮小などがあります。


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がどこにログされるかに注意する必要があります。これは、これらのフィールドを複数のトップレベルメッセージで繰り返さないもう1つの利点です。これらのフィールドを1か所で機密としてタグ付けし、ロギングから除外することができます。