Android Clean Architecture:ドメインレイヤーの理解と実践ガイド


概要

この記事では、Androidクリーンアーキテクチャにおけるドメインレイヤーの重要性について探求します。ビジネスロジックを明確にし、保守性やテスト容易性を向上させる方法を学ぶことで、開発者はより効率的なアプリケーション設計が可能になります。 要点のまとめ:

  • ドメインレイヤーはビジネスロジックの核であり、UIやデータソースから独立することで疎結合を実現します。これによって、大規模開発でもその価値が際立ちます。
  • ユースケース(インタラクター)はビジネスルールをカプセル化し、再利用性と可読性を高めます。私自身もこの手法を使うことで複雑な条件分岐がスッキリしました。
  • テスト駆動開発(TDD)を導入すると、独立したドメインレイヤーの品質が保証されます。モックオブジェクトを用いたテストで早期にバグを発見できることが多いです。
このように、本記事から得られる洞察は、Android開発者としての力強い武器となります。

ドメインレイヤーとは何か

アンドロイドアプリが複雑になるにつれて、無秩序なコードベースは密接な結合や重複したロジック、脆弱なユニットテストを引き起こし、開発を遅らせる原因となります。クリーンアーキテクチャの原則はこれらの課題に対処するために設計されており、関心事の明確な分離を促進し、アプリケーションをよりスケーラブルで保守可能かつテスト可能にします。このアーキテクチャの中心には**ドメインレイヤー**があり、この層はビジネスロジックをカプセル化し、UIとデータ処理との間に明確な境界を設定する重要な役割を果たしています。本記事ではドメインレイヤーについて詳しく掘り下げ、その目的やベストプラクティス、Androidアプリケーションへの効果的な実装方法について説明します。

ドメインレイヤーの主要な責任

ドメインレイヤーとは、アプリケーションのコアビジネスロジックを扱うための層です。このレイヤーは、ユーザーインターフェースやユーザーとの相互作用を管理するプレゼンテーションレイヤー、データベースやREST APIなどからデータの保存と取得を行うデータレイヤーとは独立しています。プレゼンテーションレイヤーとデータレイヤーはドメインレイヤーに依存していますが、ドメインレイヤーはそれらから完全に切り離されています。この構造によって、UIフレームワークやデータソースの変更があってもビジネスロジックには影響を与えません。

ドメインレイヤーには主に以下の要素が含まれています:
- **ユースケース(インタラクター)**:特定のビジネスロジックを処理し、アプリ内でデータがどのように処理され使用されるべきかを定義します。
視点の拡張比較:
ドメインレイヤーの要素責任ベストプラクティス利点
ユースケース(インタラクター)特定のビジネスロジックを処理し、データ使用方法を定義単一責任原則に従うことが重要再利用性とテスト容易性の向上
ドメインモデルビジネスエンティティを表現する明確なモデル設計を行うべきコアロジックの管理と簡潔化
リポジトリインターフェースデータアクセス方法を定義し、内部構造から切り離す役割抽象に依存する設計が求められるビジネスロジック独立性の確保
バリデーション機能ユーザー入力やビジネスルールの検証を実施する役割シンプルで集中したバリデーションロジックを維持するべきです。 一貫性と信頼性向上

ユースケースの構成要素


- **ドメインモデル**:ビジネスドメイン内のエンティティを表します。
- **リポジトリインターフェース**:データへのアクセス方法を定義し、データソース(例えば、データベースやREST API)に依存せずに内部構造からの切り離しを実現します。

## ドメインレイヤーの主な責任
✅ **ビジネスロジックのカプセル化とモジュール性の促進**
ドメインレイヤーは、ビジネスロジックを中心にまとめており、それによってプレゼンテーションレイヤーやデータレイヤーから独立させています。この層ではユースケースが定義されており、注文を行うことやユーザー入力を検証するなどの特定のビジネスワークフローを示しています。また、この層でロジックを分離することで、Kotlin Multiplatform(KMP)で共有できるため、iOSなど他のプラットフォームとのロジック重複が軽減されます。

