ドメインレイヤは、UI レイヤとデータレイヤの間に位置するオプションのレイヤです。
ドメインレイヤは、複雑なビジネス ロジック、または複数の ViewModel で再利用される単純なビジネス ロジックをカプセル化します。すべてのアプリにこのような要件があるわけではないため、このレイヤはオプションです。複雑さに対処する場合や再利用性を優先する場合など、必要な場合にのみ使用してください。
ドメインレイヤの利点は次のとおりです。
- コードの重複を回避できます。
- ドメインレイヤ クラスを使用するクラスの読みやすさが向上します。
- アプリのテストのしやすさが向上します。
- 役割を分担することで、クラスが大規模になることを回避できます。
これらのクラスをシンプルかつ軽量に保つために、各ユースケースは 1 つの機能だけを担うべきであり、可変データを含むべきではありません。代わりに、可変データを UI レイヤまたはデータレイヤで処理する必要があります。
このガイドにおける命名規則
このガイドで、ユースケースにはそれぞれが担うアクションに基づいて名前が付けられています。規則は次のとおりです。
動詞の現在形 + 名詞 / 対象(省略可) + UseCase。
例: FormatDateUseCase
、LogOutUserUseCase
、GetLatestNewsWithAuthorsUseCase
、MakeLoginRequestUseCase
。
依存関係
一般的なアプリ アーキテクチャでは、ユースケース クラスは UI レイヤの ViewModel とデータレイヤのリポジトリの間に位置します。つまり、ユースケース クラスは通常リポジトリ クラスに依存し、リポジトリと同じように、コールバック(Java の場合)またはコルーチン(Kotlin の場合)を使用して UI レイヤと通信します。詳しくは、データレイヤのページをご覧ください。
たとえば、ニュース リポジトリと著者リポジトリからデータを取得して組み合わせるユースケース クラスがアプリに用意されている場合があります。
class GetLatestNewsWithAuthorsUseCase(
private val newsRepository: NewsRepository,
private val authorsRepository: AuthorsRepository
) { /* ... */ }
ユースケースには再利用可能なロジックが含まれているため、他のユースケースでも利用できます。ドメインレイヤに複数のレベルのユースケースが存在することは普通です。たとえば、次の例で定義されているユースケースは、UI レイヤの複数のクラスが画面に適切なメッセージを表示するためにタイムゾーンに依存している場合、FormatDateUseCase
のユースケースを利用できます。
class GetLatestNewsWithAuthorsUseCase(
private val newsRepository: NewsRepository,
private val authorsRepository: AuthorsRepository,
private val formatDateUseCase: FormatDateUseCase
) { /* ... */ }
<ph type="x-smartling-placeholder">をご覧ください。
<ph type="x-smartling-placeholder">Kotlin でユースケースを呼び出す
Kotlin では、operator
修飾子を使用して invoke()
関数を定義することで、ユースケース クラス インスタンスを関数として呼び出せるようにすることができます。次の例をご覧ください。
class FormatDateUseCase(userRepository: UserRepository) {
private val formatter = SimpleDateFormat(
userRepository.getPreferredDateFormat(),
userRepository.getPreferredLocale()
)
operator fun invoke(date: Date): String {
return formatter.format(date)
}
}
この例では、FormatDateUseCase
の invoke()
メソッドにより、クラスのインスタンスを関数のように呼び出すことができます。invoke()
メソッドは、特定のシグネチャに制限されることなく、任意の数のパラメータを取り、任意の型を返すことができます。クラスでさまざまなシグネチャを使用して invoke()
をオーバーロードすることもできます。上の例からユースケースを呼び出すと次のようになります。
class MyViewModel(formatDateUseCase: FormatDateUseCase) : ViewModel() {
init {
val today = Calendar.getInstance()
val todaysDate = formatDateUseCase(today)
/* ... */
}
}
invoke()
演算子の詳細については、Kotlin のドキュメントをご覧ください。
ライフサイクル
ユースケースに独自のライフサイクルはありません。代わりに、それを使用するクラスにスコープが設定されます。つまり、UI レイヤのクラス、サービス、または Application
クラス自体からユースケースを呼び出すことができます。ユースケースに可変データを含めることはできないため、依存関係として渡すたびに、ユースケース クラスの新しいインスタンスを作成する必要があります。
スレッド化
ドメインレイヤからのユースケースはメインセーフである必要があります。つまり、メインスレッドから安全に呼び出せるようにする必要があります。長時間実行ブロッキング オペレーションを行うユースケース クラスは、そのロジックを適切なスレッドに移動する必要があります。ただしその前に、ブロッキング オペレーションを階層内の他のレイヤに配置する方がよいかどうかを確認してください。通常、再利用性やキャッシュ保存を促進するために、複雑な演算はデータレイヤで行われます。たとえば、大規模なリストでのリソースを大量に消費するオペレーションは、アプリの複数の画面で再利用するために結果をキャッシュに保存する必要がある場合、ドメインレイヤよりもデータレイヤに配置する方が適切です。
次の例は、バックグラウンド スレッドで処理を行うユースケースを示しています。
class MyUseCase(
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
suspend operator fun invoke(...) = withContext(defaultDispatcher) {
// Long-running blocking operations happen on a background thread.
}
}
一般的なタスク
このセクションでは、一般的なドメインレイヤのタスクを行う方法について説明します。
再利用可能でシンプルなビジネス ロジック
UI レイヤに存在する繰り返し可能なビジネス ロジックは、ユースケース クラスにカプセル化する必要があります。これにより、ロジックが使用されるあらゆる場所で変更を簡単に適用できます。また、ロジックを分離してテストすることもできます。
前述の FormatDateUseCase
の例について考えてみます。今後、日付形式に関するビジネス要件が変更された場合、1 か所でコードを変更するだけで済みます。
リポジトリを組み合わせる
ニュースアプリには、ニュースと著者のデータ オペレーションを処理する NewsRepository
クラスと AuthorsRepository
クラスがあります。NewsRepository
が公開する Article
クラスには著者の名前しか含まれませんが、著者に関する詳細情報を画面に表示する必要があるとします。著者情報は AuthorsRepository
から取得できます。
このロジックには複数のリポジトリが含まれており、複雑になる可能性があるため、GetLatestNewsWithAuthorsUseCase
クラスを作成して、ViewModel からロジックを抽象化し、読みやすくします。また、ロジックを分離してテストしやすくなり、アプリのさまざまな部分で再利用できるようになります。
/**
* This use case fetches the latest news and the associated author.
*/
class GetLatestNewsWithAuthorsUseCase(
private val newsRepository: NewsRepository,
private val authorsRepository: AuthorsRepository,
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
suspend operator fun invoke(): List<ArticleWithAuthor> =
withContext(defaultDispatcher) {
val news = newsRepository.fetchLatestNews()
val result: MutableList<ArticleWithAuthor> = mutableListOf()
// This is not parallelized, the use case is linearly slow.
for (article in news) {
// The repository exposes suspend functions
val author = authorsRepository.getAuthor(article.authorId)
result.add(ArticleWithAuthor(article, author))
}
result
}
}
このロジックでは、news
リスト内のすべてのアイテムがマッピングされます。したがって、データレイヤがメインセーフであっても、処理するアイテムの数は不明であるため、メインスレッドをブロックしてはなりません。そこで、このユースケースではデフォルトのディスパッチャを使用してバックグラウンド スレッドに処理を移動します。
その他のコンシューマー
UI レイヤとは別に、ドメインレイヤは、サービスや Application
クラスなどの他のクラスで再利用できます。さらに、TV や Wear などの他のプラットフォームがコードベースをモバイルアプリと共有している場合は、それらの UI レイヤもユースケースを再利用して、前述のドメインレイヤのメリットをすべて享受できます。
データレイヤのアクセス制限
ドメインレイヤを実装する際は、UI レイヤからデータレイヤへの直接アクセスを許可するか、すべてを強制的にドメインレイヤ経由にするかについても検討する必要があります。
この制限を行うメリットは、たとえばデータレイヤへの個々のアクセス リクエストに対して分析ロギングを実施している場合に、UI がドメインレイヤ ロジックをバイパスするのを防止できることです。
ただし、潜在的な重大なデメリットとして、データレイヤへの単純な関数呼び出しだけであってもユースケースを追加しなければならないことがあります。つまり、わずかなメリットのために複雑性が増す可能性があります。
おすすめのアプローチは、必要なときにだけユースケースを追加することです。UI レイヤがほぼ特定のユースケースに限ってデータにアクセスしていることを発見した場合は、その方法でのみデータにアクセスすることが合理的です。
最終的に、データレイヤへのアクセスを制限するかどうかの判断は、個別のコードベースに依存するとともに、厳格なルールと柔軟なアプローチのどちらを選ぶかに帰着します。
テスト
ドメインレイヤをテストする際は、一般的なテスト ガイダンスが適用されます。一般的に、他の UI テストではデベロッパーは偽のリポジトリを使用します。ドメインレイヤをテストするときも、偽のリポジトリを使用することが適切です。
サンプル
次の Google サンプルは、ドメインレイヤの使用方法を示しています。このガイダンスを実践するためにご利用ください。