クリーンアーキテクチャ初心者目線で記事を書いています。
勉強にもなりますので、間違っている点・改善すべき点があればコメントしていただけると助かります
サーバーサイドでクリーンアーキテクチャを使ってみました!
でもPresenterはありません!
……え?
クリーンアーキテクチャの例の図
「The Clean Architecture」と書かれたこの図、勝手に本質は右下のフローであると思っています。
この記事を見にきた皆様も、「クリーンアーキテクチャの右下の図」と聞けばこのフローを思い浮かべるのではないでしょうか?
つまり、このフローにきちんと則っていないクリーンアーキテクチャはクリーンアーキテクチャとは呼べません。
と言っても、業務を経験したことがない初心者が盲信しただけの机上の空論です。クリーンアーキテクチャもあくまで設計論であって、ギチギチに縛られてしまっては良くない、とTwitterでも耳にするので多分よくないのでしょう。
でも一度はちゃんと実装してみたい
ということで、Ktorで無理やり「クリーンアーキテクチャの右下の図」を実装してみました。
Ktor
Kotlinで書かれたWebアプリケーション用の非同期フレームワークです。
本記事ではKtorを使ってクリーンアーキテクチャを実装してみたいと思います。
Kotlin Corotuinesをガッツリ使っているので、Ktor以外のフレームワークではおそらく殆ど適用できません。Goならできるのかな?Goでのクリーンアーキテクチャの記事お待ちしてます。
クリーンアーキテクチャに立ちはだかるWebフレームワークの壁
多くのWebフレームワークではMVCモデルをベースとしている節があり、基本的にはメソッドやラムダ内でクライアントへの応答を完了させる必要があります。
いくつかの記事で紹介されているような、Controllerのメソッドに戻り値をレスポンスとしたり、Controller内でレスポンスを組み立てるような設計であれば上記の仕様を満たすことができます。
しかし、UseCaseInteractorが非同期に処理を行いPresenterに値を渡す場合ではControllerの処理が即座に完了するので、Presenterがレスポンスを送信しようとすると何かしらの例外が発生する可能性があり、多くの場合意図したレスポンスにはなりません。
つまり、「クリーンアーキテクチャの右下の図」を実装するには一工夫加えてあげる必要があります。
解決策
今回は、Kotlin Coroutinesと相性の良いKtorを使ってこの問題を解決してみます。
UseCaseInputPort/UseCaseInputPortのメソッドを中断関数に変更する。
こちらが一番見た目が綺麗ですが、関数を中断するということは中断して行われる処理の完了に依存することになります。
理想のクリーンアーキテクチャではありません。
suspendCoroutine
でPresenterの処理が完了するまで待機する
Kotlin CoroutinesにはsuspendCoroutine()
があります。この関数に渡されたラムダ内でContinuation.resume()
が呼び出されるまでその場で処理を中断することができる便利な関数です。
この図のように、リクエストによって呼び出された関数から直接Presenterを待機します。Controller・UseCaseInteractor・Presenterはそれぞれ一方通行であり、中断関数の終了を待機する必要がなくなります。
suspendCoroutine
を使ってみる
早速使っていきます。省略してあるコードがありますが、適宜補完してください。
interface HelloWroldUseCaseInputPort {
fun execute()
}
// Functional interfaceです。かなり便利。
fun interface HelloWorldUseCaseOutputPort {
fun handle(result: String)
}
class HelloWorldUseCaseInteractor(
// execute()内でコルーチンを実行するため、外部からCoroutineContextをもらう
coroutineContext: CoroutineContext,
private val outputPort: HelloWorldUseCaseOutputPort
) : HelloWorldUseCaseInputPort, CoroutineScope by CoroutineScope(coroutineContext) {
override fun execute() {
launch {
val result: String = hoge()// 非同期処理とか
outputPort.handle(result)
}
}
}
class HelloWorldController() {
suspend fun helloWorld(call: ApplicationCall, outputPort: HelloWorldUseCaseOutputPort) {
val coroutineContext = currentCoroutineContext() // この中断関数が実行されているコンテキストを取得する
val useCase = HelloWorldUseCaseInteractor(coroutineContext, outputPort)
useCase.execute()
}
}
val controller = HelloWorldController()
routing {
get("/helloworld") {
val coroutineScope = CoroutineScope(currentCoroutineContext())
// ①が実行されるまで -> Presenterの処理が終わるまで待機
suspendCoroutine<Unit> { continutation ->
coroutineScope.launch { // Presenter
controller.helloWorld(call) { result ->
call.respond("Hello, World and $result.")
}
}.invokeOnCompletion { _ ->
continuation.resume(Unit) // ①
}
}
}
}
「クリーンアーキテクチャの右下の図」の通りに実装することができました!
まとめ
工夫により無理やりクリーンアーキテクチャをサーバーサイドに適用する例でした。
サーバーサイドでControllerとPresenterを完全に分離するためには、もともとクリーンアーキテクチャによって増えていたコードに加え、本質ではないコードをさらに書く羽目になります。
というか、MVPやクリーンアーキテクチャがサポートされたフレームワーク(あるかは知らない)を使った方が本当は良いのですが、有名どころのMVCしかサポーしていないフレームワークでクリーンアーキテクチャをやろうとするのはただただ辛い、という感想しかありませんでした。
サポートされるのを待つか、新種のアーキテクチャが出るまで待つか、そのまま地獄を突き進むか。このままプロダクトで使うのは気が引けますね……
ご意見・ご感想があればぜひコメント欄にてよろしくお願いします。
モリモリ取り込んできます
宣伝
この記事は、クリーンアーキテクチャ+DDDを研究しているリポジトリ
を進めていく上で得られた知見を元に書いています。
よかったら見てやって下さい。