データモデルと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で定義しなければならないのがちょっと嫌
- Kotlinアンチパターン のdelegatedPropertyのように、コード上で簡単にargumentを渡したかった
- 遷移方法の選定時点(2018/12)でalpha版だった。変更についてくコストがちょっとかかりそう
2. メルカリアッテを支えるオートマトンを参考にする
https://tech.mercari.com/entry/2017/11/17/161508
メリット
デメリット
- TinderStateMachineのDSLの方が好み(雑)
留意点
- Swiftの例であり、KotlinとはDSLの表現力が違う
- レシーバ付きラムダはSwiftではサポートされていない Swiftと比較しながら見る、KotlinのDSLを支える技術 - ペンギン村 Tech Blog
3. Tinder StateMachine を使用する
https://github.com/Tinder/StateMachine
メリット
- DSLが見やすくて良い
con
- ステートマシンが現在のステートを持つ実装になっている
- 画面を現在のステートとしたいので、ステートマシン自体に現在のステートを保持したくない(二重管理になってしまう)
検討結果
Tinder StateMachineの実装を参考にしつつ、現在のステートはステートマシンの外から取ってくるようにしました。
コード
https://gist.github.com/TomohikoSato/0c80441743d5aaddbdb3613cfdc3e316
DroidKaigi 2019で「BLEアプリ設計パターン」という発表をしました
DroidKaigi2019で発表してきました。
スライド
(追記:動画上がってたので追加しました)
人前で30分も喋るのは初なので、これはガチでやらねばと思って会社から時間もらって色々準備させてもらいました。 おかげさまで、自分的には納得が行く発表ができました。
準備の内容や振り返りは別エントリで書くかもです。
謝辞
登壇するにあたって、BLEの仕様についてや、入社以前に決まっていたBLE通信周りの設計に関して、自分で調べつつ社内外のいろんな人に聴いて回りました。 特に上原(@u_akihiro)さんにBLEについて色々聞けたことで、いろんな疑問がクリアになったと思います。様々な疑問に答えて頂き、ありがとうございました。
また、スライドを書くにあたって様々な人にアドバイスを頂きました。 特に、@tlyncさんに聞き手の感情をイメージしてスライドを組み立てたらといったアドバイスを貰えたのがかなり参考になりました。どうもありがとうございました。
スライドのデザインについて、社内のデザイナーである id:nyaricoさんにいけてるデザインに仕上げてもらいました。 DroidKaigi他の人のプレゼンテーションスライドの気合の入り方を見ると、デザイナーにデザインしてもらって本当によかったと思いました。
他にも様々な方に協力していただきました。協力して頂いた皆さま、どうもありがとうございました!
iOSDC Japan 2018 に参加しました
ちょっと体調が悪かったため、一部しか参加できませんでした。 個人メモですが、感想を書きます。
コンパイラから紐解くSwift method dispatch
登壇者が高校生なのに衝撃を受けました。「希望の国のエクソダス」という小説をちょっと思い出しました。
メソッドディスパッチの挙動がコンパイラの最適化をかけるかけないで変わり、それによって(かなり特殊な)処理の実行結果が変わるようです。 speakerdeck.com
Swiftの型システムに入門する
むずかったです(前提知識が足りなかった)。勉強しようと思いました。 下記スライドにある、「型システム入門」に入門するためのページ ( https://speakerdeck.com/ukitaka/swiftfalsexing-sisutemuniru-men-suru-iosdc-japan-2018?slide=43 ) を参考に入門していきたいと思います。
公開鍵ピンニング
speakerdeck.com (他の発表を聞いていたため、スライドを見ての感想です)
- SSL証明書の中間者攻撃ってできるんだっけ?と最初びっくりしましたが、認証局がハッキングされた場合の話でした。
- 公衆無線LANで仕事とかしていいのか悩ましいなと思いました。
- 運用はけっこう大変そう。サーバサイドエンジニアと連携したり、強制アップデートが必要だったり等。
色々ググりましたが、公開鍵ピンニングを採用するべきかは賛否両論ありそうです。一般論ですがサービスの性質に応じて導入を検討、という感じですね。
自堕落な技術者の日記 : HPKP(HTTP Public Key Pinning)公開鍵ピニングについて考える - livedoor Blog(ブログ)
グーグル、「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に記述するとします。 以下をエラーを想定し、それぞれに例外クラスを定義したとします。
上記のケースをハンドリングしたいとすると以下のような見た目になります。
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 の例が参考になった。
例
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("失敗しました") //想定してない例外なのでとりあえずな文言を出して、裏でクラッシュレポーティングツールとかに伝えるのが良いんじゃないかと思う。 } })