Proto のベスト プラクティス

Protocol Buffersの作成に関する厳選されたベストプラクティスを共有します。

クライアントとサーバーは、同時に更新しようとしても、まったく同じタイミングで更新されることはありません。どちらかがロールバックされる可能性があります。互換性のない変更を行っても、クライアントとサーバーが同期しているから大丈夫だと仮定しないでください。

タグ番号を再利用しない

タグ番号は絶対に再利用しないでください。デシリアライズがめちゃくちゃになります。誰もそのフィールドを使っていないと思っても、タグ番号を再利用しないでください。もしその変更が一度でもライブになったことがあれば、どこかのログにプロトのシリアル化されたバージョンが残っている可能性があります。あるいは、別のサーバーに古いコードがあり、それが壊れる可能性があります。

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

使用されなくなったフィールドを削除するときは、将来誰も誤って再利用しないように、そのタグ番号を予約してください。`reserved 2, 3;` だけで十分です。型は不要です(依存関係を削減できます!)。また、現在削除されているフィールド名を再利用しないように、名前を予約することもできます。`reserved "foo", "bar";`。

削除されたEnum値の番号を予約する

使用されなくなったenum値を削除するときは、将来誰も誤って再利用しないように、その番号を予約してください。`reserved 2, 3;` だけで十分です。また、現在削除されている値名を再利用しないように、名前を予約することもできます。`reserved "FOO", "BAR";`。

新しいEnumエイリアスを最後に置くする

新しいenumエイリアスを追加するときは、サービスがそれを取り込む時間を確保するために、新しい名前を最後に配置してください。

元の名前を安全に削除するには(それが交換に使用されている場合、そうあるべきではありませんが)、次の手順を実行する必要があります。

  • 新しい名前を古い名前の下に追加し、古い名前を非推奨にする(シリアライザーは引き続き古い名前を使用します)。

  • すべてのパーサーにスキーマが展開された後、2つの名前の順序を入れ替える(シリアライザーは新しい名前の使用を開始し、パーサーは両方を受け入れます)。

  • すべてのシリアライザーがそのバージョンのスキーマを持つようになったら、非推奨の名前を削除できます。

注意:理論的にはクライアントが交換に古い名前を使用すべきではありませんが、特に広く使用されているenum名の場合、上記の手順に従うことは依然として丁寧な対応です。

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

フィールドの型は、タグ番号の再利用と同様に、デシリアライズを混乱させるため、ほとんど変更しないでください。protobufのドキュメントでは、許容される少数のケース(例えば、`int32`、`uint32`、`int64`、`bool`間の変更)が概説されています。ただし、フィールドのメッセージ型を変更すると、新しいメッセージが古いメッセージのスーパーセットでない限り、破損します

Requiredフィールドを追加しない

必須フィールドは絶対に追加しないでください。代わりに、API契約を文書化するために `// required` を追加してください。必須フィールドは非常に多くの人々に有害と見なされており、proto3からは完全に削除されました。すべてのフィールドをオプションまたは繰り返しにしてください。メッセージ型がどれくらい続くか、そして4年後に論理的に必須ではなくなったにもかかわらず、プロトがまだ必須であると言っている場合、誰かが必須フィールドを空の文字列やゼロで埋めることを余儀なくされるかどうかはわかりません。

proto3には`required`フィールドがないため、このアドバイスは適用されません。

多数のフィールドを持つメッセージを作成しない

「多数」(数百を想定してください)のフィールドを持つメッセージを作成しないでください。C++では、すべてのフィールドは、それが設定されているかどうかにかかわらず、インメモリのオブジェクトサイズに約65ビットを追加します(ポインタに8バイト、そしてフィールドがオプションとして宣言されている場合、フィールドが設定されているかどうかを追跡するビットフィールドにもう1ビット)。プロトが大きくなりすぎると、生成されたコードがコンパイルすらできない場合があります(例えば、Javaではメソッドのサイズに厳密な制限があります)。

Enumに未指定の値を含めるする

