依存関係逆転の原則の重要性について

こんにちは!eurekaのAPIチームでエンジニアをやっているrikiiです。

最近ついにAPIチームでモブプロを始めました。前は設計や実装について一人で悩んでたりした部分が、すぐ議論できたりホワイトボードに図で書いて理解を深めたりして、問題が素早く解決できてすごくいい感じで進んでいます。
さて、今回も前回の続きでSOLID原則の1つのDIP(依存関係逆転の原則)について書こうと思います。 eurekaではgo言語を使っているので、goを使ったコード例とともに説明していきたいと思います。
ちなみに依存関係逆転の原則とはSOLID原則と呼ばれるオブジェクト指向設計原則のうちのひとつです。

SOLID原則とは?

下記5つの原則の頭文字を取ってまとめた、オブジェクト指向設計原則のことです。
S : The Single Responsibility Principle(単一責任の原則)
O : The Open Closed Principle(オープン・クローズドの原則)
L : The Liskov Substitution Principle(リスコフの置換原則)
I : The Interface Segregation Principle(インターフェース分離の原則)
D : The Dependency Inversion Principle(依存性逆転の原則)

依存関係逆転の原則(DIP)

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

最初は上位レイヤが下位レイヤに依存するのは当然だし、、、なんのこっちゃと思っていました。

DIPを一言でまとめれば、「抽象に依存せよ」ということらしいです。
通常の実装だと、上位のレイヤが下位のレイヤの実体を持つようになりがちかと思います。
例えば、ServiceがRepositoryの実体に依存するなどです。
しかし、Repositoryに大きな修正が発生すると、依存している全てのServiceに影響が出てしまう可能性があります。
本来、アプリケーションの存在意義を決めているのは上位のレイヤなので、下位レイヤの修正が上位レイヤに影響を及ぼしてしまうことは避けなければいけません。
その解決方法として、上位レイヤは下位レイヤの実体に依存するのではなく、上位レイヤで宣言した抽象に依存するようにしましょうというのがDIPです。
これだけではよくわからないので、具体例で、

  • どういう場合に困るのか?
  • どういうメリットがあるのか?

の2つについて説明していきたいと思います。

要件例

例えば、Pairsのようなマッチングサービスを新たに開発することになったとします。
マッチングサービスにはいいねという機能があり、いいねを送ると相手に通知がいき、相手がいいねを返すとマッチングが成立します。
なので、誰が誰にいいねを送ったかを保存しておく必要があります。
最初は、この保存場所をMysqlの user_like テーブルに保存していたとします。

いいねのアクションで、UserControllerがUserLikeServiceを呼び出し、UserLikeServiceがUserLikeRepositoryを呼び出して、いいね情報を取得するロジックを例に説明したいと思います。

アーキテクチャに関してはわかりやすいように、お馴染みのMVCにService層を組み込んだシステムを想定します。(MがRepository)

最初の実装(DIPに従わない例)

図1–1

type UserController struct {
service UserLikeService
}

func (c UserController) Get {
// 本来はリクエストから受け取る
userID := 1
c.service = NewUserLikeService()
userLike := c.service.Fetch(userID)
// 後続処理
}
type UserLikeService struct {}

func NewUserLikeService() UserLikeService {
return UserLikeService{}
}

func (s UserLikeService) Fetch(userID int) *UserLike {
repo := NewUserLikeRepository()
return repo.FetchByUserID(userID)
}

type UserLikeRepository struct {}

func NewUserLikeRepository() UserLikeRepository {
return UserLikeRepository{}
}

func (r UserLikeRepository) FetchByUserID(userID int) *UserLike {
db, err := sql.Open("mysql", "root:@/sample?parseTime=true")
if err != nil {
panic(err.Error())
}
defer db.Close()
// 以降、UserLikeに詰め替える処理
// サンプルなのでモックを返します
u := UserLike{1, 2}
return &u
}

type UserLike struct {
UserID int
PartnerID int
}

ControllerがServiceに、ServiceがRepositoryに依存しています。

どういう場合に困るのか?

前提として、UserControllerの他にも複数のControllerで、UserLikeServiceを使用していると仮定します。
例えば、実際あるかはわかりませんが、特定のリクエスト(UserController)の読み込みに関しては、他のスキーマ(例:DynamoDB)で持つ修正が必要になったとします。
そうするとService層またはRepository層への修正が必要になります。 雑に実装しますがこんな感じになるでしょうか。

func (s UserLikeService) Fetch(userID int, type string) *UserLike {
if (/* 受け取ったtypeで制御条件 */) {
repo := NewUserLikeRepository()
return repo.FetchByUserID(userID)
} else {
repo := NewUserLikeDynamoDBRepository()
return repo.FetchByUserID(userID)
}
}

この修正が入ると、引数が変更になり、UserLikeServiceを呼び出している箇所全ての修正が必要になってしまいます。

DIPに従った実装

図1–2

// controller package
type UserController struct {
service UserServiceInterface
}

type UserServiceInterface interface {
Fetch(int) *UserLike
}

func (c UserController) Get {
// 本来はリクエストから受け取る
userID := 1
// c.serviceにはControllerを呼び出す処理が実体を入れる
userLike := c.service.Fetch(userID)
// 後続処理

// service package
type UserLikeService struct {
repo UserLikeFetcherInterface
}

type UserLikeFetcherInterface interface {
FetchByUserID(int) *UserLike
}

func NewUserLikeService() UserLikeService {
return UserLikeService{
repo: NewUserLikeRepository(),
}
}

func NewUserLikeDynamoDBService() UserLikeService {
return UserLikeService{
repo: NewUserLikeDynamoDBRepository(),
}
}

func (s UserLikeService) Fetch(userID int) *UserLike {
return s.repo.FetchByUserID(userID)
}

// repository package
type UserLikeDynamoDBRepository struct{}

func NewUserLikeDynamoDBRepository() UserLikeDynamoDBRepository {
return UserLikeDynamoDBRepository{}
}

func (r UserLikeDynamoDBRepository) FetchByUserID(userID int) *UserLike {
u := UserLike{1, 2}
return &u
}

type UserLike struct {
UserID int
PartnerID int
}

上位レベルの各レイヤでは抽象インターフェースを宣言しています。
下位レベルのレイヤはこれらの抽象インターフェースを実装するようになったため、上位が下位に依存しなくなりました。
このインターフェースを満たすモジュールであればどんなものでも使用できるため、上位レイヤが使いたい下位レイヤのモジュールを選択できます。

具体的には、UserContorller側でNewUserLikeDynamoDBServiceを呼び出すようにしておけば、Service層またはRepository層に手を加えずに要件を満たすことが可能になりました。

まとめ

DIPは抽象に依存することで、オブジェクトの再利用、保守、柔軟性のメリットが受けられます
前回のオープン・クローズドの原則もそうでしたが、Webサービスの運用は変更が生まれやすいので、場合によってはDIPを使用して変更に耐えられる設計にしておくことが重要だと思います。
逆にこの原則はほとんど変更が加わらないオブジェクトでは使う必要は無いかと思います