logo

アルパカログ

Goにおける「依存性逆転の原則」に関するメモ

SOLID原則のひとつ、「依存性逆転の原則」はご存知でしょうか?

依存性の逆転のいちばんわかりやすい説明 ではわかりやすく下記のように説明されています。

依存性逆転の原則(Dependency Inversion Principle)とは、ざっくり言えば、プログラムの重要な部分が、重要でない部分に依存しないよう設計すべきであるということです。

例えばクリーンアーキテクチャでは、依存の方向に厳しい制約があります。

依存関係が重要なことは知っていても、実際にどうやって依存関係を逆転するかというテクニックを知らなければ、実際のプログラミングで生かすことはできません。

私は仕事でときどきGoを書くのですが、Goで依存性逆転の原則を実現する方法については無頓着でした。

そこでこの記事では、備忘録を兼ねてGoで依存性逆転の法則を実現するにあたっての要点をまとめます。

依存関係を逆転する方法

例えば下記のように、警官(Police Officer)があいさつする(Greet)プログラムがあるとします。

package main

import (
        "fmt"
)

type PoliceOfficer struct {}

func (po *PoliceOfficer) Greet() {
        fmt.Println("Hi")
}

// PoliceOfficer という具象に依存している
func contact(policeOfficer *PoliceOfficer) {
        policeOfficer.Greet()
}

func main() {
        policeOfficer := &PoliceOfficer{}
        contact(policeOfficer)
}

contact() 関数に注目してください。

contact() は引数として PoliceOfficer 構造体を受け取ることを定義しています。

つまり contact() は警官なしにはあいさつできない関数ということです。

このとき contact()PoliceOfficer に依存していると言います。

図にすると下記のようになります。

画像が読み込まれない場合はページを更新してみてください。
contact()はPoliceOfficerに依存している

contact() が警官に依存しないようにプログラムを変更してみます。

そのためにはまず「依存」について正しく理解しておく必要があります。

依存とは具象に対する依存のことで、抽象に対しては依存ではないということです。

💡
ちなみにnrsさんの 実践クリーンアーキテクチャ では、実際のアプリケーション開発にクリーンアーキテクチャを適用するにあたって、依存関係の問題を具体的にどう解決するのかが詳細に書かれています。

Goでは具象の構造体に対して、抽象はインターフェースで表すことができます。

すなわち、 contact() の引数として Greet() メソッドを持つインターフェースを定義しておくことで、 PoliceOfficer への依存を無くすことができます。

ここでは Greet() メソッドを持つインターフェースとして Human を定義してみましょう。

package main

import (
        "fmt"
)

// Greet() を持つインターフェースの定義
type Human interface {
        Greet()
}

type PoliceOfficer struct {}

func (po *PoliceOfficer) Greet() {
        fmt.Println("Hi")
}

// 引数として Human インターフェースを定義
func contact(human Human) {
        human.Greet()
}

func main() {
        policeOfficer := &PoliceOfficer{}
        contact(policeOfficer)
}

相変わらず contact() に渡されるのは警官ですが、contact() の引数定義が Human インターフェースという抽象になったことにより、 contact()PoliceOfficer への依存を切ることができました。

Goにおけるインターフェースの実装

インターフェース(抽象)を満たすクラスや構造体(具象)を定義することを「インターフェースを実装する」と言います。

Goでインターフェースを実装するにはどうしたら良いでしょうか?

例えばTypeScriptでは、インターフェースを実装するために implements キーワードが用意されています。

implement はプログラミングにおいて「実装する」という意味を持ちます。

interface Human {
  Greet(): void;
}

// implements キーワードを使って Human インターフェースを実装する
class PoliceOfficer implements Human {
  Greet() {
    console.log("Hi");
  }
}

実はGoには、 implements のように明示的にインターフェースを指定して実装する方法はありません。

単純に定義した構造体がインターフェースを満たしていれば良いのです。

すなわち「 Greet() メソッドを持つ」というインターフェースさえ満たしていれば、警官だけでなく消防士や教師、さらに言えば人間ですらないウニだって contact() に渡すことができます。

package main

import (
        "fmt"
)

type Human interface {
        Greet()
}

type PoliceOfficer struct {}

func (po *PoliceOfficer) Greet() {
        fmt.Println("Hi")
}

type Uni struct {}

// あいさつができるウニ
func (u *Uni) Greet() {
        fmt.Println("...")
}

func contact(human Human) {
        human.Greet()
}

func main() {
        policeOfficer := &PoliceOfficer{}
        contact(policeOfficer) // -> Hi
  
        uni := &Uni{}
        contact(uni) // -> ...
}

さすがに Human を前提としているところにウニを渡すなんてことは暴挙なのでやらないでくださいね。

とは言え、インターフェースを満たしていれば何でも良いというのは興味深いです。

以上です。

この記事では、備忘録を兼ねてGoで依存性逆転の法則を実現するにあたっての要点をまとめした。