1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

依存性逆転の原則って、結局何が逆転してるの?

Last updated at Posted at 2022-11-30

bitFlyer Advent Calendar 2022の2日目です。
こんにちは、私はフロントエンド開発部でAndroidエンジニアをしています。

タイトルは、依存性逆転の原則に対する私のファーストインプレッションです。

依存性逆転の原則とはSOLID原則の一つです。( Dependency Inversion Principle )
これを調べると大抵 具体に依存せずにインターフェース(抽象)に依存すべきという原則 という説明がなされています。
合っているとは思うのですが、これだけだと「抽象に依存すべきというのはわかったけど、結局何が逆転したの??」という疑問が残りますよね。
ここをわかりやすく解説するのが本記事の目的です。

目次

1.素直に実装してみる
2.interfaceを使って抽象化してみる
3.結局どこが逆転していたのか
4.おわりに

1. 素直に実装してみる

カウンターアプリを例に考えていきます。
プラスボタンがあって、押すたびにカウントアップされていくだけのアプリだと思ってください。(Flutterを書いてる人は確実に見覚えがあるアレです)

カウントアップしていくだけなのですごくシンプルとはいえ、考えることはあります。

  1. インクリメントした数字をどう保存するか
  2. 数字をインクリメントするロジックはどこに書くか

などなど。
まず、1. から。
そのカウントされた数字はどうしましょう?
リモートDBに保存しますか?
それともローカルDBで良いでしょうか。
決まらないので、とりあえずin-memoryでRepositoryに保持しておくことにします。

InMemoryCounterRepository
class InMemoryCounterRepository {
    private val _counter = MutableStateFlow(0)

    fun saveCount(count: Int) {
        _counter.update {
            count
        }
    }
}

カウントアップするUseCaseも定義しちゃいましょう

IncrementCountUseCase
class IncrementCountUseCase(
    private val repository: InMemoryCounterRepository,
) : AbstractUseCase<Int, Unit>() {
    override fun execute(params: Int) {
        repository.saveCount(params + 1)
    }
}

RepositoryとUseCaseを定義しました。

これらをクラス図にすると

image.png

UseCaseRepositoryに依存しています。

とても素直です。

ところが、です。
「誰でもそのカウンターの値を見られるようにしたいから、数字をリモートDBにも送って欲しい!!」という要求が突如として湧いてきたとします。(なんだそりゃ!?、という気持ちは抑えてください)
困りましたね・・・

2. interfaceを使って抽象化してみる

こんなときスマートにRepositoryを置き換える方法があります。
依存性逆転の原則を利用します。

interfaceを用います。

CounterRepository
interface CounterRepository {
    fun saveCount(count: Int)
}

上記のinterfaceを、先ほどのInMemoryCounterRepositoryに継承させて

InMemoryCounterRepositoryImpl
class InMemoryCounterRepositoryImpl : CounterRepository {
    private val _counter = MutableStateFlow(0)

    override fun saveCount(count: Int) {
        _counter.update {
            count
        }
    }
}

上記の実装クラスを作成します。

そしてIncrementCountUseCaseのコンストラクタのRepositoryの型をCounterRepositoryに修正すると、クラス図は下記のように修正されます。

image.png

すごい!!!
よく見るやつです!

Repositoryの置き換えをスマートにやれますね。自作でMockクラスを作って返す値をコントロールしたりもしやすいのでとても便利です。
下記のような実装クラスをInMemoryCounterRepositoryの代わりにインジェクトすれば「カウントアップした数字をリモートDBに保持したい!」いう新たな要件を満たせそうです。

APIRepositoryImpl
class APIRepositoryImpl @Inject constructor(private val client: APIClient) : CounterRepository {
    override fun saveCount(count: Int) {
        client.updateCount(count)
    }
}

無事に依存性が逆転しました!!!!

めでたしめでたし

・・・・とは私は思えませんでした。

UseCaseは引き続きRepositoryに依存してるように見えるけど、結局どこが逆転してるの??」 というのが率直な感想でした。

3. 結局どこが逆転していたのか

もう少しマクロな視点で見たらわかってきます。

1. interfaceを使う前のクラス図

RepositoryUseCaseのレイヤーを分けてみましょう。(レイヤーを分ける、というのはここではpackageを分けるくらいのイメージで大丈夫です)

スクリーンショット 2022-11-18 20.03.27.png

結論として、UseCaseはDomain層に、RepositoryはData層に分けます。(UseCaseにはロジックがあり、RepositoryはDataを扱うためです)
冒頭であった、 2. インクリメントのロジックをどこに置くか という話ですが、UseCaseに書いておくととても良いと思います。
なぜなら、Repository側にロジックを漏らすと、Repositoryを差し替えたときにそのロジックを共通で使えません。※1
上記の図の矢印を見ると、依存関係は Domain -> Data となっていますね。
実際にソースコードのimportの向きもこうなっています。

(※1 若干思想的な話になるのですが、よく聞かれる「UseCaseはRepositoryのメソッドを呼び出すだけで空っぽだから不要」という思想は一理あると思います。しかし私個人としては前述の理由でUseCaseはあった方が良いと考えています。)

2. interfaceを使った後のクラス図

次に、interfaceを使った場合のクラス図のレイヤーを分けてみましょう。

スクリーンショット 2022-11-18 20.07.49.png

境界線を跨ぐ矢印の方向を見てください!!

さっきとは逆を向いていますね。

Data -> Domain依存の方向が逆転 しています!!
ここにきて初めて何が逆転しているのか理解しました。
境界を越える矢印の向きが逆転していたんだ、と。
実際に図の通りにdomainとdataでディレクトリを分けると、この矢印の向きはimportの向きとも一致します。
(※実装クラスをDomain層でインスタンス化するとDomainのパッケージがDataのパッケージに依存してしまいimportの向きが狂ってしまうので、DIコンテナを使うことでうまく避けられます)

Domain層 -> Data層だった依存の向きをData層 -> Domain層と逆を向かせて何が嬉しいのか、というと

  • Domain層がData層に依存しないのでDomain層を安定させられる( 例えばData層のライブラリを変更したり、レスポンスのプロパティが変更したときに都度Domain層に変更を入れずに済む )
  • Domainを切り出してマルチモジュール化すると、依存性を強制させられる( 循環依存したりレイヤーの崩壊を避けやすい )

などなど
まとめると、変更に強く秩序あるコードにできる、という感じです。

胸のつっかえがとれましたね:joy:(とれていない方はガンガンコメントください!)

4. おわりに

「一体何が逆転しているんだ???」と胸につっかえを抱えている人が少しでもスッキリすれば良いなという願いがあり当記事を執筆しました。
実際にレイヤーを分けてマルチモジュール化したサンプルコードを置いておきます。
ぜひ参考にしてください!

また、弊社bitFlyerではAndroidエンジニアを募集しています。ご興味のある方はぜひご応募ください!

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?