RxJavaを使ったエラーハンドリングをどうするか その2 例外翻訳編
状況
一つのユースケースでBLE通信とWebAPI通信を行う場合を考えます。
サンプルコード
class SomeDeviceUseCase { /** BLE通信でデバイスの設定を変更し、WebAPI経由でその設定をサーバーに記録する処理 */ fun changeSetting(setting: Setting) : Completable = deviceClient.changeSetting(setting) .flatmap { apiClient.uploadSetting(setting) } } class DeviceClient { fun changeSetting(setting :Setting) : Completable } class APIClient { fun uploadSetting(setting :Setting) :Compeltable }
どんなエラーがありそうか
RxJavaを使ったエラーハンドリングをどうするか その1 - hayabusa PRAY で書いたように、想定されるエラーを@OnErrorに記述するとします。 以下をエラーを想定し、それぞれに例外クラスを定義したとします。
上記のケースをハンドリングしたいとすると以下のような見た目になります。
class SomeDeviceUseCase { @OnError(BluetoothNotAvailableException::class, BLEConnectFailedException::class, BLEBadResponseException::class, NetworkException::class::class, APIBadResponseException::class) fun changeSetting(setting: Setting) : Completable = deviceClient.changeSetting(setting) .flatmap { apiClient.uploadSetting(setting) } } class DeviceClient { @OnError(BluetoothNotAvailableException::class, BLEConnectFailedException::class, BLEBadResponseException::class) fun changeSetting(setting :Setting) : Completable } class APIClient { @OnError(NetworkException::class::class, APIBadResponseException::class) fun uploadSetting(setting :Setting) :Completable }
問題
@OnErrorの記述が多くてメソッドの挙動が把握しづらいですね。。
解決策
上記レイヤーが下位レイヤーの例外をキャッチして、上位レイヤーにふさわしい抽象度の例外を投げるようにします。
参考
EffectiveJava 第2版 項目61 「抽象概念に適した例外をスローする」 (今買うなら英語で第3版が出てるのでそっちの方がオススメかもです) https://www.amazon.co.jp/dp/4621066056
詳しくはEffectiveJavaを読むとして、ざっと理解するには以下が参考になりました。 【Effective Java】項目61:抽象概念に適した例外をスローする - The King's Museum
例
class SomeDeviceUseCase { @OnError(BluetoothException::class, APIException::class) fun changeSetting(setting: Setting) : Completable //BLE通信でデバイスの設定を変更し、その設定をWebAPI経由でサーバーへ記録したいといった処理 = deviceClient.changeSetting(setting) .flatmap { apiClient.uploadSetting(setting) } } class BluetoothException : Exception { constructor(cause: BluetoothNotAvailableException) : super(cause) constructor(cause: BLEConnectFailedException) : super(cause) constructor(cause: BLEBadResponseException) : super(cause) } class DeviceClient { @OnError(BluetoothException::class) fun changeSetting(setting :Setting) : Completable = /** 省略 */ .onErrorResumeNext { t -> Single.error( when (t) { // 上位レイヤの例外へ翻訳する処理 is BluetoothNotAvailableException -> BluetoothException(t) is BLEConnectFailedException ->BluetoothException(t) is BLEBadResponseException ->BluetoothException(t) else -> t //ハンドリングしなくてよい例外はそのまま流す }) } } class APIException : Exception{ constructor(cause: NetworkException) : super(cause) constructor(cause: APIBadResponseException) : super(cause) } class APIClient { @OnError(APIException::class) fun uploadSetting(setting :Setting) :Compeltable = /** 省略 */ .onErrorResumeNext { t -> Single.error( when (t) { // 上位レイヤの例外へ翻訳する処理 is NetworkException -> APIException(t) is APIBadResponseException -> APIException(t) else -> t //ハンドリングしなくてよい例外はそのまま流す }) }
ハンドリングしなくて良い例外はRuntimeExceptionでラップするべきかも
上の例では
else -> t //ハンドリングしなくてよい例外はそのまま流す
とやってますが、本当はRuntimeExceptionでラップした方が良いかもしれないです。僕はめんどくさいのでやってないですが。。
EffectiveJava 第2版 項目58 「回復可能な状態にはチェックされる例外を、プログラミングエラーには実行時例外を使用する」 には以下のような文があります。
実装するすべてのチェックされない例外は、RuntimeExcePtionをサブクラス化すべきです
これに従うなら、ハンドリングしなくて良い例外はRuntimeExceptionにラップして投げた方が良いでしょう。 RxJavaには必要であればRuntimeExceptionでラップしてくれるメソッドがあります。活用できそうですね。 http://reactivex.io/RxJava/2.x/javadoc/io/reactivex/exceptions/Exceptions.html#propagate-java.lang.Throwable-
感想
例外の見通しが良くなった気がします。冗長になるのでやり過ぎには注意ですね。