Slide 1

Slide 1 text

Datastore/Go のデータ設計と struct の振る舞いについて

Slide 2

Slide 2 text

自己紹介 twitter : pospome blog :pospomeのプログラミング日記 職種 : サーバサイドエンジニア 興味  : クラス設計全般, DDD アイコン:羊じゃなくてポメラニアン その他 :「ポメ」って呼んでください。

Slide 3

Slide 3 text

今日の発表の結論を言うと Datastoreのデータ構造の設計やレビューでは Kindに持たせる値だけではなく、 値が持つ振る舞いと特性も一緒に考えた方がいい

Slide 4

Slide 4 text

ソーシャルゲームのユーザー情報を表現する User Kind を 例に説明します

Slide 5

Slide 5 text

ID TEL Email Profile Image Profile Movie HP ATK DEF 1 000 x x.png 100 100 100 2 111 y y.mp4 50 50 50 3 222 z z.png 200 200 200

Slide 6

Slide 6 text

RDBで考えるとこの構造で問題ないかもしれないが Datastore で適切とは限らない Datastore のデータ構造をマッピングした struct の 振る舞いを通して考えてみる

Slide 7

Slide 7 text

例1 ProfileImage と ProfileMovie は どちらか一方しか登録できない という仕様を表現する

Slide 8

Slide 8 text

ID TEL Email Profile Image Profile Movie HP ATK DEF 1 000 x x.png 100 100 100 2 111 y y.mp4 50 50 50 3 222 z z.png 200 200 200

Slide 9

Slide 9 text

