139
68

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

UseCaseの存在意義を考えてみる

Last updated at Posted at 2023-10-20

この記事は

UseCaseとは何か、どういった理由で必要なのかという基本的な知識に加えて、1つのUseCaseには1つの公開メソッドであるべきかという議論や、そのようにした場合UseCaseが増殖してしまうのではないかという問題についても言及していきます。

この記事でいうUseCaseは、あくまでモバイル開発における文脈です

UseCaseってなに

学生時代、個人開発をしていた頃には使ったことがなかったUseCase。社会人になると突然現れます。

今まではViewModelから直接Repositoryを呼び出していました。
そこには、なんの疑問もありませんでした。

そして社会人になり、Androidエンジニアとしてプロの現場に入ると、奴は突如としてViewModelとRepositoryの間に現れるのです。

それが、UseCaseです。

UseCaseは本当に必要?

「だって、個人開発してた時はいらなかったもん!」
「なくても困らないんだから、いらないんじゃない?」

そう思う方もいるかもしれません。
この疑問に対する答えを公式ドキュメントが提供してくれています。

The domain layer is responsible for encapsulating complex business logic, or simple business logic that is reused by multiple ViewModels. This layer is optional because not all apps will have these requirements.

ドメインレイヤーは、複雑なビジネスロジックや、複数のViewModelで再利用される単純なビジネスロジックのカプセル化を担当する。すべてのアプリにこのような要件があるわけではないので、このレイヤはオプションです。

「このレイヤはオプションです」とありますね。

これは、必要な場合のみUseCaseを作成してください、ということです。

別に、なくてもいいのです。必要がないなら。

UseCaseが必要な時って?

必要な時に作成してくださいとありますが、UseCaseが必要な時というのはどういうシチュエーションなのでしょうか。

これに対する答えも公式ドキュメントが与えてくれています。

  • It avoids code duplication.
  • It improves readability in classes that use domain layer classes.
  • It improves the testability of the app.
  • It avoids large classes by allowing you to split responsibilities.
  • コードの重複を避けることができる。
  • ドメイン・レイヤーのクラスを使用するクラスの可読性が向上します。
  • アプリのテスト性が向上します。
  • 責任を分割できるため、クラスが大きくなるのを防ぎます。

つまり、

ViewModelのコードが多くなって可読性が低くなった時や、ViewModelに再利用したいコードがあったり、ViewModelにあるロジックのテストがしにくいと感じた時

に、そのような処理をUseCaseに切り分けると問題が解決します。

UseCaseを作るときのルールは?

1. UseCaseにはビジネスロジックを書く

一般的に、UseCaseの責務はビジネスロジックのカプセル化です。

難しい言い方はやめましょう。

UseCaseさんには、そのアプリ独自の処理・アプリの要件が書かれてあります。

例えば、ツイッター...今やXと呼ばれているアプリがあります。
Xではポスト(ツイート)やいいねができますよね。
ポストやいいねはX独自の機能であり、Xに必要な機能であり、XをXたらしめている機能です。

そういった機能のことを、プログラミングの界隈では ビジネスロジック と呼ぶのです。

2. 公開メソッドは1つだけ

公開メソッドというのは、他のクラスからも呼び出しが可能なメソッドという意味です。
修飾子がprivateのメソッドとかは違います。

UseCaseはViewModelから呼び出されますが、その入り口となるメソッドは一つだけであるべきだ、ということです。

公式ドキュメントにもそのような記述があります。

In this guide, use cases are named after the single action they're responsible for.

このガイドでは、ユースケースは担当する1つのアクションにちなんで名付けられます。

1アクション、1UseCaseだと述べていますね。

例えば、X(旧:Twitter)でポスト(ツイート)の機能を担当しているPostUseCaseなるものがあるとします。

そこには、ポストするというアクションに関する公開メソッドが一つだけあり、それ以外の公開メソッドはない、というのが公式のガイドラインに準じたUseCaseのあり方です。

class PostUseCase(private val postRepository: PostRepository) {
    suspend fun execute(postText: String) {
        postRepository.post(postText)
    }
}

class PostViewModel(private val postUseCase: PostUseCase): ViewModel() {
    viewModelScope.launch {
        postUseCase.execute(postText)
    }
}

公開メソッドは1つであるべきですが、privateメソッドは1つである必要はありません。
また、上記のサンプルコードは雰囲気です。実際に動くコードではありません。

本当にそのルールでいいの?

「1アクションにつき1UseCaseだと?」
「そんなことをしたら、UseCaseがいっぱい出来ちゃうんじゃないか?」

と思う人もいるかもしれません。

実際にredditでもそのような議論がなされています。

1つのUseCaseが1つの公開メソッドだけを持つべき理由

それでは1つのUseCaseが何故1つだけ公開メソッドを持つべきなのか考えていきます。
これはいくつかの視点から考えることができます。

1. 名前から考える

そもそもUseCaseは、 「ユースケース」という名前を冠しているというのもあって、そいつができることは一つであるべき です。

たとえば一つのユースケースの中に複数の要件があると、それどういうユースケースなの?となってしまいます。

もっと具体的に考えてみましょう。

