hayabusa PRAY

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

iOSDC Japan 2018 に参加しました

ちょっと体調が悪かったため、一部しか参加できませんでした。 個人メモですが、感想を書きます。

コンパイラから紐解くSwift method dispatch

登壇者が高校生なのに衝撃を受けました。「希望の国エクソダス」という小説をちょっと思い出しました。

メソッドディスパッチの挙動がコンパイラの最適化をかけるかけないで変わり、それによって(かなり特殊な)処理の実行結果が変わるようです。 speakerdeck.com

Swiftの型システムに入門する

むずかったです(前提知識が足りなかった)。勉強しようと思いました。 下記スライドにある、「型システム入門」に入門するためのページ ( https://speakerdeck.com/ukitaka/swiftfalsexing-sisutemuniru-men-suru-iosdc-japan-2018?slide=43 ) を参考に入門していきたいと思います。

speakerdeck.com

公開鍵ピンニング

speakerdeck.com (他の発表を聞いていたため、スライドを見ての感想です)

  • SSL証明書の中間者攻撃ってできるんだっけ?と最初びっくりしましたが、認証局がハッキングされた場合の話でした。
  • 公衆無線LANで仕事とかしていいのか悩ましいなと思いました。
  • 運用はけっこう大変そう。サーバサイドエンジニアと連携したり、強制アップデートが必要だったり等。

色々ググりましたが、公開鍵ピンニングを採用するべきかは賛否両論ありそうです。一般論ですがサービスの性質に応じて導入を検討、という感じですね。

自堕落な技術者の日記 : HPKP(HTTP Public Key Pinning)公開鍵ピニングについて考える - livedoor Blog(ブログ)

公開鍵ピンニングについて | POSTD

グーグル、「Chrome 67」でHPKPのサポートを廃止へ - CNET Japan

全体的な感想

  • アプリ開発のノウハウが成熟してきているのか、自分の興味のバイアスがかかっているのか、発表するのに深い知識か経験が必要な発表が多いように思いました。
    • 型システム、コンパイラ等直接アプリ開発と関係ない(?)けど濃い内容
    • メッセージングアプリ等特定のサービスに特化した発表
  • 圏論とか気になるのでスライド読んで勉強していこうと思います。

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-

感想

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

RxJavaを使ったエラーハンドリングをどうするか その1

前提

  • RxJavaでは例外が投げられた時、Observable#subscribeの引数onErrorにThrowableで渡って来る。

class ApiClient {
    fun fetchSomeData() : Single<SomeData> {
    // 取得処理
    }
}

利用側

apiClient.fetchSomeData()
     .subscribe ( {  data : SomeData -> /**  成功処理 */ }, { t: Throwable -> /** エラーハンドリング */ })

問題

  • どのようなエラーが返って来るかという情報がメソッド定義にない。
    • 実装を読まないとわからない ( = カプセル化されてない)
      • 大きめのコードの場合実装を追うのが大変

解決策

@OnError アノテーションを定義して、ハンドリングして欲しい例外はメソッド定義につける

/**
 * ハンドリングするべき例外をonErrorに流す場合、メソッドの返り値に付与する
 */
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.SOURCE)
@MustBeDocumented
annotation class OnError(vararg val klass: KClass<*>)

ハンドリングして欲しい例外の基準

ユーザーが回復可能かどうか。回復可能な場合、ハンドリングしてユーザーに回復方法を伝える価値がある。 以下の Recoverable Error の例が参考になった。

qiita.com

class ApiClient {
   @OnError(NetworkException::class, ApiException::class )
    fun fetchSomeData() : Single<SomeData> {
    // 取得処理
    }
}

利用側

apiClient.fetchSomeData()
     .subscribe ( {  data : SomeData -> /**  成功処理 */ }, { t: Throwable -> 
            when (t) {
                  is NetworkException -> toast("ネットワーク接続状態を確認してください。")
                  is ApiException -> toast("リクエストに失敗しました。時間を置いて再度お試しください。")
                  else ->  toast("失敗しました") //想定してない例外なのでとりあえずな文言を出して、裏でクラッシュレポーティングツールとかに伝えるのが良いんじゃないかと思う。
            }
})