🐉

DI: 依存性逆転の原則

commits6 min read

モチベーション

クリーンアーキテクチャやSOLIDを勉強して、記事書いたり個人開発や業務で活かしたりしてましたが、正直DIは上っ面しか理解できてませんでした。
しかし、業務や個人開発を通してようやくDIの威力が身に染みてわかるようになってきました。

というわけでDIについてまとめることにしました。

こちらと少し被るかも。今回はどちらかというと概念にフォーカスして進めていきます。

https://zenn.dev/maru44/articles/a45d1150cb3986

依存関係逆転の原則とは

SOLIDの原則(The Dependency Inversion Principle)のDにあたります。個人的にSOLIDの中で最も印象的、重要で、難しいと思っています。
名前が非常に非直感的ですが、簡単に言ってしまえば抽象に依存しましょうということです。
具象(実装)に依存するとしんどいことが沢山あるのです。

補足

DIについてクリーンアーキテクチャでは、以下のように補足説明がなされています。

  • 変化しやすい具象クラスを参照しない
  • 変化しやすい具象クラスを継承しない
  • 具象関数をオーバーライドしない
  • 変化しやすい具象を名指しで参照しない

これらは具象(実装)が変化しやすいことが理由です。みなさんも業務等でさんざん味わってきたと思います。
また、抽象の変更はその具象の実装の変更に直結しますが、実装が変更しても抽象を変更しなければならないわけではありません。
それ故に抽象に依存することで実装の変更の影響を受けにくくなります。

優れたソフトウェア設計者やアーキテクトはインターフェースの変動性をできる限り抑えられる人間らしいです。
適切な粒度でインターフェース(抽象)を設定できる人 = 優れたアーキテクトと自分は解釈しました。

DIのメリットとは

DIのメリットを雑なコードで説明します。
雑とはいいつつ長くなります。

コントローラーを例にとって説明したいと思います。

blog_controller.go
package controllers

import (
    ".../domain"
    ".../usecase"
    "net/http"
)

type BlogController struct {
    in domain.BlogInteractor // 抽象(インターフェース)に依存
}

// net/httpを使って実際にhttpとして実装する用
// interactorにはsqlを注入している
func NewBlogController(sql database.SqlHandler) *BlogController {
    return &BlogController{
        in: usecase.NewBlogInteractor{
            SqlHandler: sql,
        }
    }
}

func (c *BlogController) BlogListView(w http.ResponseWriter, r *http.Request) *BlogController {
    blogs, err := c.in.BlogList()
    response(w, r, err, map[string]{"blogs": blogs})
    return
}

前提のコード

前提として使うコードです。

エンティティ

ビジネスルールを書きます

blog.go
package domain

type Blog struct {
    ID      int    `json:"id"`
    Title   string `json:"title"`
    Content string `json:"content"`
}

// interactor の抽象
type BlogInteractor interface {
    BlogList() ([]Blog, error)
}

ユースケース

blog_interactor.go
package usecase

import ".../domain"

// interactor の実装
type BlogInteractor struct {
    repo BlogRepository // 抽象(インターフェース)に依存
}

// このように書いているので勿論 BlogInteractorはdomain.BlogInteractorのインターフェースを満たす必要がある
// 返すのは勿論 interface(抽象)
func NewBlogInteractor(repo BlogRepository) domain.BlogInteractor {
    return &BlogInteractor{
        repo: repo,
    }
}

// リポジトリの抽象
type BlogRepository interface {
    GetBlogList() ([]domain.Blog, error)
}

func (in *BlogInteractor) BlogList() ([]domain.Blog, error) {
    return in.repo.GetListBlog()
}

リポジトリ(persistent)

blog_repository.go
package database

type BlogRepository struct {
    SqlHandler // これもインターフェース
}

func (repo *BlogRepository) GetListBlog() ([]domain.Blog, error) {
    rows, err := repo.Query(
        "SELECT id, title, content FROM blogs",
    )
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    for rows.Next(
        var b domain.Blog
        if err := rows.Scan(&b.ID, &b.Title, &b.Content); err != nil {
            return nil, err
        }
        blogs = append(blogs, b)
    )
    return blogs, err
}

