S.Hayakawaをフォローする

GoのDependency Inversion – 依存性逆転

バックエンド

はじめに

このブログの目的

主に社内で最近Goを始めた方向けに、依存性の扱い方について勉強会で紹介するための記事です。
コード例はGoで記載しますが、
Dependency Injection(依存性注入)、Dependency Inversionの原則(依存性逆転の原則)の基本的な考え方について扱うので、良ければご覧になってください。

Dependency InversionとDependency Injection

Dependency Inversion principle(DIP)

Dependency Inversionの原則(依存性逆転の原則)は、SOLID原則の一つで、オブジェクト指向プログラムの学習の過程で一度は見かけたことがあるのではないでしょうか。

  1. 高レベルのモジュールは低レベルのモジュールに依存してはならない。両者は抽象に依存すべきである。
  2. 抽象は詳細に依存してはならない。詳細は抽象に依存すべきである。

この説明を見てなるほどね、となる方はDIマスター?です。
このあとGoのコードを例に説明します。
(Goはオブジェクト指向言語ではありませんが、DIPの考え方は取り入れることができます。)

Dependency Injection(DI)

Dependency Injection(依存性注入)は、対象が必要とする依存関係を外部から注入する設計パターンです。
要は依存するものがあるなら、内部で生成せずに外からもらってこようね、と言うことです。
こちらも後で具体例を提示します。

Dependency Injection(DI)の基本概念

Dependency Injectionの基本的な考え方は、オブジェクトが自分で依存関係を生成するのではなく、外部から依存関係を受け取ることです。

DIの概念なしにプログラムした場合の問題点:

  • 柔軟性がなく後から変更しにくい
  • テストしにくい

DIを導入する方法:

  • 依存関係にあるものは、引数で受け取る
Go言語におけるDependency Injectionの実装方法

DIなしの例

コンストラクタのNewService()に注目してください。
Serviceが完全にMyRepositoryに依存しています。

type MyRepository struct{}

type Service struct {
    Repository *MyRepository
}

func NewService() *Service {
    return &Service{Repository: &MyRepository{}}
}

func main() {
    service := NewService()
}

DIパターン

依存していたものを引数で受け取るようにします。

type MyRepository struct{}

type Service struct {
    Repository Repository
}

func NewService(repo Repository) *Service {
    return &Service{Repository: repo}
}

func main() {
    repo := &MyRepository{}
    service := NewService(repo)
}

DIの基本的な考え方はこれだけです。

(おまけ)
メソッドインジェクション
コンストラクタ引数以外にも、メソッドの引数として依存関係を渡すケースもあります。
特定のメソッドにのみ依存関係を注入したい場合に有効です。

func (s *Service) Action(repo Repository) {
    // メソッド内で依存関係を使用
}

Dependency Inversion(DIP)の基本概念

Dependency Inversionの原則(DIP)をなるべく噛み砕いてみると、
依存関係は具体で受け取らずに、抽象化して扱いましょう。と言い換えられるかなと思っています。

以下GoでのDIPの例を見ていきましょう。

Go言語におけるDependency Inversionの実装方法

GoでDIPの概念を導入する時は、Dependency Injection(DI)の考え方と、Interfaceによる抽象化を用います。

DIの例で示したサンプルコードは、実はDIPの例にもなっています。
もう一度見てみましょう。

type MyRepository struct{}

type Service struct {
    Repository Repository
}

func NewService(repo Repository) *Service {
    return &Service{Repository: repo}
}

func main() {
    repo := &MyRepository{}
    service := NewService(repo)
}

NewService()の引数の型がMyRepositoryではないことに注目してください。
簡略のために省略していましたが、RepositoryはInterfaceです。
MyRepositoryがRepository interfaceを実装していれば、NewServiceの引数として渡すことができます。

こちらに書き直します。
Repository interfaceの定義と、MyRepositoryの実装を追加しました。

type Repository interface { // new
    GetData() string
}

type MyRepository struct{}

func (r *MyRepository) GetData() string { // new
    return "Hello, World!"
}

type Service struct {
    Repository Repository
}

func NewService(repo Repository) *Service {
    return &Service{Repository: repo}
}

func main() {
    repo := &MyRepository{}
    service := NewService(repo)
}

DIパターンとして引数で依存関係を受け取りますが、引数の型はInterfaceで抽象化しています。
具体ではなく抽象に依存することでコードの柔軟性を高めています。

なにが嬉しいか?

  • Interfaceを満たせば何でも引数に渡せるので、メンテナンスやテスト(mock)がしやすい

例えば、
・MyRepositoryではMySQLを使っていたが、PostgreSQLを使用したくなったのでPostgreRepositoryを作って渡す
・テストの時はRepository Interfaceを満たすMock Repositoryを作って渡す
といった使い方が有効です。

まとめ

Goのコードを例にDependency InversionとDependency Injectionの基本的な考え方について説明しました。
うまく活用してより良いコードを書いていきましょう。Happy Coding!

SOLIDについてもっと知りたい方は、こちらの記事から読んでみてください。
イラストで理解するSOLID原則

画像

Photo by tenntenn / CC BY 3.0
https://github.com/tenntenn/gopher-stickers
designed by Renee French.