Featured image of post その interface、本当に必要ですか? — クリーンアーキテクチャの Dependency Rule から考える

その interface、本当に必要ですか? — クリーンアーキテクチャの Dependency Rule から考える

クリーンアーキテクチャが要求しているのは Dependency Rule であり、層の境界に interface を設けることではない

要点

  • クリーンアーキテクチャが要求しているのは Dependency Rule(依存の方向)であり、層の境界に interface を設けることではない。
  • interface が目的化して、かえって開発効率を下げていないだろうか。

よく見かける構造

例えば Android のプロジェクトで、クリーンアーキテクチャに倣った以下のような構造をよく見かける。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
data/
  api/
    UserApiClient.kt          // Retrofit2 の API 定義
  datasource/
    UserRemoteDataSource.kt   // interface
    UserRemoteDataSourceImpl.kt
  repository/
    UserRepositoryImpl.kt
domain/
  repository/
    UserRepository.kt         // interface

UserApiClientUserRemoteDataSource(interface + Impl)→ UserRepository(interface + Impl)。各層の境界に interface と Impl のペアが置かれている。

しかし、UserRemoteDataSourceImpl の中身は UserApiClient の呼び出しをラップしているだけであり、UserRepositoryImplUserRemoteDataSource をラップしているだけ、ということが少なくない。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// UserRemoteDataSource の実装が ApiClient をラップしているだけ
class UserRemoteDataSourceImpl(
    private val api: UserApiClient,
) : UserRemoteDataSource {
    override suspend fun getUser(id: String): User = api.getUser(id)
}

// UserRepository の実装が DataSource をラップしているだけ
class UserRepositoryImpl(
    private val dataSource: UserRemoteDataSource,
) : UserRepository {
    override suspend fun getUser(id: String): User = dataSource.getUser(id)
}

この interface は本当に必要だろうか。

クリーンアーキテクチャが要求しているもの

クリーンアーキテクチャの本質は Dependency Rule である。

ソースコードの依存関係は、内側に向かってのみ向いていなければならない。

外側の層は内側を知ってよい。内側の層は外側を知ってはならない。これがクリーンアーキテクチャの全てであり、「層の境界に interface を設けること」は要件に含まれていない。

「安定する方向に依存する」との関係

「安定する方向に依存する」という表現は、Martin が提唱する Stable Dependencies Principle(SDP) の言葉であり、クリーンアーキテクチャの Dependency Rule とは別文脈で提唱されたものである。

ただし、両者の指す方向は一致する。内側の層は外部変化の影響を受けないため変わりにくく、安定している。したがって「内側に依存する = 安定するものに依存する」は帰結として一致する。

「安定する方向に依存する」という理解は実践上正しいが、それは Dependency Rule の帰結であり、Dependency Rule そのものではない。

interface は Dependency Rule の手段である

Uncle Bob は “Crossing boundaries” セクションで、制御フローが内側から外側に向かう場合に Dependency Inversion Principle を用いて interface を配置する手法を示している。

つまり、interface は Dependency Rule を守るための 手段(technique) であり、Dependency Rule そのものではない。

さらに、Google の公式アーキテクチャガイドは Domain 層を オプション と明示している 1

The domain layer is an optional layer that sits between the UI layer and the data layer.

You should only use it when needed — for example, to handle complexity or favor reusability.

UI 層と Data 層の 2 層であっても、Dependency Rule を守っていればクリーンアーキテクチャの原則には適合している。

interface を設ける目的を問い直す

interface を層の境界に設ける目的として、よく挙げられる理由を一度立ち止まって検討してみたい。

「テスタブルにするため」

テストのために interface が必要になる場面はある。しかし、最初から全ての層に interface を設ける必要は本当にあるだろうか。必要になった時点で interface を抽出するのでは遅いのだろうか。

「あとから差し替えやすくするため」

「Retrofit を別のライブラリに差し替えるかもしれない」「ローカル DB とリモート API を切り替えるかもしれない」— こうした理由で interface を設けることがある。

しかし、実際にそのような差し替えがどの程度発生するだろうか。HTTP クライアントを差し替える機会は、プロジェクトの生涯でほとんどない。発生するかどうか分からない差し替えのために、全ての層に interface を設ける必要があるだろうか。

「大規模チームで並行開発するため」

interface を先に決めておき、上位レイヤーをエンジニア A が、下位レイヤーをエンジニア B が担当する — このケースでは interface が開発効率を上げる。

しかし、モバイル開発のチームは小規模であることが多い。このような並行開発の分業が行われるケースは、少なくとも筆者の経験ではほとんど見かけない。

マルチモジュールとの相性

Repository interface は、マルチモジュール構成と相性が悪い。

よく見かけるクリーンアーキテクチャ構成では、Repository interface は domain 層に、RepositoryImpl は data 層に配置される。しかし、これをマルチモジュールで実現しようとすると、モジュール間の依存関係の解決が面倒になる。

Repository interface 用のモジュールを別途設け、RepositoryImpl は data モジュールに収める — これは技術的には成立する。しかし、domain 層の持ち物だったはずの Repository interface だけが別モジュールに分離されてしまう。これは当初やりたかったこと — Repository interface を domain 層に配置すること — と乖離していないだろうか。

凝集度 の観点からも検討の余地がある。関連する機能がまとまっていること(高凝集・機能的凝集)が良い設計とされるが、Repository interface と Impl が異なるモジュールに散らばっている状態を、高凝集と言えるだろうか。

一つの提案:interface を設けない構成

RemoteDataSource が API クライアントをラップしているだけであれば、Repository が直接 API クライアントを呼び出す構成を検討してもよいのではないか。

1
2
3
4
5
6
7
class UserRepository(
    private val api: UserApiClient,
) {
    suspend fun getUser(id: String): Result<User> =
        runCatchingOrCancel { api.getUser(id) }
            .withErrorLog()
}

ViewModel は Repository を直接参照する。

1
2
3
4
5
6
@HiltViewModel
class UserViewModel @Inject constructor(
    private val repository: UserRepository,
) : ViewModel() {
    // ...
}

将来テストで interface が必要になったら、その時点で UserRepository から interface を抽出する、という考え方もあるのではないだろうか。

まとめ

  • クリーンアーキテクチャの本質は Dependency Rule であり、interface の有無ではない。
  • 層の境界に interface を設けることが目的化していないか、一度立ち止まって考えてみたい。
  • ひとつのルールだけを守るのではなく、他の設計原則や開発効率も含めて総合的に検討したい。
Hugo で構築されています。
テーマ StackJimmy によって設計されています。