こういう実装があります。
この時点では正直まだそこまで旨味はありません。

ではコントローラーのテストを書きたいとします。
以下の関数のユニットテストを書くことにしましょう。

blog_controller.go
func (c *BlogController) BlogListView(w http.ResponseWriter, r *http.Request) *BlogController {
    blogs, err := c.in.BlogList()
    response(w, r, err, map[string]{"data": blogs})
    return
}

今回は非常にシンプルな形で実装しているので以下のような場合を想定するとイメージしやすいです。

blog_controller.go
func (c *BlogController) BlogListView(w http.ResponseWriter, r *http.Request) *BlogController {
    // 処理A
    //
    // あーだこーだ(例えばリクエストボディをあーだこーだ)
    //
    // 

    blogs, err := c.in.BlogList()

    // 処理B
    //
    // あーだこーだ
    //
    // 

    response(w, r, err, map[string]{"data": blogs})
    return
}

コントローラーのユニットテストで確かめたいのは処理Aからreturnまでの動作じゃないでしょうか。DBがうんたらかんたらなんてコントローラーにとっては本当にどうでもいいのです。
しかし、もしMVCのようなアーキテクチャで何も工夫がない場合、DB(モデル)への依存が切り離せていないことがあります。

では、DIするとどうなるか示します。
少し面倒なのでtable drivenには書きません。

テスト

blog_controller_test.go
package controllers

import (
    ".../domain"
    ".../usecase"
    "net/http"
    "net/http/httptest"
    "testing"
)

// まずユースケースのモックを作る
type (
    // これはdomain.BlogInteractorインターフェースを満たす必要がある。
    // そのためここはユースケースで定義した実装をembedする
    blogInteractor struct {
        usecase.BlogInteractor
        // 実装の中身は
        // type BlogInteractor struct {
        //     repo BlogRepository これはインターフェースだよ
        // }
    }
)

// ユースケースの実装(ダミー)
func (in *blogInteractor) BlogList() ([]domain.Blog, error) {
    return []domain.Blog{}, nil
}

func TestBlog_GetList(t *testing.T) {
    con := BlogController{
		in: &blogInteractor{},
	}

    r := httptest.NewRequest(http.MethodGet, "http://abc/def", nil)
    got := httptest.NewRecorder()
    con.BlogListView(got, r)
    
    assert.Equal(t, 200, got.Result().Status)
} 

こうすると何が起こるでしょう?
BlogListView()という関数には私は何も手を加えていません。
しかし、BlogListView()メソッドを所有するコントローラーが依存するものに手を加えました。
コントローラーが依存するBlogInteractor(インターフェース)にテスト用のモック(実装)を使用することでc.in.BlogList()の部分のふるまいを変えることに成功しているのです。

抽象(インターフェース)に依存してコントローラーやユースケース等を作っているためこのようにテストの時にはテストの実装への依存を注入、実行環境では実行の実装への依存を注入することができてしまいます。
控えめに言ってえぐくないでしょうか?

また、DBに依存しておらず疎結合になっていることもこういったコードに書き起こすことでわかりますね。

まとめ

すばらしきかなDI

とにかく具象(実装)に依存せず抽象に依存しましょう。

カレー(具象)はカレー🍛でしかありません。
人参🥕、玉ねぎ🧅、ジャガイモ🥔、ゴロゴロした牛肉🍖のまざったもの(抽象)に依存しましょう。
カレールーを入れてカレーにも、ビーフシチューにすることも、ビーツとトマトを入れてボルシチにもできてしまいます。
ルーは後から入れればいいんです。そしてルーは詳細です。

イージーエイトもファイアフライもスーパーシャーマンもM4中戦車です。
M4中戦車に依存しましょう。メンテや運用がしやすくなるはずです。

こう覚えましょう。覚えるまで何度も唱えましょう。
DI = 抽象に依存しましょう
DI = 抽象に依存しましょう
DI = 抽象に依存しましょう
DI = 抽象に依存しましょう
DI = 抽象に依存しましょう

GitHubで編集を提案

Discussion

ログインするとコメントできます