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

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

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


1-1-1 ベストプラクティスは、すべての proto_library と .proto ファイルを可能な限り小さく保つことであり、理想的には

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

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

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

1-1-1の理想が不可能である場合 (循環依存関係)、理想的ではない場合 (非常に概念的に結合されており、同じ場所に配置することで可読性が向上するメッセージ)、または一部の欠点が適用されない場合 (.protoファイルにインポートがない場合、推移的な依存関係のサイズに関する技術的な懸念がない) があります。他のベストプラクティスと同様に、ガイドラインから逸脱する際には適切な判断を下してください。

プロトスキーマファイルのモジュール性が重要な場所の1つは、gRPC定義を作成する場合です。次のプロトファイルのセットは、モジュール構造を示しています。

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か所で機密としてタグ付けし、ログから除外することができます。