My Favorite Life

Go there and write simply with golang. Such a person I want to be.

依存関係逆転の法則 with golang

先日書いたこちらの記事から一部抜粋したい

abemotion.hatenablog.com

今「API gatewayによるMicroservices化」プロジェクトを社内で進めている。
API gatewayには golang を採用した。

golang には、デファクトスタンダードフレームワークがまだ存在せず、
今回標準パッケージのみで開発することに決定。

レイヤ構成のベストプラクティスを調べていくうちに、
設計・デザインパターンへの理解が深まった。

フレームワークを使用しないという事は、レイヤ設計を自前で行う必要がある。

Go言語では循環参照があるとコンパイルエラーになってしまう。

今日は依存の方向性を考慮した設計について、自分の理解を残しておきたい。

大枠の設計は Lightweight DDD を採用した。

レイヤー例

interfaces層
↓
application層
↓
domain層
↓
infrastructure層

※矢印は参照の方向です。今回、大枠設計は説明の補足程度に使い、焦点をあてません。

依存関係逆転の法則(Dependency Inversion Principle : DIP)とは

上位のモジュールは下位のモジュールに依存してはならない。
どちらのモジュールも「抽象」に依存すべき
抽象は、実装の詳細に依存してはならない。
実装の詳細が抽象に依存すべきである

少しずつ噛み砕いていく。

まずはこの2文

上位のモジュールは下位のモジュールに依存してはならない。
どちらのモジュールも「抽象」に依存すべき

ここで重要なのは、
「上位のモジュールは下位のモジュールに依存してはならない」
という部分ではない。

循環参照でエラーになるからといって、
依存をなくすことは不可能。

A→Bという参照が発生する以上、
何かが何かに依存するという構図が生まれる。

ここで大切なのは、
「どちらのモジュールも抽象に依存すべき」
という部分。

モジュールではなく抽象に依存しよう。
という事。

では抽象とは具体的には何か?

その前に残りの2文

抽象は、実装の詳細に依存してはならない。
実装の詳細が抽象に依存すべきである

簡単に言うと

「抽象→実装」

ではなく

「実装→抽象」

に参照すべきという話。

まずは概念的な整理をした。

これを元に具体的な説明に入りたい。

  • what
  • how
  • why

what

まずは何をやるか。

概念だけ捉えても具体性がないと理解につながらない。

DIPで基本となるのが「抽象」だ。

「抽象」は
Go言語でいうとインターフェース(interface)にあたる。

まず前提として、
interface を用意し参照することで、
依存強度そのものを下げる必要がある。

how

次にどのようにやるか。

これはコードを見た方がわかりやすい。

type User interface {
        Save() User
}

type user struct {
        id   int
        name string
}

func NewUser(id int, name string) User {
        return &user{id, name}
}

func (u *user) Save() User {
        ...
}

まず interface を用意する。
※「まず」と書きましたが、実装の流れではなく、理解しやすい流れで書いてます。
 「抽象化を過度に先んじてやるのは悪手」とかの話とはまた別で。。。

type User interface {
        Save()
}

同じシグネチャのメソッドを実装すれば、
User interface を実装(implement)したことになる。

type user struct {
        Id   int
        Name string
}

...

func (u *user) Save() {
        ...
}

そして、user 構造体(struct)に、Save() メソッドを定義する。

この時点で user struct は User interface を implement する。

大事なのはここから

func NewUser(id int, name string) User {
        return &user{id, name}
}

NewUser() の返り値は User interface

返しているのは、user struct の実体そのものですが、
引数として保証するのは User interface かどうか。

golang は静的型付け言語なので、型情報で値の保証や制限ができる。

var u User
u = NewUser()

関数としての返り値も、NewUser() の利用側も、
User interface かを期待する。

つまりこれで実装の詳細ではなく、
interface・抽象を参照(抽象に依存)する形になる。

ここまでで「抽象への依存」の説明はできたので、 ここにDIPの「逆転」の概念を加える。

逆転とはなにか?

今までの説明で、特に逆転している所はなかった。

interfaces
↓
application
↓
domain
↓
infrastructure

矢印の方向で参照していった時に DIP を適用すると、

interfaces
↓
application
↓
domain
↑
infrastructure

最後の矢印が逆になる。
※ 適用のさせ方にもよりますが、概念的な説明のためにこうしています。

infrastructure の interface 定義を domain側で持ち、
実装コードで その interface を満たす。

一見、domain→infrastructureの方向で参照していても、
infrastructureは、domain側で定義されている interface を満たすよう要求されているので、
infrastructure → domain の方向で依存関係を保つことができる。

こうすることで、
domain 層はどの層へも依存しないよう死守する。

why

DIPをはなぜ適用するかは、
『レガシーコード改善ガイド』にも記述があったので抜粋したい

依存関係逆転の法則(Dependency Inversion Principle:DIP)
インターフェースに依存する場合、その依存は通常、とても小さく目立たないものです。
インターフェースを変更しなければ、コードを変更する必要はありません。
そして、インターフェースを変更することは、背後にある実装コードの変更に比べると、はるかに稀です。
インターフェースがあれば、そのインターフェースを使用するコードに影響を及ぼすことなく、
インターフェースの実装クラスを編集したり、新しい実装クラスを追加したりすることができます。
このような理由により、具象クラスに依存するよりも、インターフェースや抽象クラスに依存するほうが優れています。
変更の少ないものに依存することで、ある変更が巨大な再コンパイルを引き起こしてしまう可能性を小さくすることができます。

最後に

まだまだ依存性の注入(Dependency Injection : DI)の話などもありますが、
DIは、テストにおいての擬装クラス等にも関連してくるので今回は外しました。

参考

Goのサーバサイド実装におけるレイヤ設計とレイヤ内実装について考える
依存関係逆転の原則