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

2c58367194cdc9bb97f8e0fd5b20b511?s=47 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/

2c58367194cdc9bb97f8e0fd5b20b511?s=128

kent-hamaguchi

September 25, 2020
Tweet

Transcript

  1. MediaDo.go #2 Go初心者向け Clean Architectureとの付き合い方

  2. 目次 • パッケージの分け方 • リポジトリ • Use Case Interactor

  3. (書籍の)Clean Architectureは 何を伝えたいのか

  4. 優れたアーキテクチャに 共通して見られるルール

  5. ゆえに書籍のこの言葉につながる アーキテクチャの ルールはどれも 同じである!

  6. Clean Architectureは 何をクリーンに保ちたいのか

  7. ビジネスロジックを クリーンに保ちたい

  8. https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

  9. 時間が経てば ビジネスも テクノロジーも 開発者も変わる それらに対応し続けるために導入する

  10. ということで、コーディングなら このあたりの書籍や記事に求めると良い • 実践テスト駆動開発 (Steve Freeman, Nat Pryce) • Clean

    Code (Robert Cecil Martin) • https://refactoring.com/catalog/
  11. モデリングについては下記 • アナリシスパターン (Martin Fowler) • エリック・エヴァンスのドメイン駆動設計 (Eric Evans) •

    実践ドメイン駆動設計 (Vaughn Vernon)
  12. そうはいってもイメージが沸かないので Goを題材に設計を試す

  13. 本題

  14. パッケージ構成

  15. パッケージ構成 Clean Architectureの円に従い、ルートから下記のように切ってみる。 -- entity -- usecase -- adapter --

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

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

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

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

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

    -- search.go ビジネスロジックにおいて関係性が強いものは、 パッケージプライベートのオブジェクトを作用させるのも可
  21. パッケージ構成 Entityを扱う必要がなく、技術的観点が強めなら下記でも良い -- authentication -- cognito.go -- signin.go 認証とかビジネスロジックとちょっ と遠いから、

    一旦フラットに作るか・・・
  22. パッケージ構成 モジュールについては、下記のようにモノリスで作っていても... -- wallet -- book -- authentication -- go.mod

  23. パッケージ構成 切り出して別リポジトリにするときに、移動させるだけなので楽 -- wallet -- book -- authentication -- go.mod

    別リポジトリ (fuga/wallet) -- go.mod
  24. パッケージ構成 • あの円のレイヤにトップレベルを分ける必要はない ◦ 無理やり構成を合わせると管理しづらい (体験談含) ◦ 実装がFatになりがち、比例して開発コストは上がる ◦ ビジネスロジックを表現しづらくなる

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

  26. リポジトリ RepositoryはEntityの永続化と再構築の責務 book book repository book book book データベースや 外部のAPI

    プログラムの メモリ上に展開
  27. リポジトリ Repositoryはユースケースやドメインサービスから利用される Use Case Repository Service

  28. リポジトリ そのためこのような構成になる -- book -- series.go -- book.go -- repositoryの何かしらの定義

  29. リポジトリ repositoryはinterfaceで定義する -- book -- book.go -- repository -- book.go

    どちらかに type BookRepository interface { FindByID(string) (book, error) }
  30. リポジトリ 実装は別パッケージに分ける -- 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 } }
  31. リポジトリ 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 } }
  32. リポジトリ データベースへのマッピング処理はrepositoryの中身で完結させる -- book -- book.go -- repository -- book.go

    RDBとか 間にORM用の構造体を挟むなど 引数としてEntityのbookが渡される
  33. リポジトリ 実装が環境によって複数パターンあるなら、実装を増やして付け替える -- book -- book.go -- repository -- book.go

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

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

    単純にEntityの構造体をシリアライズするのもよいが、非公開フィールドを解決できない ◦ RDBやKVSや外部APIへの実装をEntityやUse Caseが意識しないようにする • Repositoryの作成単位はEntityに合わせる(ドメイン駆動設計の集約単位) ◦ 定義はEntityの隣りにあるが、実装は外側に配置 (依存性逆転の原則)
  36. Use Case Interactor

  37. Use Case Interactor ここから先の実装は大きくなりがちで、よほど複雑な要件でない限りコストが見合わなさ そうだが、一例を記す

  38. Use Case Interactor Clean ArchitectureではRepositoryだけでなく、入出力についても語られている • Use Caseを入出力から分離する • 入力処理をController、出力処理をPresenterに分ける

    ◦ 入力と出力を分離することで、 UIに関わる複雑な関係性をシンプルに保つ 流れの通りに実装するとUse CaseがPresenterに依存しがちになるなため、 Use Case Interactorを挟み、インターフェースで依存性を管理する
  39. 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自体の 入出力はシンプルな構造体 (可能な限り単純な値 )にしておく (実装内容はここでは重要ではないので省略 )
  40. 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の実行に対する流れはここが担う。
  41. 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はシンプルな値のやり取りで完結するため、 入出力の複雑さがビジネスロジックに介入しない。
  42. Use Case Interactor • 入出力が複雑な場合、インターフェースで分離して実装と流れを分ける ◦ 入力処理と出力処理を別々に実装でき、テストできる ◦ Use Caseの実装のシンプルさを保つことができる

    ◦ テスタビリティを獲得するかわりに、実装コストは上がる
  43. まとめ

  44. まとめ Clean Architectureにこだわりすぎると実装が辛くなる可能性がある • ビジネスロジックをクリーンに保ちたい場合は導入の検討をする ◦ Entityレベルをいかに実装できるかが鍵、外部の実装の逃し方は前述のとおり ◦ そうではなく、ライブラリ開発などの場合は実装が Fatになりすぎる可能性あり

    • アプリケーションの要件で形は変わってくるので、そこを中心に添えておく • 外部のことを考えずにEntityとUseCaseを実装すれば良い • 外のレイヤはControllerなどの枠に固めず、必要な分だけ実装すればよい
  45. Clean Architectureは コーディングにおける銀の弾丸ではない 効果を発揮しそうな要件を見極めて 使っていきましょう