プロトのベストプラクティス 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な意識も持っていきたいと思いました。

プロトのベストプラクティス 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;
}

「ものを買うときに快感と不快感の影響はものすごく強いらしい」という本を読んだ話

「欲しい! 」はこうしてつくられる 脳科学者とマーケターが教える「買い物」の心理

「欲しい!」はこうして作られるという本を読みました。

この本の著者は脳科学者のマット・ジョンソンさんと、科学的な視点でマーケティングをやってるプリンス・ギューマンさんのお二人ですね。

この本は科学的な知見から消費者の消費行動を分析しておりまして、今回は本書の快感と不快感が購買行動にどう影響するかについての内容についてみていきます。

快 - 不快 = 購入

本の中では、ものをため込んじゃうタイプの人と、逆にものを捨てたがるタイプの人が比較されてました。この違いは快感と不快感の差からくるもので、人間は快を求めて不快を避ける生き物だからこそ、こういう行動の違いが出るみたいな感じっす。

で、スタンフォード大学の実験によると、商品を見た時は快楽を司る脳の部位が反応して、値段を見た時は不快を感じる部位が反応するんだとか。つまり商品の魅力 > 値段のときに買っちゃうってわけですね。

ものを手放したがる人はものを持つより捨てる方が快感があるし、逆にものを手放せない人は持ってる方が捨てるより快感があるって感じみたいっす。私のように持ち物をどんどん処分していくミニマリスト志向の人もいれば、何でも取っておきたがるタイプの人もいるわけです。

快を追い求める

あっという間に次の快へ・・・

快感ってのはすぐ慣れちゃうもんで、同じものを繰り返し体験するとどんどん快感が薄れていくんです。 進化の過程でこれが合理的だったからこそ、私たち人間は常に快を求め続ける生き物になったんだとか。ドーパミン駆動型の生存戦略ともいうべきです。 人は物事に満足するようにできていないので、常に欲求を求めているというわけ。 例えば子供に自転車を買い与えたら満足して終わり!...ではなく時間も経たないうちに次のおもちゃを探し回っているなんてことはよくあることです。 これを心理学用語で「ヘドニック・トレッドミル(快楽の踏み車)」というのだそう。

もっと身近なところで言うと、新型のIphoneを毎年買い替える人とかも例に漏れないですねー。その人が欲しい機能ややりたいことは今のIphoneでも十分にできちゃうんだけど、「もっとサクサクに!」「もっと高画質で写真を撮りたい」みたいな感じで見事にヘドニック・トレッドミルに載せられちゃってるわけですね。

毎日アイスを食べれるのは幸せだ!←間違い

人間は未来に得られる快を予測するのがヘッタクソなんです。例えば心理学の実験で30日間毎日タダでアイスクリームをもらえるという実験を行いました。実験に参加した人は当初「こんなハッピーなことがあるだろうか!」と浮かれ気分でいたんですが、実験が進むにつれてそのテンションは上がるどころか急降下。なんと10日あたりから離脱者が出はじめ、最終日を終えた人は憂鬱な気分になったそう。なんでこうなったかというと、先ほどでも触れましたが快に慣れてしまってそれが当たり前になったからです。それまでアイスを食べるのがご褒美だったのに日常的に食べれるようになって特別感がなくなり、実験だから毎日食べないといけなくなったと感じ始めたのです。 こんな感じで最初はハッピーが続くと思っていたが、終わってみれば不幸になってたという具合に人は将来得られる快感の予測的中率がめっちゃ低いことがわかります。 快は不快と表裏一体なんです・・・

不快を避ける

人間は不快感を避けようとする生き物でもあります。例えば買い物するときにお金を払うのがめちゃくちゃ嫌だったりするわけです。特に現金で支払うのがイヤで、金額が大きいほど躊躇しちゃうみたいな感じっす。

逆に言えば、現金以外の方法で支払わせれば、企業側としてはもっと簡単にお金を払ってもらえるってことっすかね。カジノなんかだと最初に現金をチップに変えるのも、そのへんの心理を突いてるって感じかもしれませんね。ネットショッピングでポチっとするだけで決済できちゃうのも、思わず衝動買いしちゃった経験のある人は多いんじゃないですかね。

この「不快感を避ける心理」のことを心理学では「損失回避」って呼ばれています。で、この不快感を避けたい気持ちを逆手に取った商品もあるみたいです。「コンプレックス・ビジネス」ってやつっすね。

例えば脱毛サロンなんかだと、ムダ毛が気になるっていうコンプレックスを解消するために利用する人が多いみたいな感じっす。整形とか育毛なんかもそういう側面があるのかもしれません。まぁ、これ自体は別に悪いことじゃないんですけど、中には効果のあやしい治療法だったり、ボッタクリ価格で売りつけようとする業者もいるみたいで、そういうのはニュースでもたまに取り上げられてますよね。

まとめ

人間の快楽と不快のメカニズムを理解することで、消費行動のパターンもある程度説明できるみたいです。企業側としては、快感を最大化し、不快感を最小化するような戦略を取ることが大事なわけですな。

個人的には、こういった人間の心理のクセを知っておくことで、自分の消費行動をコントロールするのにも役立ちそうな気がしますね。欲しいものをつい買っちゃったりするのを防げるかもしれませんし。

まーいずれにしても、この【「欲しい!」はこうして作られる】って本は、消費行動の裏側にある心理的カニズムを科学的に解き明かしてるみたいで、めちゃくちゃ面白かったっす。こういうのに興味ある人にはおすすめできるかと。内容はよく知られている行動経済学に関する知見が多かったので、元々好きな人にとってみれば復習になる本だと思います。 読み終わったあとは、自分の消費行動を冷静に見つめ直すきっかけにもなりそうです。