Rxを使えば非同期か同期かを意識しなくてよくなるというのはどういうことか
AndroidエンジニアがRxを使うモチベーションって非同期処理に対するソリューションとしてが多いと思うんだけど、Futuer使ったりするのと比べて、使う側がそもそも非同期か同期かを知る必要がない点が優れていると思います
— 有象無象 (@kgmyshin) 2017年9月28日
この件、多少説明した方がいいのではと思ったので記事にしました。
一応Androidの話です。題材はなんでもよかったんですがリポジトリパターンを題材としてみました。
あらかじめ言っておくと、コードの細かいツッコミは置いておいてもらえると。伝えたいのは 非同期か同期かを意識しなくていいとはどういうことか です。
Rxのなかった世界のころ
非同期がゆえに...
こういうリポジトリのinterfaceを実装したいと考えます。
interface CourseRepository { fun resolveById(id: CourseId): Course fun resolveAll(): List<Course> }
でも実際は Course
は APIで取得するので実装は下記のようにならざるを得ないわけです。
(blockingGetすればええやんって思う人もいるかもしれませんが、Androidではmain threadでhttp通信したらCrashします)
class CourseRepositoryImpl { val service = Executors.newSingleThreadExecutor() fun resolveById(id: CourseId): Future<Course> = service.submit(Callable { return HTTP通信してResponseをCourseに変換したもの }) fun resoveAll(): Future<List<Course>> = service.submit(Callable { return HTTP通信してResponseをList<Course>に変換したもの }) }
そのため、泣く泣く 外部に提供するinterfaceを
interface CourseRepository { fun resolveById(id: CourseId): Future<Course> fun resolveAll(): Future<List<Course>> }
のように Future
で包んであげなければなりません。
キャッシュしてみる
HTTP通信したものをキャッシュする実装を考えます。
class CourseRepositoryImpl : CourseRepository { val service = Executors.newSingleThreadExecutor() val cache = Cache() // なんらかのCacheクラス fun resolveById(id: CourseId): Future<Course> = service.submit(Callable { if (cache[id] == null) { val course = HTTP通信してResponseをCourseに変換したもの cache[id] = course return course } else { return cache[id] } }) : }
Future型で返さないといけないのもあって、キャッシュを使うときも非同期用のスレッドで実行されます。無駄ですがしょうがないです。
ほかのRepositoryを実装してみる
次に下記のRepositoryを実装しようと考えます。
interface LevelRepository { fun resolveById(id: LevelId): Level fun resolveAll(): List<Level> }
Level関連のデータは同期で取得できるのでこのままのinterfaceで良しとします。
改めて先の CourseRepository
と見比べて見ましょう。
interface CourseRepository { fun resolveById(id: CourseId): Future<Course> fun resolveAll(): Future<List<Course>> }
実装を隠すために interface
きってるのに内部実装が非同期なんだなというのが丸わかりです。
そしてわかるだけならともかく resolveById
とか似たような名前なのに使い方は全く違います。LevelRepository.resolveById
では呼び出せばそのまま値が返ってきます。それに対して CourseRepository.resolveById
は Thread & Handler 使うなりして非同期で取ってきたCourseなどをMainThreadでつかうようにする処理が必要になってしまします。
返り値の型を見ればわかるのですが、おなじ resolveById
でまったく使い方が異なるので命名でもしっかり分けるようにします。今回は非同期の時は request
というprefixをつける運用にします。
interface CourseRepository { fun requestResolveById(id: CourseId): Future<Course> fun requestResolveAll(): Future<List<Course>> }
元の理想形からは遠くなりました。ただそれでも、名前だけでmain threadに戻さないといけないんだなってのはわかるので最高ではないですが以前よりはbetterでしょう。
betterな選択肢を選んできたつもりですが、以上では解決できないしょうがない課題がいくつか残りました。
- interfaceを使う側が非同期か同期かで(interfaceの内部実装の都合によって)いちいち処理を変えないといけない
- 無駄に非同期処理にせざるを得ない箇所がでてくる
いっそLevelRepositoryの各メソッドも全てFutureで包めばいいやという解決策もありですが、その場合はより課題 ( 2 ) の方が深刻になってきます。
Rxを導入する
Rxを導入します。この場合は全て Single や Maybe などの Rxの型で包む運用とします。
interface CourseRepository { fun resolveById(id: CourseId): Maybe<Course> fun resolveAll(): Single<List<Course>> }
interface LevelRepository { fun resolveById(id: LevelId): Maybe<Level> fun resolveAll(): Single<Level> }
これによって先の課題2つをまるっと解決できます。
interfaceを使う側が非同期か同期かでいちいち処理を変えないといけない問題について。
まず、interfaceを使う側は XXXRepository.resoveById(id).observeOn(uiScheduler).subscribe()
とすれば、interfaceの内部実装が非同期だろうが同期だろうが常に処理をすれば済みます。もちろんinterfaceの内部実装が非同期か同期かを知るすべはありませんし、意識する必要もありません。
次に無駄に非同期処理にせざるを得ない箇所がでてくる問題について。
これについても subscibeOn
によって処理スレッドを実装時に指定できるのでオールおっけーです。
例えば先ほどのキャッシュありの実装は下記のようになります。
class CourseRepositoryImpl : CourseRepository { fun resolveById(id: CourseId): Maybe<Course> = if (cache[id] == null) { return Single.create(HTTP通信してResponseをList<Course>に変換する処理) .doOnSuccess(キャッシュに保存) .toMaybe .observeOn(ioScheduler) // 非同期用のスケジューラを設定 } else { return Maybe.just(cache[id]) // 非同期用のスケジューラは設定しなくて良い } : }
処理によって選択することができるので無駄に非同期処理みたいなことはなくなります。
まとめ
- Rx(など)を使わなかったら下記の課題がある
- interfaceの各メソッドの内部処理によって使う側が非同期か同期かを考えて実装しなくていけなくて辛い
- 必要のない非同期処理がが入ってしまうことがある
- Rxを使うことで処理によって、使う側が非同期か同期を知らずに実装できる (一部運用は必要)
kotlinのcoroutineが出てきて、ExecutorやThread Handlerを使うのが面倒という課題があってRxを使ってた人と、この記事に書いたような課題も感じてRxを使ってた人でギャップがあるなぁと感じた昨今です。
coroutineは確かに素晴らしいですが、上記のような課題を解決(するためのもの)/(できるもの)ではないので、自分の場合は Rx を coroutine に置き換えるというのが表現として厳しいなぁと思った次第であります。
coroutineがダメだといういう気はなく、Rxのここが良いんだよというのが伝われば幸いです。
追記
記事の内容自体は(わかりみ)という感じなんですが、coroutinesでも解決できる課題な気がします。
— Keita Kagurazaka (@kkagurazaka) 2017年12月1日
suspend funは中断するかもしれない関数でしかないので、同期で返しても問題ありません。
インターフェースをRxで統一するのがOKならば、suspend funで統一してもいいんじゃないかなーと。 https://t.co/4GxVLp0SXj
申し訳ありません。kagurazakaさんの方法で確かにできそうです!
ただ、このツイートのメンションで話してますが、 coroutineでは非同期前提のものを同期でも扱えるのに対し、Rxは非同期同期関係なく扱えるみたいな違いはあるのかなと。 そして今回のケースは自分的にはやはり後者の方が用途としては妥当なのかなと思いました。まぁでもkurokawaさんのいうように気持ち良さとかのレベルの話であるのは違いないです。
ツイッターでうまく議論できない子なので、できれば今度対面でいろんな方と話したいなと思った次第です!ぜひお話しさせてくださいませ!
ドメインオブジェクトをクライアントも持つべきなのか考えてみた
きっかけ
尊敬するエンジニアの一人 @nobuoka さんのつぶやきがきっかけ。
いつも思ってるんだけど、サーバーサイドとモバイルアプリの両方に同じバウンダリーコンテキストのドメイン層があったらドメイン知識が分散してることにならない?? (サーバー側にドメイン層があるならモバイル側にドメイン層は不要では論者)
— Nobuoka Yu (@nobuoka) 2017年10月14日
常々自分も同様のことを考えていて、基本的には賛成。
しかし実際の開発においては 「(複雑な場合 は持たないとつらいなぁ)」 という実感があり、話してみるとnobuoka先生も同様の実感をもっているもよう。
今回はその 複雑な場合 とは実際どんな場合なのかを考えてまとめてた。
注意
この手の議論は燃えやすいので、しっかり前置きしておきます。
ここに書いてある考えが絶対的に正解とは自分自身考えてないです。こう言う風に考えてますと言う段階です。なにか引っかかること、疑問や多少議論してみたいなどあれば @kgmyshin までメンション投げてもらえれば、応えられるときは応えます!
ただし、SNSで議論することをあまり好んでないので、がっつり議論したい方はぜひ弊社に入社をば!一緒に楽しく議論しながら開発していきましょう!!
そもそもの前提となる自分の考え
たとえば、コンテキストマップが下記のようになっているサービスを考える。
このサービスをクライアントをつくるとなった時コンテキストマップはどうなるか。
システムが全く違うので下記のように一つのコンテキストと捉える場合もあると思う。
ただ、クライアント自身が意味のあるコンテキストを持っている訳ではないので、一つのコンテキストとして扱うのではなく必要なコンテキストを写した像のように考えた方が筋が良いのではないかと考えている。
クライアント自身が意味のあるコンテキストを持っている訳ではないからだ。
各コンテキストを拡張して一つのシステムにおいたものがクライアント だと考えている。
ドメインモデル持つべきか?
ここからが本題。どんな場合に持つべきなのか整理して行く。
持つべき場合
サーバーの構成を知らない場合
サーバーの構成やコンテキスト関係がどうなっているわからない場合はクライアントでもドメインモデルを持つべきである。
そもそもサーバー側の構成をクライアントが知る必要はないという意見も正であると思う ので、この理由ひとつだけで 持つべき としてもいいかもしれない。
コンテキストの統合がクライアント内で行われる場合
コンテキストの統合をクライアント内で行う場合がよくある。この場合はドメインモデルをクライアントで作らざるを得ない。
また複数にコンテキストをまたがる場合、コンテキストの統合はエンハンスして行く中で頻繁に発生しうるので初めから各コンテキストでドメインモデルを持っておくべきかもしれない。
オフラインでも動くことを保証される場合
オフライン時、上記のようにサーバー側がなくても動くことを求められているのでドメインモデルはクライアント側でも持たなければならない。
持つべきでない場合
逆に持つべきでない場合に触れておく。
クライアントでは一つのコンテキストしか扱わない
この場合は基本的に単純なクライアントとなるのでドメインオブジェクトを作る必要性はないでしょう。
単純なRSSリーダーなどがここに当てはまると思う。
まとめると...
いろいろ書いたが、 複数のコンテキストにまたがるアプリを作る場合は コンテキストの統合が後々発生した場合に対応するために、 クライアントにもドメインオブジェクトを持つべき 、という判断基準が個人的には妥当だと考えている。 (もちろん例外はあるが)
つまるところ、上に書いた〈複雑な場合〉はもう少し具体的に言うと〈複数のコンテキストにまたがるアプリを作る場合〉だったということになる。
一人旅してきた
9/28(木),29(金)に一泊二日の一人旅してきた。
写真をとる習慣が無いためか、写真はほとんど撮ってない。
唯一撮ったのが下記(本当にどうでもいい写真)だけで、肝心の宿や食事や部屋自体は全く撮るのを忘れていた。
隕石が割れて片方が落ちてきそうな湖だな? pic.twitter.com/FxVt003DMN
— 有象無象 (@kgmyshin) 2017年9月28日
今回は初めての一人旅だった。
疲れがどうにも取れずに思い切って有給をとり自然に囲まれた場所で温泉に浸かりたいなと思ったのがきっかけだった。
ご飯はすごい美味しかったし、温泉は最高だった。
季節的にまだ少し暑いから温泉は微妙かな?と思ったが、行った時にはちょうど気温が低くなり始めた時期になっていた。露天風呂では浸かっている部分は温かく、浸かってない部分はすこし寒いという自分の中では永遠に風呂に入っていられる条件が揃っていた。
温泉を浸かったあとは、湯上り処で無料配布している牛乳 / コーヒー牛乳を飲むこともできる。
夜ご飯は 17時半からと20時からのどちらかを先着で選べる。僕は20時からの方を選んだが、もし行く人がいれば17時半の方をオススメしようと思う。 なぜなら22時から23時までの間に夜鳴きそばというラーメンを無料配布しているからだ。食い意地がはってる自分でも20時に満腹度120%になる食事を食べた2時間後にラーメンを食べる食欲はなかった。ラーメンを食べるために17時半を選ぶべきだった。
ラーメンを食べるミッションをクリアできなかった以外は、大満足大満喫な一人旅行だった。
その初めての一人旅を体験して思ったことがあるので書き記しておこうと思う。
昔、「自分探し」という言葉があまり好きじゃなかった。というのも、〈今の自分をただただ認めたくない人の逃げの行為〉だと考えていて、その行為に嫌悪感を抱いていたんだと思う。最近の僕は「自分探し?いいんじゃない?楽しんできなっせ」くらいには嫌悪感は薄れてたけども、まぁ少なからず根っこには残ってた。
それが、一人旅をしてすこし考えが変わった。
一人で旅をすると、社会の雑念とかしがらみとか全くなしの場所で本当に一人になるので、全意識を自分に(半ば強制的に)向けられるということに気づいた。
最近特になのだが、自分の将来像をなかなかイメージすることができてなかった。 「自分は何をしたいんだっけ?」という問いに対して、無意識的にしがらみとか本来どうでもいいことが入ってきてたみたいで回答を出すのに変な矛盾を解決しなければならない状態になってしまっていたんだと思う。一人旅ではそういう雑念が剥がれて、クリアに考えられた。再確認できた。あぁ、やっぱりそういうことだよね、みたいな。
その回答みたいなのが出てきたときに「あ、これが『自分探し』か!」と一人で変に納得した。
そんな旅だった。
一人旅も良かったが、今度は彼女と行こうと思う。
【日記】DroidKaigiおもしろかった
DroidKaigiがおもしろかった。
発表の内容
まず発表してきたのでそのスライドをここに。
感想とか裏話とかを時系列に箇条書きで
前日まで
- DroidKaigiの週にいろいろな締め切りが集中してしまっていて、資料作りにとれる時間が少なくて正直かなり焦ってた…
- 枠組みはできていたけどこれくらいやれば大丈夫かなっていうレベルまで前日になるまで持っていくことができなかった
- ScalaMatsuri や try!Swift などにも参加していて、体力的にも来ていた
- スピーカーズディナーでアイコンは知ってるけど顔は知らないスピーカーやスタッフの方とお知り合いになれて、おかげで当日ちょっとやりやすかった
- 30分枠の人たちみんなスライドが100枚以上行きましたとか、200枚近く行きましたとか言っていて、俺60-70くらいしかないんだけど大丈夫なんか?って心配になったりした
1日目
- 1日目朝早く起きれたので(自分は2日目なんだけど)家でリハをしてみてた。2,3回やったら力尽きて寝てしまい遅刻した
- リハ的に早く話し過ぎたら18分くらい、ゆっくり話せば25分くらいで終わるなぁという体感を得た。ゆっくり話そうと誓う
- 会場に来てみたら、自分の発表場所 が Room3 という一番でかい場所ということを知って青ざめた
- 自分はエモい系で技術的な話の本筋とは離れるので、小さい部屋で聞きに来てくれた人としっぽりと、なんならみんなで日本酒でも飲みながらやりたいな、くらいに考えていた
- 加えて、自分の時間帯はむしろ自分が聞きたい発表ばかりある激選区 (しらじさんのツイートからもそう感じていたのは自分だけじゃなかったように思う)
2日目どうしても11:50-のセッションどれにするか選べない。。。まじでこの時間激戦区だと思うんだけど・・・。
— しらじ (@shiraj_i) 2017年3月10日
- そわそわしてきて心配になってきたので、弊社のたざわさんや Quipper のこにふぁーさんやだるま に資料見てもらって意見もらったりして、精神安定につとめてた
- 1日目が終わって、こにふぁーさんとだるまと池袋まで行って飲んで、やすべえ行って、スパラクーアに泊まるなど
- スパラクーアで深夜にDroidKaigiアプリにコミットしてた
- こにふぁーさんとは自分がスタートアップで働いていた頃からの付き合いだけど、一緒に同じコードを触るって機会がなかったので感慨深い
- 翌日296PRとのことだったので、あと4つやればよかったと後悔した
2日目
- 緊張して吐きそうだった
- 発表が始まってからは時間を忘れてしまって、ちょっと足早になってしまったようで終わったら21分くらいだった
- 質問をいっぱいしてくれたので早く切り上げて終了みたいなことにならなくて助かった
- 発表中に吐かなかった
- 終わってから Twitterで反響見るまでちょっと怖かった
- 見てみた感じ、少しでも役に立っていそうな、または共感してくれてそうなツイートばかりで胸を撫で下ろした
- 会場に、発表の中に出てきた新卒の子がいたらしい (後で知った
- アフターパーティーで前からアイコンは知ってるけどリアルに知らない人と話せてよかった
- 「アイコンと感じが違いすぎてわかんねぇよ」ともれなく言われたので、アイコン変えたい
- たしかに、写真の頃から10キロくらい痩せたし、メガネも1年以上はかけてない (だが裸眼)
- アフターパーティー後につけ麺食って帰りました
反省
- 話がやっぱり上手くなれない
- 英語圏の人が見てもわかるような資料にすればよかった (思ったけど、時間が取れなかった
- 声が小さかったかもしれない…? (動画を見て確認する
まとめ
勉強になることがたくさんあった。そして、なによりめちゃくちゃ楽しかった。
lottieが大変よろしいものだった
仕事でかっこいいインタラクションを実装することになって、canvasでごりごり頑張るかなぁとも思ったのだけど、少し前に話題になったlottieを使ってみることにした。
結果、レベルの高いアニメーションをすこぶる簡単に1時間もかからず実装することができた。
実際に導入してみるところまで +α を説明してみる。
lottieとは
詳細は こちら から見ることができる。知らない人に三行で説明してみると
After Effectsで作ったアニメーションを
jsonに変換して
それをそのまま読み込ませてアプリに組み込める
以上。
Android, iOS, Native Reactに対応しているみたい。
例えば下記はAfter Effectsで Lottie のサンプルを動かしているところ。
これを、jsonに変換してそのまま lottieのライブラリを入れてjsonを読み込ませるだけで下記のようになる。
手順
導入から、実際にアプリに組み込むところまでの流れをざっと説明します。
After Effectsにbodymovinを導入する
https://github.com/bodymovin/bodymovin をAfter Effectsに導入する
- http://aescripts.com/learn/zxp-installer/ でZXPInstallerをダウンロード
- ZXPInstallerを開こうとして「ZXPInstallerは壊れているため開けません。"ゴミ箱"に入れる必要があります。」と言われたら、
xattr -rc ZXPInstaller
とすると開けるようになる - bodymovinのZXPファイルをダウンロード
- ZXPInstallerに先のZXPファイルをドラッグ&ドロップ
- After Effectsを起動して、メニュー>AfterEffects CC>環境設定>一般設定 の「スクリプトによるファイルへの書き込みとネットワークへのアクセスを許可」を有効
jsonの作り方
これで指定した保存先にjsonが保存されます。
アプリに導入(Android)
https://github.com/airbnb/lottie-android を見てその通りにするだけ。
build.gradleに下記を追加。
dependencies { compile 'com.airbnb.android:lottie:1.0.3' }
そして、先ほどのjsonをassetsに配置して、
animationView.setAnimation("data.json"); animationView.loop(false); animationView.playAnimation();
とするだけ。自動でアニメーションを流したままとかであれば下記でも可。
<com.airbnb.lottie.LottieAnimationView android:id="@+id/animation_view" android:layout_width="wrap_content" android:layout_height="wrap_content" app:lottie_fileName="data.json" app:lottie_loop="true" app:lottie_autoPlay="true" />
以上で下記のようなクオリティ高いアニメーションを実装できる。
bodymovin入れてしまえば、あとはもう画像をImageViewにセットするレベルで簡単に高クオリティのアニメーションを実装できる。はい最高。
使う前の自分が疑問に思ってたところ
ちゃんと透過できます?
できました。下記はLottieのサンプルを動かしている様子。
サイズ調整とかどういう感じなん?
LottieAnimationView
は実質ImageViewです。画像がセットされたImageViewと思って scaleType
やらで調整できます。
はまりどころ?
うごかない?
下記のようにコンポジションの中に別のコンポジションがネストされているとき、そのネストされたコンポジションがアニメーションされないという事象にぶつかった。
ネストを解消することでうまく動いた。bodymovinの吐き出すdemo htmlではしっかり動いていたので lottie側が対応していない or bugのよう。
セットしたアニメーションのクリアはどうする?
アニメーションのセットは下記コードでできる。
animationView.setAnimation("data.json");
描画されているものをクリアするにはどうしたらいんだろう? animationView.clearAnimation();
かな?と思ったけど、これは違う。
先にも少し触れたが LottieAnimationView
は実質ImageView。
setAnimation
時、選択されたjsonファイルはLottieDrawable
に変換され setImageDrawable(lottieDrawable)
が呼ばれる。
そのため、描画されているものをクリアするには setImageDrawable(null)
で実現出来る。
感想
Cooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooool!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!