Upload
pospome
View
4.239
Download
0
Embed Size (px)
Citation preview
Datastore/Go のデータ設計とstruct の振る舞いについて
自己紹介twitter : pospomeblog :pospomeのプログラミング日記職種 : サーバサイドエンジニア興味 : クラス設計全般, DDDアイコン:羊じゃなくてポメラニアンその他 :「ポメ」って呼んでください。
今日の発表の結論を言うとDatastoreのデータ構造の設計やレビューでは
Kindに持たせる値だけではなく、値が持つ振る舞いと特性も一緒に考えた方がいい
ソーシャルゲームのユーザー情報を表現する User Kind を例に説明します
ID TEL Email ProfileImage
ProfileMovie HP ATK DEF
1 000 x x.png 100 100 1002 111 y y.mp4 50 50 503 222 z z.png 200 200 200
RDBで考えるとこの構造で問題ないかもしれないがDatastore で適切とは限らない
Datastore のデータ構造をマッピングした struct の振る舞いを通して考えてみる
例1ProfileImage と ProfileMovie はどちらか一方しか登録できない
という仕様を表現する
ID TEL Email ProfileImage
ProfileMovie HP ATK DEF
1 000 x x.png 100 100 1002 111 y y.mp4 50 50 503 222 z z.png 200 200 200
type User struct {//他のフィールドは省略
ProfileImage, ProfileMovie string}
それぞれが単なるフィールドだと「Image, Movie どちらか一方を登録する」
という仕様を表現するのは難しい
これを修正すると・・・
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 自体に持たせることで
仕様を表現することができる
ID TEL Email Profile.Image
Profile.Movie HP ATK DEF
1 000 x x.png 100 100 1002 111 y y.mp4 50 50 503 222 z z.png 200 200 200
例2Email, TEL は
OAuth Scope = Contact でしか取得できないという仕様を表現する
ID TEL Email Profile.Image
Profile.Movie HP ATK DEF
1 000 x x.png 100 100 1002 111 y y.mp4 50 50 503 222 z z.png 200 200 200
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 を表現している
これを修正すると・・・
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 = “” のように抽象度がマッチしない場合に比べると変更に強くなる
ID Contact.TEL
Contact.Email
Profile.Image
Profile.Movie HP ATK DEF
1 000 x x.png 100 100 1002 111 y y.mp4 50 50 503 222 z z.png 200 200 200
例3ATK, DEF は HP の値によって増減する
という仕様を表現する
ID Contact.TEL
Contact.Email
Profile.Image
Profile.Movie HP ATK DEF
1 000 x x.png 100 100 1002 111 y y.mp4 50 50 503 222 z z.png 200 200 200
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 に依存しているが、
他の値には依存していない
これを修正すると・・・
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 を汚さずに「対戦」を表現できる
ID Contact.TEL
Contact.Email
Profile.Image
Profile.Movie
Battle.HP
Battle.ATK
Battle.DEF
1 000 x x.png 100 100 1002 111 y y.mp4 50 50 503 222 z z.png 200 200 200
ということで、最終的に・・・
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 は以下になる
ID Contact.TEL
Contact.Email
Profile.Image
Profile.Movie
Battle.HP
Battle.ATK
Battle.DEF
1 000 x x.png 100 100 1002 111 y y.mp4 50 50 503 222 z z.png 200 200 200
User struct を保存する User Kind は以下になるので、最初のデータ構造とは違うものになった
まとめ
Datastoreのデータ構造の設計やレビューではKindに持たせる値だけではなく、
値が持つ振る舞いと特性も一緒に考えた方がいい
RDBだと struct の構造をそのまま保存するものではないので
必要に応じてORMや手動マッピングロジックで永続化データとモデルをマッピングする
struct の振る舞いを考慮してテーブル構造を考える必要性は低い
永続化データとしての正しさを考えればいいRDBはSQLによる柔軟なクエリが可能なので、無理やり struct を保存する工夫をするよりも永続化データとしての正しさを重視した方がいい
Datastore は struct をそのまま保存できるので、データ設計の段階で struct を考慮して設計すると
手戻りが少なく、自分でインピーダンスミスマッチを解消する必要もない
極端に言うとDatastore設計 = モデル設計
ただし、Datastore に保存できないデータ構造もあるので注意
Kind のプロパティ名に Prefix がある場合はその値は関連性の高い値である可能性が高い
関連性の高い値はそれ独自の振る舞いや特性を持つ可能性が高い
そういった関連性の高いデータに対して仕様を表現するロジックを紐付けることによって、
責務が明確になる
今回の例は説明用ということもあって、結構無理矢理なケースかと思います
今回の例であればDatastore のプロパティを
フラットに並べても問題ないかもしれませんここはモデルの設計方針によって変わります
重要なのは「struct の振る舞いも考慮する」
という選択肢を持つことです
Datastore の設計をする際にはstruct の振る舞いも考慮してみてはいかがでしょうか?
おわり