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

MediaDo.go #2 Clean Architectureとの付き合い方/mediado-go-2-clean-architecture

kent-hamaguchi
September 25, 2020

MediaDo.go #2 Clean Architectureとの付き合い方/mediado-go-2-clean-architecture

MediaDo.go #2 というGoの勉強会で発表したスライドです。
https://mediado-go.connpass.com/event/186625/

kent-hamaguchi

September 25, 2020
Tweet

More Decks by kent-hamaguchi

Other Decks in Technology

Transcript

  1. パッケージ構成 技術的目線でルートを切ってしまうと、ビジネスロジックの表現力が乏しくなる -- entity -- wallet.go -- book.go -- authentication.go

    パッケージプライベートなので お互いのすべてを参照可能、それを前提に作ると ビジネスロジックの凝集度が下がり、結合度が上がる
  2. パッケージ構成 -- entity -- wallet -- wallet.go -- book --

    book.go パッケージが分かれることで凝集度が上がり、結合度を下げられるが・・・
  3. パッケージ構成 -- entity -- wallet -- wallet.go -- usecase --

    wallet -- wallet.go -- adapter -- wallet -- wallet.go -- infrastructure -- wallet -- wallet.go 同じような構成のサブパッケージが 各種レイヤーに乱立しやすくなる wallet視点で見ると凝集度が低い
  4. パッケージ構成 下記のほうが自然 -- wallet -- wallet.go (もしくはentity配下にwallet.go) -- usecase --

    deposit.go -- adapter -- 〜〜〜 walletのことを豊かに表現できる幅がでる 必要に応じてパッケージ追加
  5. パッケージ構成 他のEntityには非依存なのでbookも個別にパッケージ化 -- book -- series.go -- book.go -- usecase

    -- search.go ビジネスロジックにおいて関係性が強いものは、 パッケージプライベートのオブジェクトを作用させるのも可
  6. パッケージ構成 • あの円のレイヤにトップレベルを分ける必要はない ◦ 無理やり構成を合わせると管理しづらい (体験談含) ◦ 実装がFatになりがち、比例して開発コストは上がる ◦ ビジネスロジックを表現しづらくなる

    • Goの可視性を活用する ◦ 大文字で始めればパブリック、小文字で始まるならプライベート ▪ Javaでいうクラス単位くらいの気持ちでパッケージを切っても良い ◦ パッケージプライベートを活用する • 単一のGo modulesから分離させるときも楽
  7. リポジトリ repositoryはinterfaceで定義する -- book -- book.go -- repository -- book.go

    どちらかに type BookRepository interface { FindByID(string) (book, error) }
  8. リポジトリ 実装は別パッケージに分ける -- book -- book.go -- adapter -- repository

    -- book.go import fuga/book type BookRepository struct { book.BookRepository db *sql.DB // AWSとか使うならそのAPIインターフェース } func NewBookRepository(db *sql.DB) book.BookRepository { return { db: db } }
  9. リポジトリ adapterとか外のパッケージにせず、repositoryパッケージで完結も有り -- book -- book.go -- repository -- book.go

    type BookRepository interface { findByID(string) (book, error) } // 小文字始まりなので他のパッケージから参照できない type bookRepository struct { BookRepository db *sql.DB // AWSとか使うならそのAPIインターフェース } func NewBookRepository(db *sql.DB) BookRepository { return { db: db } }
  10. リポジトリ 実装が環境によって複数パターンあるなら、実装を増やして付け替える -- book -- book.go -- repository -- book.go

    -- book_mock.go (Go modulesは分けたほうが良いけど ) モック用の実装と付替できるようにするなど
  11. リポジトリ データストアのことを考えて共通化が必要ならinternalにそれを出すのもあり -- book -- book.go -- repository -- book.go

    -- internal -- rdb.go アプリケーション全体へ渡り共通の処理があるなら、 外部のモジュールから importできないinternalパッケージに 実装をまとめて利用するなど
  12. リポジトリ • まだ実装が固まらない間はインターフェースと実装が隣りにあってもよい ◦ インターフェースをパブリックにして、実装をパッケージプライベートにする ◦ interfaceの定義を組み直したりする際に、定義は一箇所のほうがシンプルに作業が楽 • Entityの構造体とRepositoryの内部で扱う構造体は別 ◦

    単純にEntityの構造体をシリアライズするのもよいが、非公開フィールドを解決できない ◦ RDBやKVSや外部APIへの実装をEntityやUse Caseが意識しないようにする • Repositoryの作成単位はEntityに合わせる(ドメイン駆動設計の集約単位) ◦ 定義はEntityの隣りにあるが、実装は外側に配置 (依存性逆転の原則)
  13. Use Case Interactor Clean ArchitectureではRepositoryだけでなく、入出力についても語られている • Use Caseを入出力から分離する • 入力処理をController、出力処理をPresenterに分ける

    ◦ 入力と出力を分離することで、 UIに関わる複雑な関係性をシンプルに保つ 流れの通りに実装するとUse CaseがPresenterに依存しがちになるなため、 Use Case Interactorを挟み、インターフェースで依存性を管理する
  14. Use Case Interactor type Usecase interface { Deposit(DepositInput) (DepositOutput, error)

    } type DepositInput struct { UserID string Amount int } type DepositOutput struct { TotalAmount int } Use Case (walletのusecaseのつもり) EntityやRepositoryの操作を実装するUse Case自体の 入出力はシンプルな構造体 (可能な限り単純な値 )にしておく (実装内容はここでは重要ではないので省略 )
  15. Use Case Interactor type UsecaseInteractor struct { usecase Usecase controller

    Controller presenter Presenter } type Controller interface { ReadDepositInput() (DepositInput, error) } type Presenter interface { WriteDepositOutput(DepositOutput) error } Use Case Interactorの定義 入出力処理は別々のインターフェースに分離し、 UsecaseInteractorの実装の中で流れを作る。 ControllerとPresenterに入出力処理は委ねられるが、 Usecaseの実行に対する流れはここが担う。
  16. Use Case Interactor func (u *UsecaseInteractor) Deposit() error { in,

    err := u.controller.ReadDepositInput() if err != nil { return err } out, err := u.usecase.Deposit(in) if err != nil { return err } return u.presenter.WriteDepositOutput(out) } Use Case Interactorの実装 Use Case Interactorで定義したインターフェースに ControllerとPresenterは実装をするため、ここで依存性の 逆転が発生し、外部の環境に依存する実装が外側のレイ ヤにまとまる。 Use Caseはシンプルな値のやり取りで完結するため、 入出力の複雑さがビジネスロジックに介入しない。
  17. まとめ Clean Architectureにこだわりすぎると実装が辛くなる可能性がある • ビジネスロジックをクリーンに保ちたい場合は導入の検討をする ◦ Entityレベルをいかに実装できるかが鍵、外部の実装の逃し方は前述のとおり ◦ そうではなく、ライブラリ開発などの場合は実装が Fatになりすぎる可能性あり

    • アプリケーションの要件で形は変わってくるので、そこを中心に添えておく • 外部のことを考えずにEntityとUseCaseを実装すれば良い • 外のレイヤはControllerなどの枠に固めず、必要な分だけ実装すればよい