プロトのベストプラクティス 1/2

protobuf.dev

protobufの公式ドキュメントにベストプラクティスがあったので、まとめていきます。 多いので2回の記事に分けようと思います。

タグ番号を再利用しない

プロトコルバッファでは、各フィールドにはユニークなタグ番号が割り当てられます。

message Person {
  string name = 1;
  int32 age = 2;
  string email = 3;
}

同じタグ番号を再利用してしまうと、逆シリアル化が台無しになってします。例えフィールドが使われていない場合でも使わない。古いコードやログに存在している可能性があるのでデータの不整合や予期しない動作につながる恐れがある。

削除されたフィールドタグ番号を予約する

もし、プロトの定義ファイルから使用されなくなったフィールドを削除する場合、単にフィールドを削除するだけでは不十分です。削除するだけだと、将来的に誤ってそのタグ番号が再利用されてしまう可能性があるため。

ではどうすれば良いかというと削除予定のフィールドのタグ番号をreservedキーワードを使って予約することができます。

message Person {
 reserved 2, 3;
 string name = 1;
 // int32 age = 2; // 削除されたフィールド
 // string email = 3; // 削除されたフィールド
}

タグ番号だけではなく、フィールド名も予約できます。

message Person {
reserved "age", "email";
string name = 1;
}

列挙型(enum)から列挙値を削除する場合も予約する

例えばenum型に定義した列挙値の番号をフィールド名と同じようにreservedキーワードで削除予約を指定します。

// タグ番号を指定
enum Color {
  reserved 2, 3;
  RED = 0;
  GREEN = 1;
  // BLUE = 2; // 削除された列挙値
  // YELLOW = 3; // 削除された列挙値
}

// 列挙値の名前もOK
enum Color {
  reserved "BLUE", "YELLOW";
  RED = 0
  GREEN = 1;
}

フィールドの型を変更しない

一度定義したフィールドの型を変更することは、タグ番号同様互換性の問題が発生し、データの不整合が発生してしまいます。

とはいえ全てのデータ型が対象というわけでもなく、一部例外の型もあるようで

  • int32
  • uint32
  • int64
  • bool

これらは一般的には問題がないそう。

ただ、そうであってもできる限り型の変更は避けた方がベターですね。

必須フィールドを追加しない

proto3から必須のオプション設定であるrequredが削除されました。このためproto単体では必須を指定することができなくなりました。コメントでそのフィールドが必須であることを示すことを薦めていますが、それ自体で必須のバリデーションを設定できないので、protoc-gen-validate を使って必須の設定をする方が良いと思います。

message Person {
 string name = 1 [(validate.rules).string.min_len = 1];
 int32 age = 2 [(validate.rules).int32.gt = 0];
 string email = 3 [(validate.rules).string.email = true];
}

多くのフィールドを含むメッセージを作成しない

メッセージを作るときに各フィールドを追加するんですが、同じメッセージ内に多くのフィールドを書いてしまうと、可読性の低下やメンテナンス性の低下が起きたりします。パフォーマンスにも影響があり、シリアライズ・デシリアライズする時に低下する可能性があるこのこと。

Bad❌

message Person {
 string first_name = 1;
 string last_name = 2;
 string email = 3;
 string phone_number = 4;
 string address_line1 = 5;
 string address_line2 = 6;
 string city = 7;
 string state = 8;
 string country = 9;
 string postal_code = 10;
 // ... 他にも多数のフィールドが続く
}

Good⭕️

message Person {
  string first_name = 1;
  string last_name = 2;
  string email = 3;
  repeated string phone_numbers = 4;
  repeated Address addresses = 5;
}

message Address {
  string line1 = 1;
  string line2 = 2;
  string city = 3;
  string state = 4;
  string country = 5;
  string postal_code = 6;
}

これは一つのグループにまとめれるAddress関係のメッセージをまとめて、繰り返しのフィールドを定義して読みやすくてメンテナンスしやすいコードになっています。

列挙型に未指定の値を含める

これはenum型にデフォルト値として未指定であるというフィールドを追加するという意味です。

未指定値を適切に宣言することで、互換性の問題を回避し、コードの明確性を高めることができます。また、列挙型の進化を容易にし、新しい値の追加に対して柔軟に対応できるようになります。

enum Color {
 COLOR_UNSPECIFIED = 0;
 RED = 1;
 GREEN = 2;
 BLUE = 3;
}