Proto のベスト プラクティス
クライアントとサーバーは、同時に更新しようとしても、決して同時に更新されるわけではありません。どちらか一方がロールバックされる可能性があります。互換性のない変更を行っても、クライアントとサーバーが同期しているから大丈夫だと仮定しないでください。
避けるべきこと: タグ番号の再利用
タグ番号を再利用しないでください。デシリアライゼーションを台無しにします。誰もそのフィールドを使用していないと思っても、タグ番号を再利用しないでください。変更が一度でもライブであった場合、ログのどこかにプロトのシリアライズされたバージョンが存在する可能性があります。あるいは、他のサーバーに古いコードが存在し、それが壊れる可能性があります。
すべきこと: 削除されたフィールドのタグ番号を予約する
もはや使用されていないフィールドを削除するときは、将来誰も誤って再利用しないように、そのタグ番号を予約してください。reserved 2, 3; だけで十分です。型は必要ありません(依存関係を削減できます!)。現在削除されたフィールド名の再利用を避けるために、名前を予約することもできます。reserved "foo", "bar";。
すべきこと: 削除されたEnum値の番号を予約する
もはや使用されていないenum値を削除するときは、将来誰も誤って再利用しないように、その番号を予約してください。reserved 2, 3; だけで十分です。現在削除された値名の再利用を避けるために、名前を予約することもできます。reserved "FOO", "BAR";。
すべきこと: 新しいEnumエイリアスを最後に置く
新しいenumエイリアスを追加するときは、サービスがそれを受け取る時間を与えるために、新しい名前を最後に配置してください。
元の名前を安全に削除するには(交換に使用されている場合、すべきではないのですが)、以下の手順を実行する必要があります。
古い名前の下に新しい名前を追加し、古い名前を非推奨にする(シリアライザーは古い名前を使用し続ける)
すべてのパーサーがスキーマを展開した後、2つの名前の順序を入れ替える(シリアライザーは新しい名前を使い始め、パーサーは両方を受け入れる)
すべてのシリアライザーがそのバージョンのスキーマを持った後、非推奨の名前を削除できます。
注: クライアントは理論的には古い名前を交換に使用すべきではありませんが、特に広く使用されているenum名の場合、上記の手順に従うことは依然として丁寧です。
避けるべきこと: フィールドの型の変更
フィールドの型を変更することはほとんどありません。タグ番号を再利用するのと同じように、デシリアライゼーションを台無しにします。protobuf ドキュメントには、問題ない少数のケース(例えば、int32、uint32、int64、bool の間での変更)が概説されています。ただし、フィールドのメッセージ型を変更すると、新しいメッセージが古いメッセージのスーパーセットでない限り、壊れてしまいます。
避けるべきこと: 必須フィールドの追加
必須フィールドを絶対に追加しないでください。代わりに、API契約を文書化するために// requiredを追加してください。必須フィールドは非常に有害であると考えられており、proto3では完全に削除されました。すべてのフィールドをオプションまたは繰り返しにしてください。メッセージ型がどれくらいの期間存続するか、そして4年後に論理的に必須ではなくなっても、protoがまだ必須であると言っている場合に、誰かが必須フィールドを空の文字列やゼロで埋めることを余儀なくされるかどうかはわかりません。
proto3にはrequiredフィールドがないため、このアドバイスは適用されません。
避けるべきこと: 多数のフィールドを持つメッセージの作成
「たくさんの」(数百を考えてください)フィールドを持つメッセージを作成しないでください。C++では、すべてのフィールドは、それが設定されているかどうかに関わらず、インメモリオブジェクトサイズに約65ビットを追加します(ポインタ用に8バイト、そしてフィールドがオプションとして宣言されている場合、フィールドが設定されているかどうかを追跡するビットフィールドにもう1ビット)。プロトが大きくなりすぎると、生成されたコードがコンパイルできないことさえあります(例えば、Javaではメソッドのサイズに厳密な制限があります)。
すべきこと: Enumに未指定の値を含める
Enumには、宣言の最初の値としてデフォルトの FOO_UNSPECIFIED 値を含める必要があります。Enumに新しい値が追加された場合、古いクライアントはフィールドが未設定であると見なし、ゲッターはデフォルト値、またはデフォルトが存在しない場合は最初に宣言された値を返します。proto enums と一貫した動作のために、最初に宣言されたenum値はデフォルトの FOO_UNSPECIFIED 値であるべきで、タグ 0 を使用すべきです。このデフォルトを意味のある値として宣言したくなるかもしれませんが、一般的には、時間の経過とともに新しいenum値が追加されるにつれてプロトコルの進化を助けるために、そうすべきではありません。コンテナメッセージの下で宣言されたすべてのenum値は同じC++名前空間にあるため、コンパイルエラーを避けるために未指定の値にenumの名前を接頭辞として付けてください。言語間の定数が決して必要ない場合、int32 は未知の値を保持し、生成されるコードが少なくなります。なお、proto enums は最初の値がゼロである必要があり、未知のenum値をラウンドトリップ(デシリアライズ、シリアライズ)できます。
避けるべきこと: Enum値にC/C++マクロ定数を使用する
C++言語、特にmath.hのようなヘッダーファイルですでに定義されている単語をenum値として使用すると、それらのヘッダーの#includeステートメントが.proto.hのそれより前に現れる場合、コンパイルエラーを引き起こす可能性があります。「NULL」「NAN」「DOMAIN」のようなマクロ定数をenum値として使用することは避けてください。
すべきこと: 既知の型と共通の型を使用する
以下の共通の共有型を使用することを強くお勧めします。例えば、完全に適切な共通型がすでに存在する場合、コードで 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カラースペースの色です。
注: 「既知の型」(Duration や Timestamp など)は Protocol Buffers コンパイラに含まれていますが、「共通の型」(Date や Money など)は含まれていません。共通の型を使用するには、googleapis リポジトリへの依存関係を追加する必要がある場合があります。
すべきこと: メッセージ型を別々のファイルで定義する
プロトスキーマを定義する際には、ファイルごとに単一のメッセージ、Enum、拡張機能、サービス、または循環依存関係のグループを持つべきです。これにより、リファクタリングが容易になります。分離されているファイルを移動する方が、他のメッセージを含むファイルからメッセージを抽出するよりもはるかに簡単です。このプラクティスに従うことで、プロトスキーマファイルが小さく保たれ、保守性が向上します。
プロジェクト外で広く使用される場合は、依存関係なしで独自のファイルに配置することを検討してください。そうすれば、他のプロトファイルに推移的依存関係を導入することなく、誰でもそれらの型を簡単に使用できます。
このトピックの詳細については、1-1-1 ルールを参照してください。
避けるべきこと: フィールドのデフォルト値の変更
プロトフィールドのデフォルト値を変更することはほとんどありません。これにより、クライアントとサーバーの間でバージョンにずれが生じます。プロトの変更をまたいでビルドされた場合、未設定の値を読み取るクライアントは、同じ未設定の値を読み取るサーバーとは異なる結果を見ることになります。Proto3では、デフォルト値を設定する機能が削除されました。
避けるべきこと: 繰り返し型からスカラ型への変更
クラッシュは発生しませんが、データが失われます。JSONの場合、繰り返し表現の不一致によってメッセージ全体が失われます。数値型のproto3フィールドおよびproto2のpackedフィールドの場合、繰り返し型からスカラ型にすると、そのフィールド内のすべてのデータが失われます。非数値型のproto3フィールドおよび注釈なしのproto2フィールドの場合、繰り返し型からスカラ型にすると、最後にデシリアライズされた値が「勝利」します。
スカラ型から繰り返し型への変更は、proto2および[packed=false]のproto3では問題ありません。これは、バイナリシリアライゼーションの場合、スカラ値が1要素のリストになるためです。
すべきこと: 生成されたコードのスタイルガイドに従う
Protoで生成されたコードは、通常のコードで参照されます。.protoファイル内のオプションが、スタイルガイドに違反するコードの生成につながることがないようにしてください。例えば、
java_outer_classnameは https://google.github.io/styleguide/javaguide.html#s5.2.2-class-names に従うべきです。java_packageとjava_alt_packageは https://google.github.io/styleguide/javaguide.html#s5.2.1-package-names に従うべきです。packageは、java_packageが存在しない場合にJavaでも使用されますが、常にC++の名前空間に直接対応するため、https://google.github.io/styleguide/cppguide.html#Namespace_Names に従うべきです。これらのスタイルガイドが競合する場合は、Javaにはjava_packageを使用してください。ruby_packageはFoo.Bar.BazではなくFoo::Bar::Bazの形式であるべきです。
避けるべきこと: データ交換にテキスト形式のメッセージを使用する
テキスト形式やJSONのようなテキストベースのシリアライゼーション形式は、フィールドとenum値を文字列として表現します。そのため、フィールドやenum値の名前が変更された場合、または新しいフィールドやenum値、拡張機能が追加された場合、これらの形式でのプロトコルバッファのデシリアライゼーションは古いコードを使用して失敗します。データ交換には可能な限りバイナリシリアライゼーションを使用し、テキスト形式は人間による編集とデバッグのみに使用してください。
APIでJSONに変換されたプロトを使用したり、データを保存したりする場合、フィールドやenumの名前を安全に変更することが全くできない可能性があります。
決してすべきでないこと: ビルド間のシリアライゼーションの安定性に依存する
プロトのシリアライゼーションの安定性は、バイナリ間または同じバイナリのビルド間で保証されません。例えば、キャッシュキーを構築する際には、これに依存しないでください。
避けるべきこと: Java Protosを他のコードと同じJavaパッケージに生成する
Javaプロトソースは、手書きのJavaソースとは別のパッケージに生成してください。package、java_package、java_alt_api_packageのオプションは、生成されたJavaソースが出力される場所を制御します。手書きのJavaソースコードが同じパッケージ内に存在しないようにしてください。一般的な慣習は、プロトをプロジェクト内のprotoサブパッケージに生成し、そのサブパッケージにはそれらのプロトのみが含まれるようにすることです(つまり、手書きのソースコードは含まれません)。
すべきこと: .protoパッケージからJavaパッケージを派生させる(オーバーライドする場合)
java_package を設定すると、生成されたコードで完全修飾名の衝突が発生することがあります。これは、.proto のセマンティクスには存在しない衝突です。たとえば、以下の2つのファイルは、元のスキーマで完全修飾名が衝突していなくても、生成されたコードで衝突を引き起こす可能性があります。
package x;
option java_package = "com.example.proto";
message Abc {}
package y;
option java_package = "com.example.proto";
message Abc {}
これらの問題を避けるためには、異なる.protoパッケージが設定されている2つのファイルで同じjava_packageを設定してはなりません。
ベストプラクティスは、パッケージ名が.protoパッケージから派生するローカルな命名パターンを確立することです。たとえば、package yを持つベストプラクティスファイルは、一貫してoption java_package = "com.example.proto.y"を設定するかもしれません。
このガイドラインは、パッケージのオーバーライドが可能な他の言語固有のオプションにも適用されます。
フィールド名に言語のキーワードを使用しない
メッセージ、フィールド、enum、またはenum値の名前が、そのフィールドを読み書きする言語のキーワードである場合、protobufはフィールド名を変更したり、通常のフィールドとは異なるアクセス方法を持たせたりする可能性があります。たとえば、Pythonに関するこの警告を参照してください。
ファイルパスにキーワードを使用することも避けるべきです。これも問題を引き起こす可能性があります。
すべきこと: RPC APIとストレージに異なるメッセージを使用する
APIと長期ストレージに同じメッセージを再利用すると、メッセージ間の変換の定型的な記述やオーバーヘッドを削減できるため、便利に思えるかもしれません。
しかし、長期ストレージとライブRPCサービスのニーズは、後に分岐する傾向があります。たとえ最初は大幅に重複していても、個別の型を使用することで、外部クライアントに影響を与えることなくストレージ形式を変更する自由が得られます。モジュールがクライアントプロト、ストレージプロト、または変換のいずれかを扱うようにコードを階層化してください。
変換レイヤーの維持にはコストがかかりますが、クライアントができて最初のストレージ変更を行うと、すぐにそのコストに見合うだけのメリットが得られます。
避けるべきこと: 現在は2つの状態だが、将来増える可能性があるものにブール値を使用する
フィールドにブール値を使用する場合は、そのフィールドが実際に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を使用する
すべてのプロトスキーマ定義ファイルは、オプション java_outer_classname を、.proto ファイル名を「.」を削除してTitleCaseに変換した名前に設定する必要があります。例えば、ファイル student_record_request.proto は、次のように設定すべきです。
option java_outer_classname = "StudentRecordRequestProto";