あるUseCaseの中に、ログインができるという要件、ポストができるという要件、いいねができるという要件、があるとします。

このUseCaseってどういうユースケースなんですかね? :thinking:

2. 公式に書いてないことをやるな

そもそも、公式ドキュメントと違うルールを適用すること自体にリスクがあります。

我々はチームで開発をしているのです。

新しく入ってきたメンバーや練度の低い新人エンジニアでも、「1つのUseCaseに1つの公開メソッド(1つの要件)」というルールをなしにしてもなお、単一原則を守りながら他の機能に影響を与えずに実装ができるのでしょうか。

公式のガイドラインと異なる事をするなら、それなりのメリット・理由が欲しいところですが、それにしてもチームの新参者にとって 公式ガイドラインからズレたルールというのは、理解にハードルがあるもの です。

3. ユースケース図から考える

ユースケース図というのをご存知でしょうか。
次の図はレストランシステムを例にしたユースケース図です。
このレストランシステムは

  • 料理を食べる
  • 料理の代金を支払う
  • ワインを飲む
  • 料理を作る

などができるようです。

これはこのアプリの要件ですね。

image.png

参考: ユースケース図 wikipedia

ソフトウェアの界隈ではアプリの要件を決めるときにこのようなユースケース図を書いたりします。

ユースケース図を用いてこのように一つ一つ要件を定義していくのだから、その内容をプログラムに起こした場合も1対1対応であるべき

という捉え方をするとやはり、1要件 = 1公開メソッドにつき1UseCaseであるべき、と考えることができます。

4. UseCaseのライフサイクル

UseCaseのライフサイクルに関して公式ドキュメントには次のように書かれています。

Use cases don't have their own lifecycle. Instead, they're scoped to the class that uses them. This means that you can call use cases from classes in the UI layer, from services, or from the Application class itself. Because use cases shouldn't contain mutable data, you should create a new instance of a use case class every time you pass it as a dependency.

ユースケースには独自のライフサイクルはありません。代わりに、ユースケースはそれを使用するクラスにスコープされます。これは、UIレイヤーのクラス、サービス、またはアプリケーションクラス自体からユースケースを呼び出せることを意味します。ユースケースは変更可能なデータを含むべきではないので、依存関係として渡すたびにユースケースクラスの新しいインスタンスを作成する必要があります。

要約すると、

UseCaseはライフサイクルや状態をもたせるべきではない。
UseCaseのライフサイクルは、それを使う側のクラスに合わせるべきだ。

というお話で、これを実現させるためには、UseCase同士は完全に独立している必要がある、という考えに至るのです。

例えば、あるHogeUseCaseが別のFooUseCaseを実行して結果を得たいとき、もしFooUseCaseが非同期処理だったらHogeUseCaseは待ち続けるとか、定期的に結果を確認しないといけないです。

そうなるとHogeUseCaseが状態(ライフサイクルを含む)を持たざるを得なくなるので、結果的に「状態を持たない」といった話なども踏まえて、 異なるUseCaseの利用をしたくなったら、完全に独立させなければ実現ができない という形になります。

UseCase同士が完全に独立している必要がある、すなわち、1つのUseCaseの中に複数の要件があってはいけない、とまとめることができます。

お互いにライフサイクルや状態を持っていなければ問題ありません。

UseCaseがUseCaseに依存するのはあり?なし?

先の内容で

UseCase同士が完全に独立している必要がある

という説明をしましたが、それを踏まえて次の図をみてみましょう。

image.png

これは公式ドキュメントのUseCaseに関するDependenciesの説明部分に添付されている図です。

この図を見ると、UseCaseがUseCaseに依存しているように見受けられますね。

UseCase同士は完全に独立しているべきでした。
Googleさん、これはどういう見解なのでしょうか。
我々はどう生きるべきなのでしょうか。 :bird:

ありよりの意見

  • 公式ドキュメントを見る限り良さそう
  • そもそもユースケース図では「ユースケースAがユースケースBを呼ぶ」というのは普通にあるので、実装する時も許容できる

なしよりの意見

  • どっちのUseCaseがどっちのUseCaseを呼び出すのか悩んでしまう
  • 最悪、循環的に参照してしまう事態にもなりかねない

解決策を模索

UseCaseから別のUseCaseを呼び出したいシチュエーションになった時、直接依存させる以外にどのような解決策があるのでしょうか。

ある動画サービスを例にして、ログアウトした時に全てのダウンロード動画を削除する事例を考えてみましょう。

ログアウト機能を担うユースケースは、LogoutUseCaseとし、ダウンロード動画を削除する機能を担うユースケースをdeleteAllDownloadedVideoUseCaseとします。

直接UseCase同士を依存させる場合、次のような実装です。

class LogoutUseCase(
    private val deleteAllDownloadedVideoUseCase: DeleteAllDownloadedVideoUseCase
) {
  suspend fun execute(...) {
    deleteAllDownloadedVideoUseCase.execute()
  }
}

これをどのようにして依存関係を解消していきましょうか。

1. Serviceクラスを導入する

Domain Service という概念を新たに Domain Layer に導入し、 UseCase から呼び出されることだけを許容するクラス を作ります。

