Upgrade to Pro — share decks privately, control downloads, hide ads and more …

[JA] Golang Package Composition for Web Application: The Case of Mercari Kauru

mercari
September 30, 2017
30k

[JA] Golang Package Composition for Web Application: The Case of Mercari Kauru

mercari

September 30, 2017
Tweet

Transcript

  1. Mercari Tech Conf 2017 Software Engineer Osamu TONOMORI Web アプリケーションにおける

    Go 言語の パッケージ構成 〜メルカリ カウル編〜
  2. 自己紹介 • 主森 理 (Osamu TONOMORI) Screen name: @osamingo •

    株式会社ソウゾウ メルカリ カウルチーム所属 • Software Engineer Server-side, Gopher ʕ◔ϖ◔ʔ
  3. パッケージ構成は、おおまかに分けて 3 種類(個人的主観) • One Package Repository 自体を単一のパッケージとみなす。 • Flat

    Packages 各パッケージの責務を明確にし、分割を行う。 • Multiple Packages MVC や、 DDD など、各デザインパターンに準じる。
  4. One Package • Simple is Best (๑•̀ㅂ•́)و✧ • Coverage も取りやすい。

    • Library など、簡素な構成で 済むものに向いている。 . ├── ctx.go ├── debug.go ├── error.go ├── handler.go ├── handler_func.go ├── jsonrpc.go ├── method.go └── parse.go
  5. Flat Packages と Multiple Packages の違い 機能A 機能B 機能C 機能A,

    機能B, 機能C 機能A, 機能B, 機能C 機能A, 機能B, 機能C Flat Packages 機能ごとに分割 Multiple Packages デザインパターンに沿った分類
  6. Flat Packages • Go 言語の標準パッケージの 構成に親しい考え方をする。 • 階層を掘ったとしても、 2 階層ぐらいで落ち着くはず。

    • パッケージの責務を明確にし、 お互いに疎結合を心がける。 • Micro Services や、Middleware などに向いている。 . ├── ctx │ └── ctx.go ├── debug │ └── debug.go ├── error │ └── error.go ├── handler │ ├── handler.go │ └── handler_func.go ├── jsonrpc.go ├── parse │ └── parse.go └── method └── method.go
  7. Multiple Packages • デザインパターンに寄り添った パッケージ設計。 • Monolithic な Web サービスに向

    いている。 . ├── controller │ ├── debug │ │ └── debug.go │ └── handler.go ├── jsonrpc.go ├── model │ ├── ctx │ │ └── ctx.go │ ├── error │ │ └── error.go │ ├── marshal │ │ └── unmarshal.go │ └── method │ └── method.go └── view └── index.html.tpl
  8. メルカリ カウルは、 Multiple Packages です。 • 1 Repository で管理されている、 Monolithic

    な Web サービス。  GAE を利用しているので、 Project Based な GOPATH との相性が良い。 • DDD を簡素化した様なデザインパターンを採用しています。  先行の メルカリ アッテの構成を継承し、人材流動性も考慮したため。
  9. Google App Engine (GAE) とは? • Google Cloud Platform が提供する、Platform

    as a Service (PaaS) • 競合は、 Heroku, Engine Yard など
  10. Repository structure • GAE プロジェクトと相性が、 良い GB を利用している。 • GB

    は、 Project Based な ビルドツールです。 • その為、直下に src ディレクトリがある。 . ├── Makefile ├── appengine # GAE の定義ファイル │ ├── api │ ├── cron.yaml │ ├── default │ ├── dispatch.yaml │ ├── management │ └── queue.yaml ├── cmd # お手製の便利コマンド群 ├── docs # uml, sql などの設計ドキュメント ├── src │ └── kauru # アプリケーション本体 │ ├── application │ ├── domain │ ├── infrastructure │ ├── interfaces │ └── library ├── test # テスト用の環境変数定義 │ └── .envrc └── vendor # 依存ライブラリ管理 └── src
  11. Lightweight DDD • 4 つの Layer で構成している。  Application  Domain  Infrastructure

     Interface(s) • Library ディレクトリの存在  単体で成立するPackage 群 . └── kauru ├── application ├── domain ├── infrastructure ├── interfaces └── library
  12. Application layer • Domain layer の処理をまとめ、 Interface layer に提供する。 •

    Flat Packages の思想で、 パッケージ分割している。 • internal パッケージに活用し、 共通ロジックを隠蔽している。 . └── kauru ├── application │ ├── audio │ │ └── audio.go │ ├── book │ │ └── book.go │ ├── exhibit │ ├── install │ ├── internal │ ├── product │ ├── ranking │ ├── search │ ├── stock │ ├── user │ └── visual ├── domain ├── infrastructure ├── interfaces └── library
  13. Domain layer • Domain layer は、他 layer には 依存しない。 •

    Entity や、 Enum, Interface を 提供している。 . └── kauru ├── application ├── domain │ ├── bookmaster │ │ └── repository.go │ ├── bookmaster.go │ ├── configuration.go │ ├── domain.go │ ├── error.go │ ├── mercari.go │ ├── pager.go │ ├── product.go │ ├── time.go │ ├── user │ │ └── repository.go │ └── user.go ├── infrastructure ├── interfaces └── library
  14. Infrastructure layer • GAE のライブラリを Wrap • 基礎的な処理を持っている  Logging  Validation

    Rule  Word Filtering  Value Get/Set in Context  Persistence . └── kauru ├── application ├── domain ├── infrastructure │ ├── backoff │ │ └── backoff.go │ ├── configuration │ │ └── configuration.go │ ├── crypto │ ├── ctx │ ├── identifier │ ├── log │ ├── persistence │ ├── queue │ ├── storage │ ├── tx │ ├── validation │ └── wordfilter ├── interfaces └── library
  15. Interface layer • JSON-RPC の Handler 群  Search.FindProduct など •

    Original Error 定義  -32044: Not Found など • Interceptor 群  Context に色々詰める  Action Logger など • その他  Startup 処理、状態取得 . └── kauru ├── application ├── domain ├── infrastructure ├── interfaces │ ├── api │ │ └── search │ │ └── find_product.go │ ├── error.go │ ├── interceptor │ │ └── recovery.go │ ├── interfaces.go │ ├── management │ │ └── cron │ │ └── backup_datastore.go │ ├── stats.go │ └── warmup.go └── library
  16. Library directory • アプリ非依存のパッケージ群 • 将来的に 2nd party 共通に なりえそうなパッケージ。

    . └── kauru ├── application ├── domain ├── infrastructure ├── interfaces └── library ├── device │ └── device.go ├── errutil │ └── temporary.go ├── io └── strrecord
  17. 気をつけているポイント • 各 Layer 配下は、 Flat Packages の概念で構築している。  Package を切る判断を的確に行うのが、ポイントに感じる。

    • Layer の中でも、特に Domain Layer は死守するスタンスを貫いている。  Domain が崩れると、Cycle Import 地獄と共に変更に弱くなる。 • 迷ったらシンプルな道を選択する。  個人的にも Keep It Simple Stupid の格言が、好きというのもある。
  18. そもそも Go 言語って、オブジェクトがないのでは? • 「Goはオブジェクト指向言語だろうか?」  http://postd.cc/is-go-object-oriented/  この @spf13 さんのエントリが、とても参考になります。 •

    ここでいう、オブジェクト指向とは? a. コードとデータとしての構造プログラムではなく、 ”オブジェクト”という概念を用いてこの2つを統合させる。 b. オブジェクトは、状態(データ)と振る舞い(コード)を 持つ抽象データ型とする。
  19. interface 定義のしかた • Method list を定義したもの。 • Method を持たない interface

    が interface{} です。 • 定義された全ての type は、 interface{} を満たしている。 type ( Duck interface { Waddler Quacker } Waddler interface { Waddle() (lat, lng float64) } Quacker interface { Quack() string } )
  20. interface 実装のしかた • Duck Typing の考え方 • Waddler と Quacker

    を実装 すれば、それは Duck となる。 type Osamingo struct { Lat float64 Lng float64 } func (o *Osamingo) Waddle() (float64, float64) { return o.Lat, o.Lng } func (o *Osamingo) Quack() string { return "Hey" } func main() { var o interface{} = &Osamingo{ Lat: 35.662056, Lng: 139.728451, } switch o.(type) { case Duck: fmt.Println("Hi, duck!") } }
  21. User 情報を DB に登録するフローの実例 • メルカリ カウルで、実際にやっている例です。 • 下記の STEP

    で紹介していきます。 a. エンティティの Interface 設計 b. User エンティティの実装 c. UserRepository の Interface 設計 d. UserRepository の実装 e. Cloud Datastore ライブラリの関数の Wrap
  22. Cloud Datastore とは • GCP が提供する、 Full-Managed NoSQL サービス •

    Pokémon GO などが利用している。 • 「Google Cloud Datastore Inside-Out」  https://www.slideshare.net/enakai/google-cloud-datastore-insideout
  23. エンティティの Interface • EntityBehavior  DB に格納される為の条件 • Identifier  ID のGetter/Setter

    • datastore.PropertyLoadSaver  Datastore からの Load, Save を管理する。 • UpdateTimestamper  UpdatedAt の管理 // Domain layer - domain/domain.go type ( EntityBehavior interface { Identifier datastore.PropertyLoadSaver } Identifier interface { GetID() string SetID(string) } UpdateTimestamper interface { GetUpdatedAt() time.Time SetUpdatedAt(time.Time) } )
  24. User エンティティの設計 • User エンティティを設計  特に難しいことはせず // Domain layer -

    domain/user.go type User struct { ID string `json:"id" datastore:"-" validate:”required”` ConnectedAt time.Time `json:"connected_at" datastore:"connected_at" validate:”required”` }
  25. User エンティティの実装 • User に EntityBehavior を実装 • Load, Save

    については、 エンティティ間で共通の関数を用 意する。 // Domain layer - domain/user.go func (u *User) GetID() string { return u.ID } func (u *User) SetID(id string) { u.ID = id } func (u *User) Load(p []datastore.Property) error { return Load(u, p) } func (u *User) Save() ([]datastore.Property, error) { return Save(u) }
  26. エンティティの Load 関数 • App Engine ライブラリの LoadStruct を利用 •

    UpdateTimestamper の実装 されていれば、読み込みを 行う。 // Domain layer - domain/domain.go func Load(dst interface{}, p []datastore.Property) error { err := datastore.LoadStruct(dst, p) if err != nil { return err } return LoadTimestamps(dst, p) } func LoadTimestamps(i interface{}, p []datastore.Property) error { ut, isUT := i.(UpdateTimestamper) if !isUT { return nil } for i := range p { if p[i].Name == "updated_at" { ut.SetUpdatedAt(p[i].Value.(time.Time)) } } return nil }
  27. エンティティの Save 関数 • App Engine ライブラリの SaveStruct を利用 •

    UpdateTimestamper 実装されて いれば、時刻をセットする。 // Domain layer - domain/domain.go func Save(src interface{}) ([]datastore.Property, error) { p, err := datastore.SaveStruct(src) if err != nil { return nil, err } return append(p, SaveTimestamps(src)...), nil } func SaveTimestamps(i interface{}) []datastore.Property { p := make([]datastore.Property, 0, 1) if ut, ok := i.(UpdateTimestamper); ok { ut.SetUpdatedAt(time.Now()) return append(nil, datastore.Property{ Name: "updated_at", Value: ut.GetUpdatedAt(), }) } return nil }
  28. Repository の Interface • Java 的な Interface 定義 • Infrastructure

    layer の persisntence パッケージが実装 を担当している。 // Domain layer - domain/user/user.go type repository interface { Get(c context.Context, id int64) (*domain.User, error) Save(c context.Context, u *domain.User) error } var Repository repository
  29. Repository の実装 • Infrastructure layer から、 Domain layer に DI

    をする。 • get, put に関しては、Repository 間で共通の関数を 用意する。 // Infrastructure layer - // infrastructure/persistence/user_repository.go type UserRepository datastoreRepository func NewUserRepository() *UserRepository { return &UserRepository{ kind: "User", } } func (r *UserRepository) Get(c context.Context, id int64) (*domain.User, error) { u := &domain.User{} u.SetID(id) return u, get(c, r.kind, u) } func (r *UserRepository) Save(c context.Context, u *domain.User) error { return put(c, r.kind, u) }
  30. Datastore 関数の Wrap • 引数には、kind とEntityBehavior を指定する。 • App Engine

    ライブラリの Get, Put を利用する。 // Infrastructure layer - // infrastructure/persistence/persistence.go func get(c context.Context, kind string, dst domain.EntityBehavior) error { key := datastore.NewKey(c, kind, dst.GetID(), 0, nil) return datastore.Get(c, key, dst) } func put(c context.Context, kind string, src domain.EntityBehavior) error { if err := validation.Check(src); err != nil { return err } key :-= datastore.NewKey(c, kind, src.GetID(), 0, nil) _, err := datastore.Put(c, key, src) return err }
  31. 工夫しているポイント • Entity としての interface と Repository としての interface の使い分け。

     Entity の場合は、振舞いを期待する。  Repository の場合は、 DI されることを期待する。 • App Engine ライブラリを Wrap して、利用する。  Entity としての interface を活用する。  
  32. より明確な DDD へ • アプリケーションが育つと共にロジックが、 Application layer になだれ込み、 必要以上に internal

    パッケージが肥大化している。 => 開発速度を優先する為、 DRY (Don’t Repeat Yourself) を心がけてきた。 => しかしながら、 DDD と DRY は相反することは、事実である。 => Interface, Application layers から Infrastrucre layer への参照を無くすべき。
  33. 初期化の統一 • アプリケーションが育つと共に初期化の処理が、 いつのまにか Interface layer と Infrastructure layer に分散している。

    => Interface layer では、 JSON-RPC の Method 登録 => Infrastructure layer では、 Domain repository の DI => そもそも、各 Layer で初期化すべきではない… (ヽ´ω`) => アプリケーションを起動する、 main 関数内で初期化処理を統一させる。
  34. Knowledge の切り出し • 紹介した Identifier や、 TimeStamper interface は良く使う機構 =>

    Datastore ライブラリを薄く Warp する仕組みとして切り出した方が、 汎用的しやすく、メンテナンスもやりやすい。 => ソウゾウ社では、新しいサービスも GAE + Datastore を使うコトが、 多いのでシナジーが生まれやすい。 => ライブラリ化し、テストも補充して提供していく。