type User struct { //他のフィールドは省略 ProfileImage, ProfileMovie string } それぞれが単なるフィールドだと 「Image, Movie どちらか一方を登録する」 という仕様を表現するのは難しい

Slide 10

Slide 10 text

これを修正すると・・・

Slide 11

Slide 11 text

type User struct { Profile Profile } type Profile struct { Image, Movie string } func NewImageProfile(image string) Profile { return Profile{ Image: image, } } func NewMovieProfile(movie string) Profile { return Profile{ Movie: movie, } } 「Image, Movie どちらか一方を登録する」 というルールを Profile 自体に持たせることで 仕様を表現することができる

Slide 12

Slide 12 text

ID TEL Email Profile. Image Profile. Movie HP ATK DEF 1 000 x x.png 100 100 100 2 111 y y.mp4 50 50 50 3 222 z z.png 200 200 200

Slide 13

Slide 13 text

例2 Email, TEL は OAuth Scope = Contact でしか取得できない という仕様を表現する

Slide 14

Slide 14 text

ID TEL Email Profile. Image Profile. Movie HP ATK DEF 1 000 x x.png 100 100 100 2 111 y y.mp4 50 50 50 3 222 z z.png 200 200 200

Slide 15

Slide 15 text

type User struct { TEL, Email string } func Get(scope string) User { var u User = GetUserFromDB() if scope != "Contact" { u.TEL = "" u.Email = "" } return u } ロジック上で Contact scope = Email, TEL を表現している

Slide 16

Slide 16 text

これを修正すると・・・

Slide 17

Slide 17 text

type User struct { Contact Contact } type Contact struct { TEL, Email string } func Get(scope string) User { var u User = GetUserFromDB() if scope != "Contact" { u.Contact = nil } return u } Scope が扱う Contact という概念は 具体的に TEL, Email を含む 抽象度の違う値同士を扱おうとすると、 ロジックが複雑になる可能性がある TEL, Email という具体的な概念を Scope の Contact という抽象度に合わせる Scope と struct が一致しているので、 直感的に理解しやすい Contact の持つ値が変化しても、 u.Contact = nil に修正は発生しない これはロジックが Contact という抽象度の概念 を扱っているからであって、 t.TEL = “” のように抽象度がマッチしない場合 に比べると変更に強くなる

Slide 18

Slide 18 text

ID Contact .TEL Contact .Email Profile. Image Profile. Movie HP ATK DEF 1 000 x x.png 100 100 100 2 111 y y.mp4 50 50 50 3 222 z z.png 200 200 200

Slide 19

Slide 19 text

例3 ATK, DEF は HP の値によって増減する という仕様を表現する

Slide 20

Slide 20 text

ID Contact .TEL Contact .Email Profile. Image Profile. Movie HP ATK DEF 1 000 x x.png 100 100 100 2 111 y y.mp4 50 50 50 3 222 z z.png 200 200 200

Slide 21

Slide 21 text

type User struct { HP, ATK, DEF int } func (u *User) GetAtk() int { //HPに依存する return u.ATK * u.HP } func (u *User) GetDef() int { if u.HP < 100 { //ピンチになると強くなる return u.DEF * 2 } return u.DEF } それぞれの値を算出するロジックは HP, ATK, DEF に依存しているが、 他の値には依存していない

Slide 22

Slide 22 text

これを修正すると・・・

Slide 23

Slide 23 text

type User struct { Battle Battle } type Battle struct { HP, ATK, DEF int } func (b *Battle) GetAtk() int { //HPに依存する return b.ATK * b.HP } func (b *Battle) GetDef() int { if b.HP < 100 { //ピンチになると強くなる return b.DEF * 2 } return b.DEF } HP, ATK, DEF を Battle として定義 「対戦」に関するロジックは Battle に集中させる User は「対戦」以外のロジックに集中できる 対戦のロジックはゲームのコアな要素なので、 複雑な仕様になりやすい User から分離しておくと Battle に interface を持たせて 特定のロジックを抽象化させたり、 固定値を設定した Battle に差し替えるなど、 User を汚さずに「対戦」を表現できる

Slide 24

Slide 24 text

ID Contact .TEL Contact .Email Profile. Image Profile. Movie Battle. HP Battle.A TK Battle. DEF 1 000 x x.png 100 100 100 2 111 y y.mp4 50 50 50 3 222 z z.png 200 200 200

Slide 25

Slide 25 text

ということで、最終的に・・・

Slide 26

Slide 26 text

type User struct { ID int64 Contact Contact Profile Profile Battle Battle } type Contact struct { TEL, Email string } type Profile struct { Image, Movie string } type Battle struct { HP, ATK, DEF int } 振る舞いを考慮すると User struct は以下になる

Slide 27

Slide 27 text

ID Contact .TEL Contact .Email Profile. Image Profile. Movie Battle. HP Battle.A TK Battle. DEF 1 000 x x.png 100 100 100 2 111 y y.mp4 50 50 50 3 222 z z.png 200 200 200 User struct を保存する User Kind は以下になるので、 最初のデータ構造とは違うものになった

Slide 28

Slide 28 text

まとめ

Slide 29

Slide 29 text

Datastoreのデータ構造の設計やレビューでは Kindに持たせる値だけではなく、 値が持つ振る舞いと特性も一緒に考えた方がいい

Slide 30

Slide 30 text

RDBだと struct の構造を そのまま保存するものではないので 必要に応じてORMや手動マッピングロジックで 永続化データとモデルをマッピングする struct の振る舞いを考慮してテーブル構造を 考える必要性は低い 永続化データとしての正しさを考えればいい RDBはSQLによる柔軟なクエリが可能なので、 無理やり struct を保存する工夫をするよりも 永続化データとしての正しさを重視した方がいい

Slide 31

Slide 31 text

Datastore は struct をそのまま保存できるので、 データ設計の段階で struct を考慮して設計すると 手戻りが少なく、 自分でインピーダンスミスマッチを解消する必要もない 極端に言うと Datastore設計 = モデル設計 ただし、 Datastore に保存できないデータ構造もあるので注意

Slide 32

Slide 32 text

Kind のプロパティ名に Prefix がある場合は その値は関連性の高い値である可能性が高い 関連性の高い値は それ独自の振る舞いや特性を持つ可能性が高い そういった関連性の高いデータに対して 仕様を表現するロジックを紐付けることによって、 責務が明確になる

Slide 33

Slide 33 text

今回の例は説明用ということもあって、 結構無理矢理なケースかと思います 今回の例であれば Datastore のプロパティを フラットに並べても問題ないかもしれません ここはモデルの設計方針によって変わります 重要なのは 「struct の振る舞いも考慮する」 という選択肢を持つことです

Slide 34

Slide 34 text

Datastore の設計をする際には struct の振る舞いも考慮してみてはいかがでしょうか?

Slide 35

Slide 35 text

おわり