hayabusa PRAY

技術的で気になった事を書きます。Androidがメイン。

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に記述するとします。 以下をエラーを想定し、それぞれに例外クラスを定義したとします。

  • BLE
    • Bluetoothが使えない (BluetoothNotAvailableException)
      • 端末のBluetoothがオフになっているとか
    • 接続できない (BLEConnectFailedException)
      • ハードウェアが近くになかったとか
    • 接続したデバイスからエラーレスポンスがきた (BLEBadResponseException)
  • API
    • 接続できない (NetworkException)
    • 接続先からエラーレスポンスがきた。 (APIBadResponseException)

上記のケースをハンドリングしたいとすると以下のような見た目になります。

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-

感想

例外の見通しが良くなった気がします。冗長になるのでやり過ぎには注意ですね。