はやぶさダイアリー

主にWebのエンジニアリングについて書くブログ

構造が定まってないHashにアクセスする場合は[]よりもfetchのほうが安全

Railsの話。Hashは[]で値が取り出せる

pry(main)> p = {a: {b: :c}}
pry(main)> p[:a][:b]
 => :c

Hashの構造が定まってなくて、求めている構造のときのみ値を取得するケースを考える

例えば信用できない外部APIのレスポンスをパースしたり、ユーザーがPOSTするデータ*1をパースするケースが該当する。

[]だと発生する問題

pry(main)> p = {} #  p = {a: {b: :c}}を期待しているが実際は {} が来た
=> {}

pry(main)> p[:a][:b] # NoMethodError: undefined method `[]' for nil:NilClass 

p.try(:[], :a).try(:[], :b) # tryを使ってNoMethodErrorを回避する
=> nil 

これで良いかと思ったら以下のケースで問題が起きた

pry(main)> p = {a: 'b'} # こういうのが来た
=> {:a=>"b"}

pry(main)> p.try(:[], :a).try(:[], :b)
# TypeError: no implicit conversion of Symbol into Integer
# from /Users/user/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/activesupport-5.2.3/lib/active_support/core_ext/object/try.rb:19:in `[]'

何故TypeErrorが起きたか

pry(main)> p = {a: 'b'}
=> {:a=>"b"}

pry(main)> p.try(:[], :a)
=> "b"

なので 文字列に対する[]メソッドが呼ばれる。 ドキュメントを見る限りこのメソッドの引数にsymbolは期待されていないので、無理やりIntegerに変換しようとしてエラーが起きたのだと思われる。

解決策

fetchを使う。 fetchは文字列に対してメソッドが定義されてないので、tryによってnilが返却される

pry(main)> p = {a: 'b'}
=> {:a=>"b"}
pry(main)> p.fetch(:a).try(:fetch, :b)
=> nil

教訓

  • 構造が定まってないHashにアクセスする場合は[]よりもfetchのほうが安全かもしれない。
  • [] はいろんなクラスに定義されているメソッドなので、型が不定な場合に[]を使うことに気をつけた方が良い。

*1:正確にはユーザーがPOSTするデータはHashではなくActionController::Parameters だが、同じようなものとして扱えると思う

『良いウェブサービスを支える 「利用規約」の作り方』を読んだ

良いウェブサービスを支える 「利用規約」の作り方 を読んだ。

利用規約に限らず、ウェブサービスに関する法規制についてのわかりやすいガイドブックだった。

この本を読んだモチベーション

  • 法的な制約によりXXの実装が必要だったことが後から発覚して手戻りが発生する、といったリスクを減らしたいため。
    • 例えば、確認画面を実装する必要があることが後から発覚したり
      • 参照: 通信販売|特定商取引法ガイド
        • 申込みをする際、消費者が申込み内容を容易に確認し、かつ、 訂正できるように措置していないことを「顧客の意に反して売買契約等の申込みをさせようとする行為」として禁止し、行政処分の対象としています。

  • サービスを開発する上でのルールを知ることで、全体最適された解決策を出せるようにしたいため。

個人的に面白かった点

プラットフォームを利用する場合は法律に加えてプラットフォームの規約も意識しなければならないこと

    • 資金決済法の前払式支払手段による規制を避けるため、ポイントの有効期限を180日以内にしようと思っても(a)、プラットフォームのレギュレーションによりApp内課金でポイントを購入する場合はポイントに有効期限を付けられない(b)
      • (a)は法律の範囲だが、(b)はプラットフォームの規約。(b)まで法務がカバーしてない可能性がある。エンジニアが実装のことしか考えてないと、iOSアプリのレビューが却下された段階で(b)が発覚してヤバイことになるので気をつけたい。
      • App Store Reviewガイドライン - Apple Developer
        • App内課金で購入されたクレジットやゲーム内通貨に有効期限を設定することはできません。

事業のことがわかると良い提案ができる

  • サービスの現状や将来のビジョンがわかると、それらを踏まえた上でより良い利用規約が提案できる。
    • しかしそうではなく、むしろそういったネガティブな「リスク」こそ、みなさんから積極的に開示して相談してほしいのです。そうすれば、私たちのような職業に就く者の多くは、そういった実直なリスクコミュニケーションをしてくれる経営者・エンジニアには「アイデアイノベーションの可能性をできるだけ無駄にせずに、しかしリスクは利用規約によってできるだけ最小化しよう」という発想で、対応策を考えます。

  • ここらへんはエンジニアも法務も同じだなと思った。

データモデルとUIの整合性をとる

ソフトウェア開発で要件を詰める際に、データモデルとUIの整合性に気をつけている。 特にどういった点に気をつけて整合性を確認しているか書く。

データモデルとは

一言で言うとデータをモデリングしたもの、つまりソフトウェアが扱うデータを整理して構造化したものこと。 ここでは、どのようなデータがあってそれらがどんな関係性か表した図のことをざっくり言っていて、特定の技術(例えばRDBMS)に特化した図までは想定してない。

なぜ整合性をとりたいか

整合性が取れてないと、あるユースケースでUIを通してデータを表示したり操作することが困難だったり、可能であってもシステムの複雑度を無駄にあげてしまうことがあるため。

整合性をとる際の観点

1. nullが入るかどうか

nullは演算が定義されてないため、演算を独自で定義する必要がある。*1 なので演算の対象となるデータの場合にnullが入る場合、システムが複雑化するリスクがある。