Enumは、宣言の最初の値としてデフォルトの `FOO_UNSPECIFIED` 値を含める必要があります。enumに新しい値が追加された場合、古いクライアントはフィールドが未設定と認識し、ゲッターはデフォルト値、またはデフォルトが存在しない場合は最初に宣言された値を返します。proto enumとの一貫した動作のために、最初に宣言されたenum値はデフォルトの `FOO_UNSPECIFIED` 値であるべきであり、タグ0を使用すべきです。このデフォルトを意味のある値として宣言したくなるかもしれませんが、新しいenum値が時間とともに追加されるプロトコルの進化を助けるために、一般原則としてそうすべきではありません。コンテナメッセージの下で宣言されたすべてのenum値は同じC++名前空間にあるため、コンパイルエラーを避けるために、未指定の値をenumの名前でプレフィックスしてください。言語を跨いだ定数が必要ない場合は、`int32`が未知の値を保持し、生成されるコードが少なくなります。なお、proto enumは最初の値がゼロである必要があり、未知のenum値をラウンドトリップ(デシリアライズ、シリアライズ)できます。

Enum値にC/C++マクロ定数を使用しない

C++言語で既に定義されている単語、特に `math.h` などのヘッダーで使用されている単語をenum値として使用すると、それらのヘッダーの `#include` ステートメントが `.proto.h` の前に現れると、コンパイルエラーが発生する可能性があります。また、「`NULL`」、「`NAN`」、「`DOMAIN`」などのマクロ定数をenum値として使用することは避けてください。

Well-Known Typesと共通の型を使用する

以下の共通の共有型を使用することを強く推奨します。たとえば、完全に適切な共通型がすでに存在する場合に、コードで `int32 timestamp_seconds_since_epoch` や `int64 timeout_millis` を使用しないでください。

  • `duration` は、符号付きの固定長の時間間隔です(例:42秒)。
  • `timestamp` は、タイムゾーンやカレンダーに依存しない時点です(例:2017-01-15T01:30:15.01Z)。
  • `interval` は、タイムゾーンやカレンダーに依存しない時間間隔です(例:2017-01-15T01:30:15.01Z - 2017-01-16T02:30:15.01Z)。
  • `date` は、完全なカレンダー日付です(例:2005-09-19)。
  • `month` は、年の月です(例:4月)。
  • `dayofweek` は、曜日です(例:月曜日)。
  • `timeofday` は、時刻です(例:10:42:23)。
  • `field_mask` は、シンボリックフィールドパスのセットです(例:f.b.d)。
  • `postal_address` は、郵便住所です(例:1600 Amphitheatre Parkway Mountain View, CA 94043 USA)。
  • `money` は、通貨タイプを含む金額です(例:42 USD)。
  • `latlng` は、緯度/経度ペアです(例:緯度37.386051、経度-122.083855)。
  • `color` は、RGBAカラースペースの色です。

メッセージ型を別のファイルで定義する

protoスキーマを定義する際には、1つのファイルにつき1つのメッセージ、enum、拡張、サービス、または循環依存関係のグループを持つべきです。これにより、リファクタリングが容易になります。分離されたファイルを移動する方が、他のメッセージを含むファイルからメッセージを抽出するよりもはるかに簡単です。この慣行に従うことで、protoスキーマファイルを小さく保つことができ、保守性が向上します。

プロジェクト外で広く使用される場合は、依存関係のない独自のファイルにそれらを配置することを検討してください。そうすれば、誰でも他のprotoファイルに推移的な依存関係を導入することなく、それらの型を簡単に使用できます。

このトピックの詳細については、1-1-1ルールを参照してください。

フィールドのデフォルト値を変更しない

protoフィールドのデフォルト値を変更することはほとんどありません。これにより、クライアントとサーバー間でバージョンがずれます。ビルドがprotoの変更にまたがっている場合、設定されていない値を読み取るクライアントは、同じ設定されていない値を読み取るサーバーとは異なる結果を見ることになります。Proto3ではデフォルト値を設定する機能が削除されました。

RepeatedからScalarに変更しない

