概要
この記事では、Androidクリーンアーキテクチャにおけるドメインレイヤーの重要性について探求します。ビジネスロジックを明確にし、保守性やテスト容易性を向上させる方法を学ぶことで、開発者はより効率的なアプリケーション設計が可能になります。 要点のまとめ:
- ドメインレイヤーはビジネスロジックの核であり、UIやデータソースから独立することで疎結合を実現します。これによって、大規模開発でもその価値が際立ちます。
- ユースケース(インタラクター)はビジネスルールをカプセル化し、再利用性と可読性を高めます。私自身もこの手法を使うことで複雑な条件分岐がスッキリしました。
- テスト駆動開発(TDD)を導入すると、独立したドメインレイヤーの品質が保証されます。モックオブジェクトを用いたテストで早期にバグを発見できることが多いです。
ドメインレイヤーとは何か
ドメインレイヤーの主要な責任
ドメインレイヤーには主に以下の要素が含まれています:
- **ユースケース(インタラクター)**:特定のビジネスロジックを処理し、アプリ内でデータがどのように処理され使用されるべきかを定義します。
ドメインレイヤーの要素 | 責任 | ベストプラクティス | 利点 |
---|---|---|---|
ユースケース(インタラクター) | 特定のビジネスロジックを処理し、データ使用方法を定義 | 単一責任原則に従うことが重要 | 再利用性とテスト容易性の向上 |
ドメインモデル | ビジネスエンティティを表現する | 明確なモデル設計を行うべき | コアロジックの管理と簡潔化 |
リポジトリインターフェース | データアクセス方法を定義し、内部構造から切り離す役割 | 抽象に依存する設計が求められる | ビジネスロジック独立性の確保 |
バリデーション機能 | ユーザー入力やビジネスルールの検証を実施する役割 | シンプルで集中したバリデーションロジックを維持するべきです。 | 一貫性と信頼性向上 |
ユースケースの構成要素
- **ドメインモデル**:ビジネスドメイン内のエンティティを表します。
- **リポジトリインターフェース**:データへのアクセス方法を定義し、データソース(例えば、データベースやREST API)に依存せずに内部構造からの切り離しを実現します。
## ドメインレイヤーの主な責任
✅ **ビジネスロジックのカプセル化とモジュール性の促進**
ドメインレイヤーは、ビジネスロジックを中心にまとめており、それによってプレゼンテーションレイヤーやデータレイヤーから独立させています。この層ではユースケースが定義されており、注文を行うことやユーザー入力を検証するなどの特定のビジネスワークフローを示しています。また、この層でロジックを分離することで、Kotlin Multiplatform(KMP)で共有できるため、iOSなど他のプラットフォームとのロジック重複が軽減されます。
ユースケースの命名規則

ビジネスロジックの定義と依存関係
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シンプル具現 化された唯一真実源収拾出来欠乏考慮要素各種倉庫取り扱い負担減少されていますお陰様ですね!
ユースケースにおけるベストプラクティス
データ検証をドメインレイヤーで行う理由
ビジネスルールの検証は、データが特定の業界固有の規則に従っていることを確保します。フロントエンドでこのような検証を行うことで、行動が実行される前にビジネスルールをチェックできるため、ユーザー体験も向上します。例えば、注文を出す前に十分な資金があるかどうか確認することで、不必要なREST API呼び出しを避けたり、購入手続きに進む前に商品在庫があることを確認したりできます。このチェックが失敗した場合、「購入」ボタンを無効化することも可能です。
フォーマット検証は、データが事前定義された構造やパターンに従っていることを確保します。例えば、日付文字列が`YYYY-MM-DD`という形式になっているかどうかなどです。このような検証は、一貫性と信頼性の高いデータ管理につながります。
ユースケースの管理とライフサイクル
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`)がそれぞれの条件に対してチェックし、不正な場合には適切な結果を返します。このようにして、アプリケーションの状態に応じた柔軟なユースケース設計が可能になります。
データアクセスを制限する利点と欠点
@ViewModelScoped class GetCustomerBagUseCase @Inject constructor() {}
パッケージ構造の例
@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 DevelopersGoogle推奨アーキテクチャとClean Architectureの違い
UIレイヤーをデータレイヤーに依存させてアプリを構築し、必要に応じてドメインレイヤーを後から導入することができます。最終的には、公式ガイダンス ...
ソース: Qiitaドメイン駆動設計で実装を始めるのに一番とっつきやすい ...
クリーンアーキテクチャ は、Androidアプリケーションのアーキテクチャとして、DDDとは関係なく使われている記事がちらほら見受けられました。 これは ...
ソース: はてなブログクリーンアーキテクチャーでスマホアプリ開発した感想(勉強会用)
優れたアーキテクチャとは ; 開発しやすい. 適切に分割されたコンポーネントがあれば分担しやすい ; テストしやすい. ユースケースがフレームワークに依存 ...
ソース: QiitaGo言語で構築するクリーンアーキテクチャ設計 (技術の泉 ...
この書籍は、クリーンアーキテクチャの理念や具体的な実装方法が掴みにくい、レイヤーごとの責務分担やドメインサービスの実装、ユースケースレイヤーでのトランザクション ...
ソース: Amazon.jp大規模なリファクタリングに備えて、Androidアプリのアーキテク ...
Android のアプリアーキテクチャーガイドは、アプリの堅牢性、品質の向上、そしてテストの容易さを重視しています。 アーキテクチャの基本原則として関心の ...
ソース: ZennAndroidアーキテクチャことはじめ ― 選定する意味と、MVP
Clean Architecture はDDDの考えに基いており、Dataレイヤー、Domainレイヤー、Presentationレイヤーに分かれています。 このうち、Domainレイヤーに ...
ソース: AMBI状態ホルダーと UI 状態 | App architecture
ビジネス ロジック状態ホルダーはユーザー イベントを処理し、データレイヤまたはドメインレイヤのデータを画面 UI 状態に変換します。Android のライフサイクルと ...
ソース: Android Developers
関連ディスカッション