ユースケースの命名規則

✅ **ビジネスロジックとデータソースを切り離す** ドメインレイヤーは、具体的な実装ではなく抽象に依存することで、ビジネスロジックがデータソースから独立することを確保します。これにより、REST APIからGraphQLへの変更やデータベースプロバイダーの変更など、データの取得や保存方法を柔軟に変えることができても、コアのビジネスロジックには影響しません。 ✅ **テスト容易性の向上** ビジネスロジックを小さく単責任のクラスに分けることで、ドメインレイヤーはテストしやすさを高めます。ユースケースは抽象(例えばリポジトリインターフェイス)に依存しているため、依存関係をモックして孤立した状態でユニットテストを書くことが簡単になります。この設計はテスト駆動開発(TDD)をサポートし、高いテストカバレッジを保証します。 ✅ **プレゼンテーション層の複雑さ軽減** ビジネスロジックと変換処理を担当することで、ドメインレイヤーはViewModelがUI状態の管理だけに集中できるようにします。ビューからユースケースへ論理を移行することで、それらの再利用も可能になり、アプリケーション全体の保守性が向上します。その結果、プレゼンテーション層もより簡単にテストできます。 ❌ **データ保存や取得について責任を持たない** ドメインレイヤーはデータの永続化や取得には関与せず、その役割はデータレイヤーに属しています。このレイヤーにはリポジトリ、データソース、REST API、およびデータベースなどのコンポーネントがあります。したがってドメインレイヤー自体はどのようにデータが取得または保存されるかについて無関心です。


ユースケースの命名規則 Free Images


ビジネスロジックの定義と依存関係

