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

前回の続きです

よく知られた型と一般的なデータ型を使用する

syntax = "proto3";

import "google/protobuf/duration.proto";
import "google/protobuf/timestamp.proto";
import "google/type/date.proto";
import "google/type/dayofweek.proto";
import "google/type/latlng.proto";
import "google/type/money.proto";
import "google/type/postal_address.proto";
import "google/type/color.proto";

message Event {
 string name = 1;
 google.protobuf.Timestamp start_time = 2;
 google.protobuf.Duration duration = 3;
 google.type.Date event_date = 4;
 google.type.DayOfWeek event_day = 5;
 google.type.LatLng location = 6;
 google.type.Money ticket_price = 7;
 google.type.PostalAddress venue_address = 8;
 google.type.Color theme_color = 9;
}

上記のコードではTimestampやDate型などよく使われるデータ型に対応した共通の型が存在しております。

これらの共通型を使用することで、コードの意図がより明確になり、他のプロトコルバッファユーザーとの相互運用性が向上します。独自のカスタム型を定義するのではなく、可能な限りこれらの共通タイプを使用することをが勧められています。

多用されるメッセージタイプは別ファイルで定義する

メッセージを多く書いていると、共通化できるものも出てきます。そういう時は別のファイルにまとめることで再利用性と保守性が向上します。

// common_types.proto

syntax = "proto3";

package common;

message Timestamp {
 int64 seconds = 1;
 int32 nanos = 2;
}

message Date {
 int32 year = 1;
 int32 month = 2;
 int32 day = 3;
}

enum DayOfWeek {
 MONDAY = 0;
 TUESDAY = 1;
 WEDNESDAY = 2;
 THURSDAY = 3;
 FRIDAY = 4;
 SATURDAY = 5;
SUNDAY = 6;
}

こんな形でまとめて、別ファイルでは

// event.proto

syntax = "proto3";

import "common_types.proto";

message Event {
  string name = 1;
  common.Timestamp start_time = 2;
  int64 duration_seconds = 3;
  common.Date event_date = 4;
  common.DayOfWeek event_day = 5;
  // 他のイベント関連のフィールド
}

このように呼び出します。

repeatedからScalarにしない

繰り返しのrepeatedから単一のスカラー(Scalar)に変換してはいけないということです。

それはJson形式にデシリアライズ時にrepeatedとして期待されていたデータがスカラーとして解釈されてしまうのです。

これによってメッセージ全体のデシリアライズが失敗し、データが失われる可能性があります。

一方、スカラーからrepeatedへの変更は、以下の場合には安全です。

proto2 とproto3 の packed=false:

  • packed=false を指定すると、バイナリ形式では、スカラー値が1要素のリストとして扱われます。
  • これにより、スカラーからrepeatedへの変更が可能になり、データの損失を防ぐことができます。
// 変更前: スカラー値
message Person {
  string name = 1;
  int32 age = 2;
}

// 変更後: repeated値 (packed=false)
message Person {
  string name = 1;
  repeated int32 age = 2 [packed=false];
}

ただ、基本的には型の直接の変更というよりは、deprecated = trueを指定してから新しい別のフィールドを用意するほうが安全だったりするので極力避けたいです。

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

これは各言語で指定されているキーワード(funcなどの予約語)を使ってはいけません。なぜなら生成したコードでfuncの様ながフィールド名が使われるとコンパイルエラーが起きる可能性があるからです。

そんなに多くはないケースだと思いますが、覚えておいて損はないでしょう。

ビルド間のシリアライゼーションの安定性に依存しない

キャッシュ・キーのビルドの際にシリアライゼーションに依存したコードを書かないようにするのが良いということです。(深く理解してない)

まとめ

基本的には「フィールドの型の更新は避けようねー」とか、「削除する場合はreserveしようねー」とかデータの不整合を防ぐためのTipsが書かれていました。 あとは、メッセージを共通化して別ファイルで管理するといったDRYな意識も持っていきたいと思いました。