Skip to content

Instantly share code, notes, and snippets.

@nobuoka
Last active February 10, 2023 06:56
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nobuoka/da8cecec098217d0dbdb58e44f30d049 to your computer and use it in GitHub Desktop.
Save nobuoka/da8cecec098217d0dbdb58e44f30d049 to your computer and use it in GitHub Desktop.
Kotlin における、呼び出し側に処理して欲しい例外の扱い方を検討する。

Kotlin で例外処理

Kotlin で、呼び出し側に処理して欲しい例外をどう扱うか。

背景

  • Web アプリケーションにおいて、例外的な状況を呼び出し側に処理して欲しい場合がある。
    • サーバー内部で起こったどうしようもない例外的状況については、500 Internal Server Error でユーザーに返せば良いので、web 層で何かをする必要はない。
    • 一方で、ユーザー入力の誤りだったり、DB 内の状態がクライアント側の想定している状態と違っていて衝突する場合など、丁寧にエラーメッセージを返す必要がある場合については、、アプリケーション層内部で検知されたそれらの例外的状況に対して、web 層で丁寧にメッセージを組み立てる必要がある。
  • また、メソッドシグネチャのような形で、扱うべき例外的状況が明示されていると嬉しい。

課題

  • Kotlin ではチェック例外がないため、呼び出し側に送出された例外の処理を強制することができない。
  • 組み込みの例外機構とは別の方法で、呼び出し側に例外的状況の処理を強制する方法を考える必要がある。

方法一覧

思いつく方法として、以下の 2 通りがある。

  • (A) Either クラスなどを作って、レスポンスの型を成功時の型と失敗時の型の直和型っぽいもので表現する。 (ExceptionWithEither.kt の方法。)
  • (B) 例外的な状況ごとに例外クラスを生成するインターフェイスを定義し、その実装をメソッドパラメータで受け取るような設計にする。 (ExceptionWithMethodParameter.kt の方法。)

方法検討

  • 直和型が扱いやすいのであれば、(A) の方法が単純で良さそうに思える。 が、Kotlin の場合は直和型を表現するためにいちいち sealed class を定義する必要があり、扱いにくい。
    • メソッドごとに sealed class を定義する必要がある。
    • 下の層の sealed class で定義されている例外を表すクラスを、上の層の sealed class で再利用できない。 新たに定義しなおす必要がある。
  • (B) の方法には上記の問題がない。
  • (A) の方法では組み込みの例外機構を使用しないので、「呼び出し側が扱うべき例外的状況」 と 「本来は起こって欲しくない、どうしようもない例外的状況」 を明確に区別できる。 (前者はメソッドの返り値で扱い、後者を組み込みの例外機構で扱う。)
  • (B) の方法では、呼び出し側が扱うべき例外的状況も、本来は起こって欲しくないどうしようもない例外的状況も、どちらも組み込みの例外機構で扱われる。
    • 明確な区別をすることができなくなるが、呼び出し側がいい感じに扱うことでなんとかなる。

といったところを考えて、(B) の方法を使いたい。

import javax.ws.rs.WebApplicationException
import javax.ws.rs.core.Response
// 使う側の例。
fun main(args: Array<String>) {
FooApplicationService.fetchFoo("taro", "").then {
when (it) {
is Either.Right -> Response.ok().entity(it.value)
is Either.Left -> when (it.value) {
is FooApplicationService.Exception.AuthorizationException -> throw WebApplicationException("権限がありません。", Response.Status.FORBIDDEN)
is FooApplicationService.Exception.FooNotFoundException -> throw WebApplicationException("指定の Foo が存在しません。", Response.Status.NOT_FOUND)
}
}
}
}
// 便利定義。
inline fun <T, R> T.then(next: (T) -> R) = next(this)
sealed class Either<L, R>() {
class Left<L, R>(val value: L) : Either<L, R>()
class Right<L, R>(val value: R) : Either<L, R>()
}
fun <L, R> L.toEitherLeft() = Either.Left<L, R>(this)
fun <L, R> R.toEitherRight() = Either.Right<L, R>(this)
// アプリケーション層のクラスの例。
object FooApplicationService {
fun fetchFoo(actor: String, id: String): Either<Exception, String> =
AuthorizeFoo.verifyAuthorization(actor).then {
when (id) {
"200" -> "[foo:$id]".toEitherRight()
else -> Exception.FooNotFoundException().toEitherLeft()
}
}
sealed class Exception {
class FooNotFoundException : Exception()
class AuthorizationException : Exception()
}
}
object AuthorizeFoo {
fun verifyAuthorization(actor: String): Either<Exception, Unit> =
if (actor != "taro") Exception.AuthorizationException().toEitherLeft() else Unit.toEitherRight()
sealed class Exception {
class AuthorizationException : Exception()
}
}
import javax.ws.rs.WebApplicationException
import javax.ws.rs.core.Response
// 使う側の例。
fun main(args: Array<String>) {
FooApplicationService.fetchFoo("taro", "", object : OnFooNotFoundException, OnAuthorizationException {
override fun authorizationException(actor: String) = WebApplicationException("$actor には権限がありません。", Response.Status.FORBIDDEN)
override fun fooNotFoundException() = WebApplicationException("指定の Foo が存在しません。", Response.Status.NOT_FOUND)
}).then { Response.ok().entity(it) }
}
// 例外生成クラスの例。
interface OnAuthorizationException {
fun authorizationException(actor: String): RuntimeException
}
interface OnFooNotFoundException {
fun fooNotFoundException(): RuntimeException
}
interface OnBarNotFoundException {
fun barNotFoundException(barId: String): RuntimeException
}
inline fun <T, R> T.then(next: (T) -> R) = next(this)
// アプリケーション層のクラスの例。
object FooApplicationService {
fun <E> fetchFoo(actor: String, id: String, errors: E): String
where E : OnFooNotFoundException, E : OnAuthorizationException =
AuthorizeFoo.verifyAuthorization(actor, errors).then {
when (id) {
"200" -> "[foo:$id]"
else -> throw errors.fooNotFoundException()
}
}
}
object AuthorizeFoo {
fun <E> verifyAuthorization(actor: String, errors: E)
where E : OnAuthorizationException {
if (actor != "taro") throw errors.authorizationException(actor)
}
}
// 複数のアプリケーションサービスの例外をまとめて扱うクラスを作る例。
fun test() {
// 複数のアプリケーションサービスの例外をまとめて扱うクラスを 1 個定義しておく。
val exceptions = object : OnFooNotFoundException, OnBarNotFoundException, OnAuthorizationException {
override fun authorizationException(actor: String) = WebApplicationException("...")
override fun fooNotFoundException() = WebApplicationException("...")
override fun barNotFoundException(barId: String) = WebApplicationException("...")
}
// 複数のアプリケーションサービスで使いまわせる。
BarApplicationService.fetchBar("", "", exceptions).then { Response.ok().entity(it) }
FooApplicationService.fetchFoo("", "", exceptions).then { Response.ok().entity(it) }
}
object BarApplicationService {
fun <E> fetchBar(actor: String, barId: String, errors: E): String
where E : OnBarNotFoundException =
when (barId) {
"100" -> "[bar:$barId}"
else -> throw errors.barNotFoundException(barId)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment