$30 off During Our Annual Pro Sale. View Details »

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

mercari
PRO
September 30, 2017
30k

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

mercari
PRO

September 30, 2017
Tweet

More Decks by mercari

Transcript

  1. Mercari Tech Conf 2017
    Software Engineer
    Osamu TONOMORI
    Web アプリケーションにおける Go 言語の
    パッケージ構成 〜メルカリ カウル編〜

    View Slide

  2. 目次
    1. 自己紹介
    2. メルカリ カウルとは
    3. パッケージ構成について
    4. Interface の使いどころ
    5. 今後の展開
    6. まとめ

    View Slide

  3. 自己紹介

    View Slide

  4. 自己紹介
    • 主森 理 (Osamu TONOMORI)
    Screen name: @osamingo
    • 株式会社ソウゾウ
    メルカリ カウルチーム所属
    • Software Engineer
    Server-side, Gopher ʕ◔ϖ◔ʔ

    View Slide

  5. GitHub - https://github.com/osamingo

    View Slide

  6. はい、私がド真ん中の ふざけている 写真の人です

    View Slide

  7. View Slide

  8. メルカリ カウルとは

    View Slide

  9. View Slide

  10. View Slide

  11. View Slide

  12. GAE の使い方は、こちら - TECH PLAY Conference 2017
    • 「gae 新規 web」で、ググると出てきます。

    View Slide

  13. パッケージ構成

    View Slide

  14. Survey results ʕ◔ϖ◔ʔ who participated in golang.tokyo

    View Slide

  15. Advanced Testing in Go - GopherCon 2017
    • 「gophercon testing」でググると出てきます。

    View Slide

  16. パッケージ構成は、おおまかに分けて 3 種類(個人的主観)
    • One Package
    Repository 自体を単一のパッケージとみなす。
    • Flat Packages
    各パッケージの責務を明確にし、分割を行う。
    • Multiple Packages
    MVC や、 DDD など、各デザインパターンに準じる。

    View Slide

  17. One Package
    • Simple is Best (๑•̀ㅂ•́)و✧
    • Coverage も取りやすい。
    • Library など、簡素な構成で
    済むものに向いている。
    .
    ├── ctx.go
    ├── debug.go
    ├── error.go
    ├── handler.go
    ├── handler_func.go
    ├── jsonrpc.go
    ├── method.go
    └── parse.go

    View Slide

  18. Flat Packages と Multiple Packages の違い
    機能A 機能B 機能C
    機能A, 機能B, 機能C
    機能A, 機能B, 機能C
    機能A, 機能B, 機能C
    Flat Packages
    機能ごとに分割
    Multiple Packages
    デザインパターンに沿った分類

    View Slide

  19. 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

    View Slide

  20. 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

    View Slide

  21. メルカリ カウルは、 Multiple Packages です。
    • 1 Repository で管理されている、 Monolithic な Web サービス。
     GAE を利用しているので、 Project Based な GOPATH との相性が良い。
    • DDD を簡素化した様なデザインパターンを採用しています。
     先行の メルカリ アッテの構成を継承し、人材流動性も考慮したため。

    View Slide

  22. Google App Engine (GAE) とは?
    • Google Cloud Platform が提供する、Platform as a Service (PaaS)
    • 競合は、 Heroku, Engine Yard など

    View Slide

  23. 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

    View Slide

  24. Lightweight DDD
    • 4 つの Layer で構成している。
     Application
     Domain
     Infrastructure
     Interface(s)
    • Library ディレクトリの存在
     単体で成立するPackage 群
    .
    └── kauru
    ├── application
    ├── domain
    ├── infrastructure
    ├── interfaces
    └── library

    View Slide

  25. 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

    View Slide

  26. 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

    View Slide

  27. 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

    View Slide

  28. 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

    View Slide

  29. Library directory
    • アプリ非依存のパッケージ群
    • 将来的に 2nd party 共通に
    なりえそうなパッケージ。
    .
    └── kauru
    ├── application
    ├── domain
    ├── infrastructure
    ├── interfaces
    └── library
    ├── device
    │ └── device.go
    ├── errutil
    │ └── temporary.go
    ├── io
    └── strrecord

    View Slide

  30. 気をつけているポイント
    • 各 Layer 配下は、 Flat Packages の概念で構築している。
     Package を切る判断を的確に行うのが、ポイントに感じる。
    • Layer の中でも、特に Domain Layer は死守するスタンスを貫いている。
     Domain が崩れると、Cycle Import 地獄と共に変更に弱くなる。
    • 迷ったらシンプルな道を選択する。
     個人的にも Keep It Simple Stupid の格言が、好きというのもある。

    View Slide

  31. Interface の使いどころ

    View Slide

  32. なぜこの話になるのか
    • Multiple Packages で採用するデザインパターンでは、
    オブジェクト指向的な動作を期待されることが、多いのではないか。
    • Go 言語では、 Interface などを利用して、
    オブジェクト指向的な動作を期待させることが、出来る。

    View Slide

  33. そもそも Go 言語って、オブジェクトがないのでは?
    • 「Goはオブジェクト指向言語だろうか?」
     http://postd.cc/is-go-object-oriented/
     この @spf13 さんのエントリが、とても参考になります。
    • ここでいう、オブジェクト指向とは?
    a. コードとデータとしての構造プログラムではなく、
    ”オブジェクト”という概念を用いてこの2つを統合させる。
    b. オブジェクトは、状態(データ)と振る舞い(コード)を
    持つ抽象データ型とする。

    View Slide

  34. interface 定義のしかた
    • Method list を定義したもの。
    • Method を持たない interface が
    interface{} です。
    • 定義された全ての type は、
    interface{} を満たしている。
    type (
    Duck interface {
    Waddler
    Quacker
    }
    Waddler interface {
    Waddle() (lat, lng float64)
    }
    Quacker interface {
    Quack() string
    }
    )

    View Slide

  35. 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!")
    }
    }

    View Slide

  36. User 情報を DB に登録するフローの実例
    • メルカリ カウルで、実際にやっている例です。
    • 下記の STEP で紹介していきます。
    a. エンティティの Interface 設計
    b. User エンティティの実装
    c. UserRepository の Interface 設計
    d. UserRepository の実装
    e. Cloud Datastore ライブラリの関数の Wrap

    View Slide

  37. Cloud Datastore とは
    • GCP が提供する、 Full-Managed NoSQL サービス
    • Pokémon GO などが利用している。
    • 「Google Cloud Datastore Inside-Out」
     https://www.slideshare.net/enakai/google-cloud-datastore-insideout

    View Slide

  38. エンティティの 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)
    }
    )

    View Slide

  39. 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”`
    }

    View Slide

  40. 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)
    }

    View Slide

  41. エンティティの 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
    }

    View Slide

  42. エンティティの 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
    }

    View Slide

  43. 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

    View Slide

  44. 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)
    }

    View Slide

  45. 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
    }

    View Slide

  46. 工夫しているポイント
    • Entity としての interface と Repository としての interface の使い分け。
     Entity の場合は、振舞いを期待する。
     Repository の場合は、 DI されることを期待する。
    • App Engine ライブラリを Wrap して、利用する。
     Entity としての interface を活用する。
     

    View Slide

  47. 今後の展開

    View Slide

  48. より明確な DDD へ
    • アプリケーションが育つと共にロジックが、 Application layer になだれ込み、
    必要以上に internal パッケージが肥大化している。
    => 開発速度を優先する為、 DRY (Don’t Repeat Yourself) を心がけてきた。
    => しかしながら、 DDD と DRY は相反することは、事実である。
    => Interface, Application layers から Infrastrucre layer への参照を無くすべき。

    View Slide

  49. 初期化の統一
    • アプリケーションが育つと共に初期化の処理が、
    いつのまにか Interface layer と Infrastructure layer に分散している。
    => Interface layer では、 JSON-RPC の Method 登録
    => Infrastructure layer では、 Domain repository の DI
    => そもそも、各 Layer で初期化すべきではない… (ヽ´ω`)
    => アプリケーションを起動する、 main 関数内で初期化処理を統一させる。

    View Slide

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

    View Slide

  51. まとめ

    View Slide

  52. まとめ
    • 採用言語の思想は酌んだ上で設計を行うと良い。
     その言語の得意/不得意も考慮して設計するとスムーズ。
    • 最初から完璧は難しい。しかし、変更に強くすることは出来る。
     最初段階で、特定のケースは受け入れられない状態を作るのは NG。

    View Slide

  53. “ ”
    Clear is better than clever.
    Rob Pike
    Go Proverts, Google, Inc.

    View Slide

  54. Thanks for your attention!

    View Slide

  55. View Slide

  56. View Slide