要点
- クリーンアーキテクチャが要求しているのは Dependency Rule(依存の方向)であり、層の境界に interface を設けることではない。
- interface が目的化して、かえって開発効率を下げていないだろうか。
よく見かける構造
例えば Android のプロジェクトで、クリーンアーキテクチャに倣った以下のような構造をよく見かける。
|
|
UserApiClient → UserRemoteDataSource(interface + Impl)→ UserRepository(interface + Impl)。各層の境界に interface と Impl のペアが置かれている。
しかし、UserRemoteDataSourceImpl の中身は UserApiClient の呼び出しをラップしているだけであり、UserRepositoryImpl も UserRemoteDataSource をラップしているだけ、ということが少なくない。
|
|
この 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 クライアントを呼び出す構成を検討してもよいのではないか。
|
|
ViewModel は Repository を直接参照する。
|
|
将来テストで interface が必要になったら、その時点で UserRepository から interface を抽出する、という考え方もあるのではないだろうか。
まとめ
- クリーンアーキテクチャの本質は Dependency Rule であり、interface の有無ではない。
- 層の境界に interface を設けることが目的化していないか、一度立ち止まって考えてみたい。
- ひとつのルールだけを守るのではなく、他の設計原則や開発効率も含めて総合的に検討したい。