search
LoginSignup
205

More than 3 years have passed since last update.

posted at

updated at

Goで書くClean Architecture API

はじめに

クリーンアーキテクチャの考え方を採用したコードをGoで書いてみました。
すでにいろんな方がソースを書かれておりますが、一例として見ていただければと思います。

ソース(メモとタグを新規登録、閲覧できるAPI)
https://github.com/muroon/memo_sample

クリーンアーキテクチャの構成図とソースの一覧

overview3.png

左側はクリーンアーキテクチャの構成図です。
右側は各ソースがクリーンアーキテクチャのどのレイヤーに属しているかを示しています。

Clean Architectureとは

Clean Architecture (クリーンアーキテクチャ) とは、 Robert C. Martin (Uncle Bob) 氏が提唱したアーキテクチャである。

こちらの書籍を読んでいる方が多いのではないでしょうか
Clean Architecture 達人に学ぶソフトウェアの構造と設計

導入のメリット

  • テストが書きやすい
    Mockを生成しやすくなることによりテストが実施しやすくなる

  • フレームワークに依存しない
    技術環境が変化、進化に対応しやすい
    あまりないケースかもしれないが、
    Webアーキテクチャとコンソールアーキテクチャを同じドメイン(ドメイン駆動設計で言うところのドメイン)で作成

  • データリソースからの独立
    フレームワークに依存しないのと同様、技術環境が変化に対応しやすい
    例えば、postgreSQLからMySQL、またはMongo, KVSに変更など

ルール

4つのレイヤー

アプリケーションの各機能を下記の4つのレイヤーに分類します。

レイヤー 内容
Enterprise Business Rules エンティティーモデル
Application Business Rules ビジネスロジックを担う
Interface Adapter Controller, Presenter
Frameworks & Drivers Frwamework, Database, View
Enterprise Business Rules

ビジネスルールの為のデータ構造を持ったオブジェクト。
データの実態を表す場所。

Application Business Rules

ビジネスルールを操作する場所。
つまりこのアプリケーションで何ができるかを実践します。

Interface Adapter

外部からの入力、データの永続化、表示を担当する場所

Frameworks & Drivers

Webフレームワーク、DB操作の実際に担うソース、
フロントエンドのUIなどがここに所属しています。

外側のレイヤーの要素を直接参照してはならない

izon.png

上記の図におけるこの矢印は依存を表しており、
内側のレイヤーから外側のレイヤーの要素への依存を禁じます。
ここでいう依存とは要素(構造体、変数など)への直接参照をさせないということです。

では外側のレイヤー要素を参照せざる得ないは、どうするのでしょうか?
例えば、ビジネスロジックからデータアクセスオブジェクト(敷いてはリーソースへ管理する箇所)

そのようなときは必ずインターフェイスを使用します。

詳しくは依存性逆転の原則

ソースについて

メモとタグを新規登録、閲覧できるRestAPIアプリです。

  • メモは関連するタグを付けることが可能
  • タグとメモはN対N

Repository

インターフェイス

repository_interface.png

  • アプリの使用要件からタグとメモそれぞれにrepositoryを設けます
    • tag_repository
    • memo_repository
  • トランザクションを担うtransaction_repositoryを設けます

実装部

Repositoryの実装はadapter配下に設けてあります。

repository_adapter.png

今回下記の2種類用意してあります。

  • db
  • memory (ソースのみ、Mock用)

一つのリポジトリに対して、dbとmemoryに分けたのは
例えば、usecaseの単体テストのときに一時的にdbではなく、Mockを使用したりすることが可能だからです。
規模が大きくなるとdbとmemoryのそれぞれ用意するほうがコスト高になるかもしれませんが
あるリポジトリだけMockにしたいみたいなケースもあるかと思います。

トランザクションなしの場合

// memo save
_, err = memoRepo.Save(ctx, "Memo Text")
if err != nil {
    panic(err)
}

// tag save
_, err = tagRepo.Save(ctx, "Tag Text")
if err != nil {
    panic(err)
}

トランザクションありの場合

defer func() {
    if err := recover(); err != nil {
        transactionRepo.Rollback(ctx)
    }
}()

// begin
ctx, err := transactionRepo.Begin(ctx)
if err != nil {
    panic(err)
}

// memo save
_, err = memoRepo.Save(ctx, "Memo Text")
if err != nil {
    panic(err)
}

// tag save
_, err = tagRepo.Save(ctx, "Tag Text")
if err != nil {
    panic(err)
}

// commit
_, err = transactionRepo.Commit(ctx)
if err != nil {
    panic(err)
}

つまり、memoリポジトリとtagリポジトリのメソッドはトランザクション有無にかかわらず同一のメソッドが使用できます。
メソッド内部でトランザクションの有無を見ています。

こちらgoroutineにも対応しており、並行処理でも実行可能です。
https://github.com/muroon/memo_sample/blob/master/adapter/db/multi_thread_test.go

Controller, Presenter

インターフェイス

interface.png

presenterのインターフェイスはUseCase層に所属します。

実装部

presenter.png

controller, presenterの実処理はInterface Adapter層に記載してあります。

Controller

requestパラメータをInput用モデルへ組み込み、UseCase Interactorに処理を渡しています。

func (c Controller) PostMemo(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    ctx = addResponseWriter(ctx, w)

    ipt := &input.PostMemo{Text: r.URL.Query().Get("text")}
    c.it.PostMemo(ctx, *ipt) // ←Interactorに処理を渡している
}

UseCase Interactor

Interactor内から各処理を行い、Presenterへ処理を渡します。

func (i Interactor) PostMemo(ctx context.Context, ipt input.PostMemo) {

    // 新規メモの登録
    id, err := i.memo.Post(ctx, ipt)
    if err != nil {
        i.pre.ViewError(ctx, err) // ←Presenterに処理を渡している
        return
    }

    // 登録メモの取得
    iptf := &input.GetMemo{ID: id}
    memo, err := i.memo.GetMemo(ctx, *iptf)
    if err != nil {
        i.pre.ViewError(ctx, err) // ←Presenterに処理を渡している
        return
    }

    i.pre.ViewMemo(ctx, memo) // ←Presenterに処理を渡している
}

Presenter

プレゼンターでViewモデルの生成、表示を行います。

func (m presenter) ViewMemo(ctx context.Context, md *model.Memo) {
    defer deleteResponseWriter(ctx)
    w := getResponseWriter(ctx)

    m.JSON(ctx, w, m.render.ConvertMemoJSON(md))
}

最後に

Dependency Injectionを利用して環境変化に対応した仕組みを作る考え方はすごいと思いました。

自分でつくってこんなにも時間がかかるものかと思いました。

ただ、正しいソフトウェアをつくることが最終的には最もはやくソフトウェアをつくることに繋がるとボブおじさんもおっしゃってました。

参考

【ボブおじさんのClean Architectureまとめ】オブジェクト指向 ~SOLIDの原則~

実装クリーンアーキテクチャ

Clean ArchitectureでAPI Serverを構築してみる

やはりお前たちのRepositoryは間違っている

クリーンアーキテクチャの書籍を読んだのでAPIサーバを実装してみた

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
205