Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

ということで、コーディングなら このあたりの書籍や記事に求めると良い ● 実践テスト駆動開発 (Steve Freeman, Nat Pryce) ● Clean Code (Robert Cecil Martin) ● https://refactoring.com/catalog/

Slide 11

Slide 11 text

モデリングについては下記 ● アナリシスパターン (Martin Fowler) ● エリック・エヴァンスのドメイン駆動設計 (Eric Evans) ● 実践ドメイン駆動設計 (Vaughn Vernon)

Slide 12

Slide 12 text

Slide 13

Slide 13 text

そうはいってもイメージが沸かないので Goを題材に設計を試す

Slide 14

Slide 14 text

本題

Slide 15

Slide 15 text

パッケージ構成

Slide 16

Slide 16 text

パッケージ構成 Clean Architectureの円に従い、ルートから下記のように切ってみる。 -- entity -- usecase -- adapter -- infrastructure やってみた系で結構見かける構成

Slide 17

Slide 17 text

パッケージ構成 技術的目線でルートを切ってしまうと、ビジネスロジックの表現力が乏しくなる -- entity -- wallet.go -- book.go -- authentication.go パッケージプライベートなので お互いのすべてを参照可能、それを前提に作ると ビジネスロジックの凝集度が下がり、結合度が上がる

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

パッケージ構成 下記のほうが自然 -- wallet -- wallet.go (もしくはentity配下にwallet.go) -- usecase -- deposit.go -- adapter -- 〜〜〜 walletのことを豊かに表現できる幅がでる 必要に応じてパッケージ追加

Slide 21

Slide 21 text

パッケージ構成 他のEntityには非依存なのでbookも個別にパッケージ化 -- book -- series.go -- book.go -- usecase -- search.go ビジネスロジックにおいて関係性が強いものは、 パッケージプライベートのオブジェクトを作用させるのも可

Slide 22

Slide 22 text

パッケージ構成 Entityを扱う必要がなく、技術的観点が強めなら下記でも良い -- authentication -- cognito.go -- signin.go 認証とかビジネスロジックとちょっ と遠いから、 一旦フラットに作るか・・・

Slide 23

Slide 23 text

パッケージ構成 モジュールについては、下記のようにモノリスで作っていても... -- wallet -- book -- authentication -- go.mod

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

パッケージ構成 ● あの円のレイヤにトップレベルを分ける必要はない ○ 無理やり構成を合わせると管理しづらい (体験談含) ○ 実装がFatになりがち、比例して開発コストは上がる ○ ビジネスロジックを表現しづらくなる ● Goの可視性を活用する ○ 大文字で始めればパブリック、小文字で始まるならプライベート ■ Javaでいうクラス単位くらいの気持ちでパッケージを切っても良い ○ パッケージプライベートを活用する ● 単一のGo modulesから分離させるときも楽

Slide 26

Slide 26 text

リポジトリ

Slide 27

Slide 27 text

リポジトリ RepositoryはEntityの永続化と再構築の責務 book book repository book book book データベースや 外部のAPI プログラムの メモリ上に展開

Slide 28

Slide 28 text

リポジトリ Repositoryはユースケースやドメインサービスから利用される Use Case Repository Service

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

リポジトリ repositoryはinterfaceで定義する -- book -- book.go -- repository -- book.go どちらかに type BookRepository interface { FindByID(string) (book, error) }

Slide 31

Slide 31 text

リポジトリ 実装は別パッケージに分ける -- 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 } }

Slide 32

Slide 32 text

リポジトリ 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 } }

Slide 33

Slide 33 text

リポジトリ データベースへのマッピング処理はrepositoryの中身で完結させる -- book -- book.go -- repository -- book.go RDBとか 間にORM用の構造体を挟むなど 引数としてEntityのbookが渡される

Slide 34

Slide 34 text

リポジトリ 実装が環境によって複数パターンあるなら、実装を増やして付け替える -- book -- book.go -- repository -- book.go -- book_mock.go (Go modulesは分けたほうが良いけど ) モック用の実装と付替できるようにするなど

Slide 35

Slide 35 text

リポジトリ データストアのことを考えて共通化が必要ならinternalにそれを出すのもあり -- book -- book.go -- repository -- book.go -- internal -- rdb.go アプリケーション全体へ渡り共通の処理があるなら、 外部のモジュールから importできないinternalパッケージに 実装をまとめて利用するなど

Slide 36

Slide 36 text

リポジトリ ● まだ実装が固まらない間はインターフェースと実装が隣りにあってもよい ○ インターフェースをパブリックにして、実装をパッケージプライベートにする ○ interfaceの定義を組み直したりする際に、定義は一箇所のほうがシンプルに作業が楽 ● Entityの構造体とRepositoryの内部で扱う構造体は別 ○ 単純にEntityの構造体をシリアライズするのもよいが、非公開フィールドを解決できない ○ RDBやKVSや外部APIへの実装をEntityやUse Caseが意識しないようにする ● Repositoryの作成単位はEntityに合わせる(ドメイン駆動設計の集約単位) ○ 定義はEntityの隣りにあるが、実装は外側に配置 (依存性逆転の原則)

Slide 37

Slide 37 text

Use Case Interactor

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

Use Case Interactor Clean ArchitectureではRepositoryだけでなく、入出力についても語られている ● Use Caseを入出力から分離する ● 入力処理をController、出力処理をPresenterに分ける ○ 入力と出力を分離することで、 UIに関わる複雑な関係性をシンプルに保つ 流れの通りに実装するとUse CaseがPresenterに依存しがちになるなため、 Use Case Interactorを挟み、インターフェースで依存性を管理する

Slide 40

Slide 40 text

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自体の 入出力はシンプルな構造体 (可能な限り単純な値 )にしておく (実装内容はここでは重要ではないので省略 )

Slide 41

Slide 41 text

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の実行に対する流れはここが担う。

Slide 42

Slide 42 text

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はシンプルな値のやり取りで完結するため、 入出力の複雑さがビジネスロジックに介入しない。

Slide 43

Slide 43 text

Use Case Interactor ● 入出力が複雑な場合、インターフェースで分離して実装と流れを分ける ○ 入力処理と出力処理を別々に実装でき、テストできる ○ Use Caseの実装のシンプルさを保つことができる ○ テスタビリティを獲得するかわりに、実装コストは上がる

Slide 44

Slide 44 text

まとめ

Slide 45

Slide 45 text

まとめ Clean Architectureにこだわりすぎると実装が辛くなる可能性がある ● ビジネスロジックをクリーンに保ちたい場合は導入の検討をする ○ Entityレベルをいかに実装できるかが鍵、外部の実装の逃し方は前述のとおり ○ そうではなく、ライブラリ開発などの場合は実装が Fatになりすぎる可能性あり ● アプリケーションの要件で形は変わってくるので、そこを中心に添えておく ● 外部のことを考えずにEntityとUseCaseを実装すれば良い ● 外のレイヤはControllerなどの枠に固めず、必要な分だけ実装すればよい

Slide 46

Slide 46 text

Clean Architectureは コーディングにおける銀の弾丸ではない 効果を発揮しそうな要件を見極めて 使っていきましょう

Slide 47

Slide 47 text