❌ **UIの状態管理には責任を持たない**ドメイン層はUIの状態を管理しません。これはプレゼンテーション層(ViewModel)が担当します。代わりに、ドメイン層はビジネスロジックを実行し、不変の結果を返します。---## **ユースケースの構造**ユースケースとは、単一のビジネス操作をカプセル化するクラスであり、再利用可能でテストが容易です。よく構築されたユースケースは、1つの公共関数のみを持ち、単一責任に焦点を当てるべきです。もしユースケースが複数の公共関数を持っている場合、それは多くの懸念事項を扱っている可能性があり、「単一責任原則(SRP)」に違反しています。そのような場合は、ロジックを別々のより集中したユースケースに分割することを検討してください。### ユースケース名付け規則ユースケース名は次の形式に従うべきです:> _**[動詞]**_ + _[_名詞_]_ + _UseCase_例:✅ _**`Get**CustomerBag_UseCase`✅ _**`Logout**User_UseCase`✅ C**acheO**_rderDetailsU_seCase### ビジネスロジックの定義ビジネスロジックは、アプリが異なる状況でどのように振る舞うかについて制御するルールや決定となります。それによって以下が決まります:- **データ処理方法 -** 表示前に変換処理を適用します。- **アクション実行タイミングと方法 -** 保存前にユーザー入力を検証します。- **アプリ内で異なる部分がどのように相互作用するか -** 責任範囲を明確に保ちます。### ユースケース依存関係ユースケースは抽象化と純粋なKotlin構造のみ依存すべきであり、プラットフォーム独立性が保たれるべきです。以下は何を含めるべきか避けるべきかについてのガイダンスです:✅ **他のユースケース -** 共有ビジネスロジック用(あまり使用しないことで過度なチェーン呼び出しを避けます)。✅ **純粋Kotlinユーティリティクラス -** 検証ルールや数字フォーマットなど再利用可能なロジック用です。✅ **リポジトリインターフェイス -** 具体的な実装に依存せずデータ層と対話します。❌ **Android依存関係 -** `Context`や`LiveData`、`Application`などは避けましょう。「LiveData」の代わりにはKotlin の `Flow` や `suspend` 関数 を使用してドメイン層との結合度が低い状態になります。### ユースケースによるデータ提供方法ユースケースはいくつか異なるメカニズムによってプレゼンテーション層へデータ提供します。それぞれ操作実行方法によります:- **通常関数呼び出し -** 同期操作向け(例:メモリ内チェック・簡易計算)。- **サスペンド関数 -** 非同期一次操作向け(例:ネットワークリクエスト・データベースクエリ)。- **フロー -** 時間経過による更新発信(例:データベース変更観察)。- **オブザーバブル/コールバック -** RxJavaベースアーキテクチャ向け。この選択肢では、その操作が即時軽量なのか非同期長時間運用なのかどうかによります。---## ユースケースで留意すべき最良慣行### 1. invoke演算子利用についてユースケースには単一責任がありますので、Kotlin の `invoke` 演算子 を使うことで関数として呼び出せます。同じメソッド呼び出しを書かななくて済むため直感的になります
GetCustomerBagUseCase @Inject constructor( private val userRepository: UserRepository, // interface private val bagRepository: BagRepository // interface) { suspend operator fun invoke(): Bag { val userId = userRepository.getUserId() return bagRepository.getCustomerBag(userId) }}
これによってViewModel内で呼び出す際も簡潔になり直感的になります
customerBag = getCustomerBagUseCase()
### 2. スレッド安全性保持についてユースケースは「メインスレッド安全」である必要があります。この文脈ではUIブロッキングなしでメインスレッドから呼ばれることが求められます。またREST APIコールやデータベースクエリ等長時間運用処理についてもデータ層へ委譲されます。そしてドメイン層ではI/Oバウンドスレッドへの切替えは禁止されており、その根元近くから背景スレッドへの切替えも行います。ただしCPU集中的作業—例えば並び替えや変換処理—の場合にはこの中でも `Dispatchers.Default` を使います。我々の商品群から取得した製品群へ加工・整形日付表示・割引計算等必要になる事態想像できますね。この場合次ようになります.- 【製品取得】→ データ層ハンドリング(例:REST APIまたはDBからリポート取得)。.- 【並び替え & 整形】→ ドメイン側ビジネスロジックこの商品の加工というCPU集約型作業もバックグラウンド環境下執行、一方UIフリーズ及びANR (応答不可アプリケーション) エラー防止にも役立ちます ✅ 最良慣行 : 重い計算処理だけでもバックグラウンド環境移転
FormatProductListUseCase @Inject constructor( private val productRepository: ProductRepository, // interface private val formatDateUseCase: FormatDateUseCase, private val calculateDiscountUseCase: CalculateDiscountUseCase, @CoroutineDefault private val defaultDispatcher: CoroutineDispatcher) { suspend operator fun invoke(): List<Product> = withContext(defaultDispatcher) { productRepository.getProducts() .sortedByDescending { it.price } .map { product -> val formattedDate = formatDateUseCase(product.createdAt) val discount = calculateDiscountUseCase(product.price)  product.copy( createdAt = formattedDate, discount = discount ) }}data class Product( val id: String, val name: String, val price: Double, val createdAt: String, val discount: Double = 0.0)} 
### この仕組み上手く機能する理由 ✅ データ取得仕事委譲先として機能させています 商品群獲得その内容自体今後考える方向性も明確ですが ドメイン側との一致点維持につながっています ✅ CPU集約型タスク向け利用者とも言える 「Dispatchers.Default」 使用細かな整列及び変換プロセス背景環境下進めればパフォーマンス高まりますので UIフリーズ防ぐ助力にも繋げられます ✅ テスト可能性増加目的為「CoroutineDispatcher」を注入しました テスト中ディスポッチャー置换容易さ及時制御条件下動作展開させそれ以外影響受験所落ち着いた予測できる結果得られ様々試験条件下独立出来ました ✅ ビジュアルロギング論題モジュラー化保持 クリーンコード設計目指しましょう 各日付整形割引値調整同様独自少ない及可視化改善としてチーム全体貢献できます 。### 3. 複数リポジトリからデータ統合複雑して見えてしまうため ViewModel また Repository 内部整理難しく感じられるものもあります。しかしながら より効果的なのはこちら方針採取して新設された ユーザービジョン 統合情報収集専門 リポート設定こそ望ましいという流れでしょう! ✅ 最良慣行 : フロー経由商材統合&バッグ件数 簡潔提示```kotlinclass GetProductsWithBagCountUseCase @Inject constructor( private val productsRepository: ProductsRepository, // interface private val bagRepository: BagRepository, // interface private val userRepository: UserRepository // interface) { operator fun invoke(): Flow
 { var userId=userReposiotory.getUserId() return combine(productsRepositroy.getUserProducts(userId),bagRepositroy.getUserBagCount(userId)){products ,bagCount-> ProductsWithBagCount(products ,bagCount)}} data class ProductsWithBagCount(val products :List,val bagCount:Int)` `` ### なぜこれがお勧めなの? ✅ `Flow.combine()` 利活用 自然派生情報気づけばどちらとも更新通知随時確認出来れば最新値伝播されてゆく仕組み頼んだあなた自身までできます! ✅ ブロッキング主張無効 Data Fetching 非同期型フロー通じ進展迅速効果的遂げています! ✅ ViewModelシンプル具現 化された唯一真実源収拾出来欠乏考慮要素各種倉庫取り扱い負担減少されていますお陰様ですね!

ユースケースにおけるベストプラクティス

✅ **リポジトリは単一責任を持つ** リポジトリ同士の依存関係を避け、明確な責務の分離を保つことで、テストやメンテナンスが簡素化されます。 ✅ **テスト可能性の向上** ユースケースは単体テストでモックしやすく、ViewModel内で複数の `StateFlow` や `Flow` インスタンスを直接扱う必要がなくなります。### 4. データバリデーションデータバリデーションは、アプリケーション内で流れるデータの整合性と正確さを保証するために重要です。 バリデーションロジックを **ドメイン層** 内で処理することによって、次のような利点が得られます: ✅ **一貫性** 処理または表示される前にデータが検証されることを保証します。 ✅ **再利用性** アプリ内の異なる部分で冗長なバリデーションロジックを避けることができます。 ✅ **関心の分離** バリデーションをプレゼンテーション層やデータ層から独立させておきます。

データ検証をドメインレイヤーで行う理由

ユーザーからの入力を処理または保存する前に、特定の条件を満たしているかどうかを確認することが重要です。例えば、メールアドレスが正しい形式(例:`[email protected]`)であるかどうかや、パスワードが最低限の長さと複雑さの要件を満たしているかを検証します。

ビジネスルールの検証は、データが特定の業界固有の規則に従っていることを確保します。フロントエンドでこのような検証を行うことで、行動が実行される前にビジネスルールをチェックできるため、ユーザー体験も向上します。例えば、注文を出す前に十分な資金があるかどうか確認することで、不必要なREST API呼び出しを避けたり、購入手続きに進む前に商品在庫があることを確認したりできます。このチェックが失敗した場合、「購入」ボタンを無効化することも可能です。

フォーマット検証は、データが事前定義された構造やパターンに従っていることを確保します。例えば、日付文字列が`YYYY-MM-DD`という形式になっているかどうかなどです。このような検証は、一貫性と信頼性の高いデータ管理につながります。

ユースケースの管理とライフサイクル

ユーザー登録を行う前に、メールアドレスとパスワードを検証する必要がある場合の例を考えてみましょう。以下は、そのためのKotlinコードです。

class ValidateUserRegistrationUseCase @Inject constructor(
private val emailValidator: EmailValidator,
private val passwordValidator: PasswordValidator
) {
suspend operator fun invoke(
email: String,
password: String
): ValidationResult {
return when {
!emailValidator.isValid(email) -> ValidationResult.InvalidEmail
!passwordValidator.isValid(password) -> ValidationResult.InvalidPassword
else -> ValidationResult.Valid
}
}
}

sealed interface ValidationResult {
data object Valid : ValidationResult
data object InvalidEmail : ValidationResult
data object InvalidPassword : ValidationResult
}


このコードでは、`ValidateUserRegistrationUseCase` クラスを通じて、ユーザーが入力したメールアドレスとパスワードの妥当性を確認しています。各バリデータ(`emailValidator` と `passwordValidator`)がそれぞれの条件に対してチェックし、不正な場合には適切な結果を返します。このようにして、アプリケーションの状態に応じた柔軟なユースケース設計が可能になります。

データアクセスを制限する利点と欠点

### バリデーションの概要✅ **シンプルで集中したバリデーションロジックを維持** 各バリデーターは、特定のタスクに焦点を当てるべきです(例:メール形式やパスワードの長さの検証など)。 ✅ **エラーハンドリング** バリデーション状態(有効/無効)を表すために、sealedクラスまたはsealedインターフェースを使用すると、エラーハンドリングがシンプルで読みやすくなります。 ✅ **バリデーションロジックの重複回避** アプリ内の異なる部分でコードが重複しないように、バリデーションロジックは再利用可能に保ちましょう。 ✅ **独立してバリデーションロジックをテストする** バリデーションロジックはビジネスルールの一部なので、ユニットテストではさまざまなシナリオ(例:無効なメール形式や短いパスワードなど)でその正確さを確認することに重点を置くべきです。 ❌ **エラーメッセージのハードコーディング** エラーメッセージをハードコーディングすることは避けるべきです。代わりに、一貫性を保つためにエラー状態を定義しましょう。---## ユースケースの管理: インジェクション、ライフサイクル、および実行 ViewModelベースのアーキテクチャでは、ユースケースがViewModelに注入され、必要に応じて実行されます。Hiltは唯一の実装しかない場合、自動的にユースケースを提供できます。このユースケースがViewModelと同じライフサイクルになるようにするには、**`@ViewModelScoped`** アノテーションを付けます。これによって、そのユースケースインスタンスはViewModelが初期化されたときに一度作成され、その後も同じものとして保持され、不必要な再生成が回避されます。
@ViewModelScoped class GetCustomerBagUseCase @Inject constructor() {}

パッケージ構造の例

いくつかのユースケースは、アプリのライフサイクルにおいてシングルトンスコープであることが有益です。これは、認証などの持続的なビジネスロジックに役立ちます。また、ユースケースに複数の実装がある場合やインターフェースベースの抽象化を行っている場合は、`@Module`内で実装をバインドし、それを`ViewModelComponent`にインストールします。

@Module
@InstallIn(ViewModelComponent::class)
abstract class BagModule {
@Binds
@ViewModelScoped
abstract fun bindGetCustomerBagUseCase(
impl: GetCustomerBagUseCaseImpl
): GetCustomerBagUseCase
}


### 注入メソッド:標準 vs レイジー

ユースケースを注入する方法には2つの主な方法があります。
- **標準注入**:頻繁に使用されるユースケース向け。
- **レイジー注入**:条件付きで必要となるユースケース向け。

### 1. 標準注入(頻繁に使用されるユースケース向け)

頻繁に使用されるユースケースについては、コンストラクターインジェクションを使って直接ViewModelに注入できます。この場合、そのインスタンスはViewModelが初期化されたときに一度作成され、その後ViewModelのライフサイクル全体でアクティブなままとなります。

@HiltViewModel
class BagViewModel @Inject constructor(
private val getCustomerBagUseCase: GetCustomerBagUseCase) : ViewModel() {

private val _customerBag = MutableStateFlow<CustomerBag?>(null)
val customerBag: StateFlow<CustomerBag?> = _customerBag.asStateFlow()

fun getCustomerBag() {
viewModelScope.launch {
_customerBag.update { getCustomerBagUseCase() }
}
}
}


### 2. レイジー注入(特定の場合のみ使用するユースケース向け)

特定のシナリオでのみ使用するユースケースについては、レイジーインジェクションによって必要になるまでインスタンス化を遅延させます。これによって不要なオブジェクト生成が減り、依存関係が未使用の場合でも回避できます。

import dagger.Lazy

@HiltViewModel
class BagViewModel @Inject constructor(
private val saveItemToWishListUseCase: Lazy<SaveItemToWishListUseCase>) : ViewModel() {

fun saveItemToWishList(item: Item) {
viewModelScope.launch {
saveItemToWishListUseCase.get().invoke(item)
}
}
}


ここでは `SaveItemToWishListUseCase` は `get()` が呼ばれるまでインスタンス化されないため、メモリ使用量が削減され、このユースケースが常時必要ない場合でもアプリ性能が向上します。

### コルーチン管理には `viewModelScope` を利用 🚀

ViewModel内でユースケースを実行する際は必ず `viewModelScope.launch{}` を用います。これには以下の利点があります:
- Viewモデルが破棄されたとき、自動的にコルーチンがキャンセルされてメモリリークを防ぎます。
- UIがもはやアクティブではなくなるとバックグラウンドタスクも適切に終了します。

デフォルトでは `viewModelScope.launch{}` は **Main Dispatcher** を利用しており、UI関連操作には理想的です。
> 注意すべき点として `_@ViewModelScoped_` はそのユースケースインスタンスのライフサイクルを制御し、「1つのビュー・モデルライフサイクルごとの単一インスタンス」を保証します。一方 `_viewmodelScope.launch{}_` はコルーチン実行とキャンセル管理を行います。同じ名前ですが別々の懸念事項です。

### 主なポイントまとめ

✅ **ビュー・モデルとの共存性**
  `@ViewModelScoped` によって ユーズ ケース がビュー・モデルと共存し、不必要な再生成を避けられます。

✅ **ビュー・モデル内では `viewmodelScope.launch{

参考記事

ドメインレイヤ | App architecture

ドメインレイヤ は、UI レイヤとデータレイヤの間に位置するオプションのレイヤです。 含まれている場合、オプションのドメインレイヤは UI レイヤに ...

ソース: Android Developers

Google推奨アーキテクチャとClean Architectureの違い

UIレイヤーをデータレイヤーに依存させてアプリを構築し、必要に応じてドメインレイヤーを後から導入することができます。最終的には、公式ガイダンス ...

ソース: Qiita

ドメイン駆動設計で実装を始めるのに一番とっつきやすい ...

クリーンアーキテクチャ は、Androidアプリケーションのアーキテクチャとして、DDDとは関係なく使われている記事がちらほら見受けられました。 これは ...

ソース: はてなブログ

クリーンアーキテクチャーでスマホアプリ開発した感想(勉強会用)

優れたアーキテクチャとは ; 開発しやすい. 適切に分割されたコンポーネントがあれば分担しやすい ; テストしやすい. ユースケースがフレームワークに依存 ...

ソース: Qiita

Go言語で構築するクリーンアーキテクチャ設計 (技術の泉 ...

この書籍は、クリーンアーキテクチャの理念や具体的な実装方法が掴みにくい、レイヤーごとの責務分担やドメインサービスの実装、ユースケースレイヤーでのトランザクション ...

ソース: Amazon.jp

大規模なリファクタリングに備えて、Androidアプリのアーキテク ...

Android のアプリアーキテクチャーガイドは、アプリの堅牢性、品質の向上、そしてテストの容易さを重視しています。 アーキテクチャの基本原則として関心の ...

ソース: Zenn

Androidアーキテクチャことはじめ ― 選定する意味と、MVP

Clean Architecture はDDDの考えに基いており、Dataレイヤー、Domainレイヤー、Presentationレイヤーに分かれています。 このうち、Domainレイヤーに ...

ソース: AMBI

状態ホルダーと UI 状態 | App architecture

ビジネス ロジック状態ホルダーはユーザー イベントを処理し、データレイヤまたはドメインレイヤのデータを画面 UI 状態に変換します。Android のライフサイクルと ...

ソース: Android Developers

Columnist

エキスパート

関連ディスカッション

❖ 関連記事