ここでいう演算とは、以下のようなものを指す。

  • 四則演算
  • 集計
  • 比較
  • 条件式の真偽値

nullが入る場合のシステムの複雑化とそれがユーザーにもたらす価値を比較して、仕様の決定をするのが良いと思う。

例えば

食事の栄養素を保存、表示するとして 栄養素が測定できない場合にnullを保存する場合。

栄養素に関する演算が入りそうであれば、仕様やシステムの複雑さが上がる 例えば1日あたりに摂取した栄養素を集計する場合にnullな栄養素が入っていた場合、 集計結果をnullとして扱うかもしれないし、nullな栄養素は0として集計するかもしれない。 そこらの仕様を議論して、栄養素を集計する場合の演算を実装する必要がある。

2. 多重度の整合性が取れているか

多重度とは、あるデータともう一方のデータの数の関係性のこと。1:1、1:n、n:n といったものを指す。

提案されたUIの背後にあるデータモデルと、実際あるべきデータモデルの多重度と違う場合がある。その場合は何故違うのか掘り下げてみる。

例えば

スマートロックのアプリケーションで、データモデル的には錠前: 鍵 = 1: n だった。つまりある錠前に対していろんな鍵を持てた。これは他の要求によってそうする必要があった。 なのに、提案された解施錠を行うUIは 錠前: 鍵 = 1: 1 になっていた。 この場合、なぜそうなっているのか確認するのが良いと思う。 例えば特定の鍵を選択していることが前提で、選択する画面が考慮から漏れていたのかもしれない。 ここらを掘り下げることで良い議論ができるんじゃないかと思う。

*1:以下の記事がNULLについて参考になった。よりデータベース寄りの話をしている。 https://speakerdeck.com/sinsoku/db-design-without-updating?slide=5

画面遷移が複雑なAndroidアプリにどう対処したか

(メモレベルの雑文なので、読みづらいかもしれません)

やりたいこと

作っていたAndrodアプリの画面遷移が複雑なため、画面遷移を一望し、条件付きの分岐を行う仕組みを作りたい。

以下のようなIoT機器の設定アプリをイメージしてください

  • ある画面に出入りするパターンが複数ある
    • 同じ設定用の画面を使用するが、機器登録後の初期設定 または 設定の更新のいずれかによって画面遷移が異なる
    • 1つ目の機器登録か2つ目の機器登録かによって分岐

やったこと

以下を比較

1. archtecture component navigation を使用する

https://developer.android.com/topic/libraries/architecture/navigation/navigation-conditional

メリット

  • 画面遷移全体をGUIで俯瞰できる
  • 画面遷移の実装が比較的簡単

デメリット

  • 画面遷移の条件分岐の管理はarch navigationはやってくれない
  • argumentsをxmlで定義しなければならないのがちょっと嫌
  • 遷移方法の選定時点(2018/12)でalpha版だった。変更についてくコストがちょっとかかりそう

2. メルカリアッテを支えるオートマトンを参考にする

https://tech.mercari.com/entry/2017/11/17/161508

メリット

デメリット

  • TinderStateMachineのDSLの方が好み(雑)

留意点

3. Tinder StateMachine を使用する

https://github.com/Tinder/StateMachine

メリット

  • DSLが見やすくて良い

con

  • ステートマシンが現在のステートを持つ実装になっている
    • 画面を現在のステートとしたいので、ステートマシン自体に現在のステートを保持したくない(二重管理になってしまう)

検討結果

Tinder StateMachineの実装を参考にしつつ、現在のステートはステートマシンの外から取ってくるようにしました。

コード

https://gist.github.com/TomohikoSato/0c80441743d5aaddbdb3613cfdc3e316

DroidKaigi 2019で「BLEアプリ設計パターン」という発表をしました

DroidKaigi2019で発表してきました。

スライド

speakerdeck.com

(追記:動画上がってたので追加しました)

www.youtube.com

人前で30分も喋るのは初なので、これはガチでやらねばと思って会社から時間もらって色々準備させてもらいました。 おかげさまで、自分的には納得が行く発表ができました。

準備の内容や振り返りは別エントリで書くかもです。

謝辞

登壇するにあたって、BLEの仕様についてや、入社以前に決まっていたBLE通信周りの設計に関して、自分で調べつつ社内外のいろんな人に聴いて回りました。 特に上原(@u_akihiro)さんにBLEについて色々聞けたことで、いろんな疑問がクリアになったと思います。様々な疑問に答えて頂き、ありがとうございました。

f:id:hayabusa1113:20190211192856p:plain
上原さんに色々質問している様子

また、スライドを書くにあたって様々な人にアドバイスを頂きました。 特に、@tlyncさんに聞き手の感情をイメージしてスライドを組み立てたらといったアドバイスを貰えたのがかなり参考になりました。どうもありがとうございました。

f:id:hayabusa1113:20190210230130p:plain
聞き手の感情をイメージしてスライドを組み立てるアドバイス的なやつ

スライドのデザインについて、社内のデザイナーである id:nyaricoさんにいけてるデザインに仕上げてもらいました。 DroidKaigi他の人のプレゼンテーションスライドの気合の入り方を見ると、デザイナーにデザインしてもらって本当によかったと思いました。

f:id:hayabusa1113:20190210230740p:plain:w500f:id:hayabusa1113:20190210230744p:plain:w500
Before→After的なやつ

他にも様々な方に協力していただきました。協力して頂いた皆さま、どうもありがとうございました!

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-

感想

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