クラッシュは発生しませんが、データが失われます。JSONの場合、繰り返し性の不一致により、メッセージ全体が失われます。数値のproto3フィールドとproto2の`packed`フィールドの場合、repeatedからscalarに変更すると、そのフィールドのすべてのデータが失われます。非数値のproto3フィールドと注釈なしのproto2フィールドの場合、repeatedからscalarに変更すると、最後にデシリアライズされた値が「勝利」します。

proto2および`[packed=false]`を持つproto3では、scalarからrepeatedへの変更は問題ありません。バイナリシリアル化の場合、scalar値は1要素のリストになるためです。

生成されたコードのスタイルガイドに従うする

Protoで生成されたコードは、通常のコードで参照されます。`.proto`ファイルのオプションがスタイルガイドに違反するコード生成をもたらさないように注意してください。例えば

Text形式メッセージを交換に使用しない

テキスト形式やJSONのようなテキストベースのシリアル化形式は、フィールドとenum値を文字列として表現します。その結果、これらの形式でプロトコルバッファを古いコードでデシリアル化すると、フィールドやenum値が名前変更されたり、新しいフィールドやenum値、または拡張が追加されたりすると失敗します。データ交換には可能な限りバイナリシリアル化を使用し、テキスト形式は人間による編集とデバッグのみに使用してください。

APIでJSONに変換されたプロトを使用する場合や、データを保存するために使用する場合、フィールドやenumの名前を安全に変更できない場合があります。

ビルド間でのシリアル化の安定性に決して依存しない

プロトのシリアル化の安定性は、バイナリ間または同じバイナリのビルド間で保証されません。例えば、キャッシュキーを構築する際にこれに依存しないでください。

他のコードと同じJavaパッケージにJava Protosを生成しない

Java protoソースは、手書きのJavaソースとは別のパッケージに生成してください。`package`、`java_package`、および`java_alt_api_package`オプションは、生成されたJavaソースが出力される場所を制御します。手書きのJavaソースコードが同じパッケージに存在しないことを確認してください。一般的な慣行は、プロトをプロジェクト内の`proto`サブパッケージに生成し、そのサブパッケージにはプロトのみが含まれるようにすることです(つまり、手書きのソースコードは含まれません)。

フィールド名に言語キーワードを使用しない

メッセージ、フィールド、enum、またはenum値の名前が、そのフィールドを読み書きする言語のキーワードである場合、protobufはフィールド名を変更したり、通常のフィールドとは異なるアクセス方法を提供したりする可能性があります。例えば、Pythonに関するこの警告を参照してください。

ファイルパスにキーワードを使用することも避けるべきです。これも問題を引き起こす可能性があります。

RPC APIとストレージに異なるメッセージを使用する

APIと長期保存に同じメッセージを再利用することは、定型文やメッセージ間の変換のオーバーヘッドを削減し、便利に見えるかもしれません。

しかし、長期保存とライブRPCサービスのニーズは、後に異なる傾向があります。たとえ最初は大部分が重複していても、別々の型を使用することで、外部クライアントに影響を与えることなくストレージ形式を変更する自由が得られます。モジュールがクライアントプロト、ストレージプロト、または変換のいずれかを扱うようにコードをレイヤー化してください。

変換レイヤーの維持にはコストがかかりますが、クライアントを持ち、最初のストレージ変更を行う必要があるとすぐにその価値がわかります。

現在は2つの状態しか持たないが、将来的に増える可能性があるものにBooleanを使用しない

フィールドにブーリアンを使用する場合は、そのフィールドが本当に2つの可能な状態のみを記述していることを確認してください(現在と近い将来だけでなく、常に)。enumを使用する将来の柔軟性は、たとえ最初に導入されたときに2つの値しかない場合でも、多くの場合それだけの価値があります。

message Photo {
  // Bad: True if it's a GIF.
  optional bool gif;

  // Good: File format of the referenced photo (for example, GIF, WebP, PNG).
  optional PhotoType type;
}

java_outer_classnameを使用する

すべてのprotoスキーマ定義ファイルは、`java_outer_classname`オプションを`.proto`ファイル名をTitleCaseに変換し、'.'を削除したものに設定する必要があります。たとえば、`student_record_request.proto`ファイルは、次のように設定する必要があります。

option java_outer_classname = "StudentRecordRequestProto";