すると次のような分割ができます。

class LogoutUseCase(
    private val authRepository: AuthRepository,
    private val downloadedVideoDomainService: DownloadedVideoDomainService,
) {
  suspend fun execute(...) {
    downloadedVideoDomainService.deleteAll()
    authRepository.delete()
  }
}

class DeleteAllDownloadedVideoUseCase(
    private val downloadedVideoDomainService: DownloadedVideoDomainService,
) {
  suspend fun execute(...) {
    downloadedVideoDomainService.deleteAll()
  }
}

UseCaseはViewModelから使われるのが一般的なので、UseCaseより奥のレイヤーにServiceクラスを作成しViewModel→UseCase1→Service→UseCase2という依存関係になるよう分割しました。

これの良いところはもちろん、UseCase同士を完全に独立できたという点にありますが、悪いところもあります。

それは、やはり 公式ドキュメントにないことをするな というところです。

Serviceクラスを導入しているプロダクトは多いかとは思いますが、Serviceクラスに関する詳しい説明は公式ドキュメントにはなく、かなり抽象的で、プロダクトによって存在意義が変わるものだと思います。

Androidの公式ドキュメントは充実しています。

それが故に、先にも述べましたが、公式ドキュメントにないルールを導入するというのはチームの新参者にとって理解にハードルが生まれる のです。

また、シンプルに層がひとつ増えてより複雑になる、という意味でもやはり好ましくはないでしょう。

2. ViewModelで順番にUseCaseを呼び出す

別にUseCase同士を依存させなくても ViewModelで順番ずつ呼び出せばいい じゃんともなります。

しかし、ログアウト時に 必ず ダウンロード動画を削除するなら、要件をコードにできる限り落とし込むという意味合いでは少し弱い気がします。

とはいえそれも、DIを使って List<UseCase>と、UseCaseのリストを受け取るようにすれば解決されます。

Listは順番を持っているので、実行順序に制約があるようなパターンだとUseCaseをListで受け取るのが良いです。

UseCaseではビジネスロジックという意味のある単位を扱っているので、その単位から逸脱して「呼び出し側が上手く扱えば良い。」という判断をするのは、あまり好ましくありません。このような策を取らざるを得ない場合はできるだけバラバラで呼び出すのは避けたいです。

3. ユースケースを順番に実行するだけのユースケース

ユースケースを順番に実行するだけのユースケースを別途作る ことで、「特定ロジックのユースケース間の依存」ではなく「画面や実行順序への依存」に鞍替えすることもできます。

例えば次のような実装になると思います。

class DeleteAllDownloadedVideoAndLogoutUseCase(
  private val logoutUseCase: LogoutUseCase,
  private val deleteAllDownloadedVideoUseCase: DeleteAllDownloadedVideoUseCase,
) {
  suspend fun execute(...) {
     deleteAllDownloadedVideoUseCase()
     logoutUseCase()
  }
}

こうすると、Serviceクラスのような公式ドキュメントにない新しい概念によって新規メンバーを混乱させることなく、かつ要件的に何がしたいのかも明確です。

しかし、「画面や実行順序への依存」に鞍替えしたことによって、おそらくこれはログアウト画面でのみ呼ばれる特別なUseCaseとなりました。

ドキュメンテーションコメントに基本的にはログアウト画面でのみ、このUseCaseを使用すると書き示しておくなどの工夫があると、他の画面から呼び出される心配がなく新規メンバーにもより配慮した実装になりそうです。

また、2つのUseCaseをまとめあげたことによって命名が複雑になり、実装者目線でDeleteAllDownloadedVideoAndLogoutUseCaseが見つけにくくなってしまいました。

言い換えると、LogoutUseCaseの方が見つけやすいので、DeleteAllDownloadedVideoAndLogoutUseCaseを呼び出したいタイミングで結局LogoutUseCaseを単独で呼び出してしまう可能性があります。

このような場合もLogoutUseCaseのドキュメンテーションコメントに「ログアウト時は基本的にDeleteAllDownloadedVideoAndLogoutUseCaseを使う」と書いておけばチームメンバーに意図を伝えることができます。

上記のサンプルコードは全て雰囲気です。実際に動くコードではありません。

結局、UseCaseの存在意義ってなんなのさ?

1要件につき1UseCaseだとUseCaseが不必要に増殖するのでは?という疑問があったかと思いますが、先にも述べたとおり、UseCaseは必ず必要というわけではありません。

必要なければ、作らなくていいのです。

必要だと判断した場合に作成されたUseCaseは1要件1UseCaseであったとしても、そしてそのルールによってUseCaseが多少増えたとしても、それは 必要な余白 なのです。

おわりに

UseCaseの基本的な知識に加えて、あるべき姿や依存関係など答えのない問題に対しても解説してみましたが、いかがでしたでしょうか。他にもさまざまな意見があるかと思いますので、改善案や訂正・アドバイス等お待ちしております :bow:

参考リンク

公式ドキュメント Domain layer
ユースケース図 wikipedia
redditでのUseCaseに関する議論

139
68
10

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
139
68

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?