ある日仕事で日付操作のutility的な関数(プロジェクト全体でも使えるし、移植しようと思えばできそうな関数)を書いていて、その関数のAPIデザインについて議論があり、Kotlinらしいコードについて改めて意識するきっかけになったので、内容をメモがてら書いてみます。
作った関数
Javaの標準ライブラリのjava.time.LocalDate
のLocalDate.parse(dateStr: String): LocalDate
メソッドは融通がきかなく、2011-12-03
的な形式の日付文字列しかパースしてくれず、それ以外の形式の場合は例外を投げるような仕様になっています。
そこで様々な形式の日付文字列がパースできる以下のようなutility的なメソッドを作りました。
(object DateUtil
というグローバルなオブジェクト内に定義しています)
fun parseLocalDate(dateStr: String): LocalDate? = TODO()
改善された点は2点です。
- yyyy-MM-dd以外の形式の日付フォーマットでもパースできるようにした
- パースできないフォーマットの場合は、例外を投げるのではなくnullを返すようにした
2つのメソッドの使い方は、以下のようになります。
// java.time.LocalDateのネイティブなメソッドを使う
// ※ld1, ld2の型はLocalDate
val ld1 = LocalDate.parse("2020-09-23") //OK
val ld2 = LocalDate.parse("2020/09/23") //例外を投げる
// 実装したメソッドを使う
// ※ld3, ld4, ld5の型はLocalDate?
val ld3 = DateUtil.parseLocalDate("2020-09-23") //OK。LocalDate.parse("2020-09-23")と同じ結果
val ld4 = DateUtil.parseLocalDate("2020/09/23") //OK。LocalDate.parse("2020-09-23")と同じ結果
val ld5 = DateUtil.parseLocalDate("invalid date") //null
議論内容
さて、このメソッドについて同僚にレビューを依頼した際に、
「LocalDate.parse
が異常系において例外を投げているのに、DateUtil.parseLocalDate
が異常系において例外を投げるのではなくnullを返しているのはなぜか」という質問がありました。
これについて以下のように理由を回答しました。
- 例外を返す必要性がないため
- 日付形式が不正という事実は、コールスタックをさかのぼり伝播させる必要がない情報であるため
- 異常系のケースは「形式が不正」というパターンだけなので、例外の種類により表現し分けなくてもnullで表現できる
- Kotlinの標準ライブラリとかを見ててもこちらのほうがKotlinっぽいから
ここでこの記事の主題でもある、*「Kotlinっぽいから」*という主観的な回答をしたことについて、何かもう少しきちんと説明できないかと感じていたところに、ちょうど自分の考えていたことと近い記事を見つけたのでそれについて内容紹介・整理したいと思います。
頭を整理する上で参考になった記事
この記事が参考になりました。 Kotlin and Exceptions
(執筆者はJetbrainsでKotlinライブラリのチームリードをしている方のようです)
要約としては KotlinらしいメソッドのAPIデザインとしては、基本的にはメソッドの返り値をnullにするなり型を用意するなりでメソッドの振る舞いを表現すべき。例外は予期せぬ問題発生時に投げ、その例外はアプリケーションのルートレベル(大外)でHandlingすべき。 というものでした。
記事のポイントをまとめると、
- Javaには検査例外という仕組みがある
- 例えばファイルI/O処理の場合、I/O途中にエラーが発生する可能性があるため、検査例外の仕組みにより例外ハンドリング(try catch)をすることを強制できる
- この機能により異常系処理を書き忘れないというメリットが昔はあった
- しかしその後メモリI/Oのようなエラーがほとんど発生しない処理にも検査例外が使われるようになり、例外チェックのボイラープレートコードが大量発生した
- またJava8でLambda記法が登場したが、検査例外のせいで使いづらい面があった
- Kotlinも例外という仕組みを引き継いだ。そのおかげでJVMライブラリをシームレスに使える
- しかし検査例外は前述の問題があるため言語仕様としては引き継がなかった
- Kotlinにおける例外の使い方
- 例外はロジックのエラーに対して使う(例:『価格を表す引数は0以上でなければいけない』)
- 例外は基本的にキャッチしない。トップレベルでキャッチされるようにする。
- I/Oのエラーについてもキャッチせず、トップレベルでキャッチされるようにする。
- **標準ライブラリも
OrNull
がついたものが多い。**異常系になりうるケースではそれらを使う(例:失敗しうるケースではString.toInt()ではなく、String.toIntOrNull()を使う) - 複数種類の失敗があり得る場合も例外ではなく、sealed classなどで各失敗を表現する型を定義すべき
※ Javaにそこまで詳しくないので、細かい点誤りあればすみません
ちょうど自分が感じていた「Kotlinっぽさ」というのは、例外ではなく返り値(null等)でメソッドの振る舞いを表現する
ということであり、Jetbrainsで標準ライブラリを作っている人もまさにそういうことを考えていたことを知れました。