Proto のベストプラクティス

Protocol Buffers の作成に関する、検証済みのベストプラクティスを共有します。

クライアントとサーバーは、同時に更新しようとしても、完全に同時に更新されることはありません。どちらか一方がロールバックされる可能性があります。互いに同期しているからといって、破壊的な変更を行っても大丈夫だと考えないでください。

タグ番号を再利用しない

タグ番号を再利用しないでください。デシリアライズがうまくいかなくなります。たとえ誰もそのフィールドを使用していないと思っていても、タグ番号を再利用しないでください。変更が一度でも公開された場合、どこかのログにProtoのシリアライズされたバージョンが残っている可能性があります。あるいは、他のサーバーに古いコードが残っていて、それが壊れる可能性があります。

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

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

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

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

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

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

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

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

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

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

注:理論的にはクライアントは交換に古い名前を使用すべきではありませんが、特に広く使用されている enum 名の場合、上記のステップに従うのが丁寧です。

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

フィールドの型はほとんど変更しないでください。タグ番号の再利用と同様に、デシリアライズを混乱させます。protobuf ドキュメントには、問題ないケースが少数記載されています(たとえば、int32uint32int64bool 間での変更)。ただし、フィールドのメッセージ型を変更すると、新しいメッセージが古いメッセージのスーパーセットでない限り、壊れます

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

必須フィールドは絶対に追加しないでください。代わりに、API契約を文書化するために// requiredを追加してください。必須フィールドは非常に多くの人によって有害であると見なされ、proto3からは完全に削除されました。すべてのフィールドをオプションまたは繰り返しにしてください。メッセージ型がどれくらいの期間使用されるか、そして論理的にはもはや必須ではないのにprotoが必須であると指定しているために、4年後に誰かが必須フィールドに空の文字列やゼロを強制的に入力しなければならなくなるかどうかはわかりません。

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

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

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

Enumに未指定値を含め

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

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

C++ 言語、特に math.h などのヘッダーですでに定義されている単語を使用すると、それらのヘッダーの #include 文が .proto.h の前に現れる場合、コンパイルエラーを引き起こす可能性があります。Enum 値として "NULL"、"NAN"、"DOMAIN" のようなマクロ定数を使用することは避けてください。

Well-Known Types と一般的な型を使用する

以下の一般的で共有された型の使用が強く推奨されます。例えば、完璧に適切な共通型がすでに存在する場合、コードでint32 timestamp_seconds_since_epochint64 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つのメッセージ、enum、拡張機能、サービス、または循環依存関係のグループを含めるべきです。これにより、リファクタリングが容易になります。ファイルが分離されていれば、他のメッセージを含むファイルからメッセージを抽出するよりもはるかに簡単です。このプラクティスに従うことで、proto スキーマファイルが小さく保たれ、保守性が向上します。

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

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

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

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

繰り返しからスカラーへ変更しない

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

スカラー型から繰り返し型への変更は、proto2および[packed=false]を持つproto3では問題ありません。これは、バイナリシリアライゼーションの場合、スカラー値が1要素のリストになるためです。

生成コードのスタイルガイドに従

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

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

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

APIで、またはデータの保存に、JSONに変換されたprotoを使用する場合、フィールドやenumの名前を安全に変更することが全くできない可能性があります。

ビルド間のシリアライズの安定性に決して依存しない

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

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

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

フィールド名に言語キーワードの使用を避ける

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

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

java_outer_classname を使用する

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

option java_outer_classname = "StudentRecordRequestProto";

付録

API のベストプラクティス

このドキュメントは、破損を引き起こす可能性が極めて高い変更のみをリストしています。優雅に成長するproto APIの作成方法に関するより高レベルのガイダンスについては、APIのベストプラクティスを参照してください。