【Android】interfaceと実装を別モジュールに分離する

こんにちは！tkhsktです。
みなさん、Android書いてますか？Android、楽しいですよねぇ。

この記事では、オブジェクトのinterfaceと実装を別モジュールに配置した際の依存解決について、いくつかの方法を紹介したいと思います。

## 実現したいこと

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155135/10df87f3-2b75-b4be-764c-2c84bace7278.jpeg" width="100%" />

この記事では、図のようなモジュールの依存関係を目指します。

### なぜやるのか

interfaceと実装を別モジュールに分離し、実装の詳細へのアクセスを制限することで、アプリケーション固有の処理やI/O処理などをドメインレイヤーから隔離できます。これにより、ドメインモデルの理解が困難なったり複雑性が増加することを抑制できます。

Androidのプロジェクトでは複雑なドメインロジックを記述することはあまりないと思いますが、このような制約を設けることで多人数での開発でもある程度の秩序を保つことができます。

## 実現方法

Repositoryを例としてinterfaceと実装を別モジュールに分離する方法を紹介します。

この記事では、便宜上インターフェースを配置するモジュールを`:interface`とし、実装を配置するモジュールを`:impl`として記載しますが、実際のプロジェクトでは`:data`や`:infra`など適切な名前にするのが良いと思います。

各方法の説明中に特別に記載がない限りは、Repository, RepositoryImpl, ViewModel, Module(Hiltのモジュール)の実装は下記のようになります。

Repositoryはinterfaceモジュールに配置します。

```kotlin
// :interface
interface SampleRepository {
   fun get(): Sample
}

class Sample
```

Repositoryの実装はimplモジュールに配置します。

```kotlin
// :impl
class SampleRepositoryImpl @Inject constructor(
    private val source: DataSource
) : SampleRepository {

    override suspend fun get(): Sample {
        // do something
        return sample
    }
}

class DataSource @Inject constructor() {
   ...
}
```

ViewModelはuiモジュールに配置します。

```kotlin
@HiltViewModel
class SampleViewModel @Inject constructor(
    private val repository: SampleRepository,
) : ViewModel() {

    ...
}
```

DIのモジュールは方法によって配置する場所が異なります。

```kotlin
@Module
@InstallIn(SingletonComponent::class)
internal abstract class SampleModule {

    @Binds
    abstract fun sampleRepository(impl: SampleRepositoryImpl): SampleRepository
}
```

### 方法1: appモジュールでDIする

1つ目の方法はappモジュールでDIする方法です。この方法ではappモジュールでDIの設定を行い、uiモジュールなどにInjectします。

モジュールの依存関係は下図のようになります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155135/a372ee7d-69bf-6758-96b0-34f412580e4b.jpeg" width="80%" />

#### appモジュール

```app/build.gradle
dependencies {
    ...
    
    implementation(project(":interface"))
    implementation(project(":impl"))
    implementation(project(":ui"))
}
```

DIのモジュールはappモジュールに配置します。

#### uiモジュール

```ui/build.gradle
dependencies {
    ...

    // implモジュールへの依存は不要
    implementation(project(":interface"))
}
```

- メリット
  - 比較的容易に実装可能
- デメリット
  - appモジュールからimplモジュールのオブジェクトを直接使用できてしまう

### 方法2: implモジュールでDIする

2つ目の方法はimplモジュールでDIする方法です。この方法では、implモジュールにあるオブジェクトの可視性をinternalにできるため、その他の方法よりも本来の目的に沿った構造になります。

依存関係はappモジュールでDIする場合と同様です。

#### appモジュール

```app/build.gradle
dependencies {
    ...
    
    implementation(project(":interface"))
    implementation(project(":impl")) // DIのために依存を追加する必要がある
    implementation(project(":ui"))
}
```

#### implモジュール

```kotlin
// internalにできる
internal class SampleRepositoryImpl @Inject constructor(
    private val source: DataSource
) : SampleRepository {

    override suspend fun get(): Sample {
        return Sample(source.get())
    }
}
```

DIのモジュールはimplモジュールに配置します。

- メリット
  - implモジュールのオブジェクトの可視性をinternalにできるので、外部のモジュールからのアクセスを強く制限できる
    - ただしuiモジュールやappモジュールで統合テストを書きたい時にimplモジュールのオブジェクトにアクセスできないので困る
- デメリット
  - DIのためだけにappモジュールに依存を追加することになる 

### 方法3: DI用のモジュール作成する

3つ目の方法はDI用のモジュールでDIする方法です。この方法はappモジュールでDIする場合のデメリットである、appモジュールからimplモジュールのオブジェクトにアクセスできてしまう問題を解決できます。

![ジョージメモ - Frame 6.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155135/82f0fa43-3584-a753-1926-769830caea99.jpeg)

#### appモジュール

```app/build.gradle
dependencies {
    ...
    
    implementation(project(":di")) // DIのために依存を追加する必要がある
    implementation(project(":ui"))
}
```

#### diモジュール

```di/build.gradle
dependencies {
    ...

    implementation(project(":interface"))
    implementation(project(":impl"))
}
```

```kotlin
@Module
@InstallIn(SingletonComponent::class)
internal abstract class SampleModule {

    @Binds
    abstract fun sampleRepository(impl: SampleRepositoryImpl): SampleRepository
}
```

DIのモジュールはdiモジュールに配置します。

- メリット
  - implモジュールを直接依存追加しない限りは他のモジュールからimplモジュールのオブジェクトにアクセスできない
- デメリット
  - DI専用のモジュールを作る必要があるので、モジュール構成が複雑になる

### オプション: runtimeOnly

ここまで紹介した方法では、モジュールの依存追加に`implementation`を使用していましたが、依存追加には`runtimeOnly`を使用する方法もあります。これを使うことでインクリメンタルビルドの高速化が期待できます。

ただし、`runtimeOnly`で依存追加したモジュールのオブジェクトにはコード上ではアクセスできないため注意が必要です。

例えばimplモジュールでDIする場合、appモジュールの`build.gradle`で下記のようにimplの依存を追加することで、appモジュールからimplモジュールのオブジェクトをへのアクセスを防ぐことができ、インクリメンタルビルドの高速化が期待できます。

```app/build.gradle
dependencies {
    ...

    implementation(project(":interface"))
    runtimeOnly(project(":impl"))
    implementation(project(":ui"))
}
```