どこまで理解すれば「理解した」と言えるか
雑に。
Kotlin のコルーチンについて勉強していてふと思った。 どこまで理解すれば理解したと言えるのか。 何かを理解しているつもりの人でも、どこかで「とりあえずこれはこういうものだということにしよう」として理解せずに存在やその効果を認めているはずだ。
たとえば、みんな掛け算を理解していると思っている。 3x4=12。これは3が4つあると考えることができるので、3x4=3+3+3+3。3+3+3+3=12だと。 けどこれには足し算を理解している必要がある。そして足し算は理解している。 3+3というのは、3つと3つを足したもの。「●●●+●●●=●●●●●●」だから6だと。 たぶんこの足し算が「これはこういうものだということにしよう」としているところである。 考えようと思えば、なぜ●+●が●●になるのかを考えないといけない。 なぜ1+1が2なのかという問いになる。
いや、なんなら掛け算はいちいち足し算に置き換えて理解していないかもしれない。 小学2年生のころに覚えた九九を使って、機械的に答えを出しているかもしれない。 「さんしじゅうに」なので3x4は12。その場合九九が「これはこういうもの」である。
話を戻して、プログラミングに関しても同じことが言える。というか多分どんなことにでも言える。
Kotlin のコルーチンは中断可能な処理を実行する機構だ。
中断点 (suspension point) で処理を中断し、なんらかの処理が終わったらまたそこから再開される。
中断点はほとんどの場合 Kotlin 標準ライブラリの suspendCoroutine
関数を呼び出すことで作られる。
suspendCoroutine
関数は引数に取るブロックの引数に Continuation
型の値を取る。
なんらかの処理を行って、それが終わったら Continuation
の resumeWith
メソッドを実行することで中断していた処理は中断したところから再開される。
では suspendCorotuine
はどのようにしてそれを実現しているのか?
たぶん気にしなくていい。「コルーチンの中断は suspendCoroutine で行われ、そのブロックの引数に渡される Continuation の resumeWith を呼び出すことでコルーチンが再開される」というふうに理解しても問題ない。
掛け算の例で言うところの九九を暗記しているのと同じだと思う。
コルーチンがどのように Continuation を作成して、どのように中断したポイントから再開しているのかという具体的な技術的な詳細はとりあえずコルーチンを使ってプログラミングをする上では必須の知識ではない。
suspendCoroutine
は SafeCoroutine
クラスと suspendCoroutineUninterceptOrReturn
関数を使っているということは知らなくてもたぶん問題ない。
suspendCoroutineUninterceptOrReturn
関数がどのように実装されているかも、理解していなくてもコルーチンを使ったプログラミングはできる。
仮にここをさらに掘り下げても、その先には JVM の仕組み、 OS の仕組み、 CPU とメモリの仕組み、論理回路の仕組み、電圧と電流と電子に関する学問が待っている。結局どこで「これはこういうものだということにしよう」と割り切るかが問題になる。
個人的に、 Kotlin のコルーチンでは suspendCoroutine
がどういうものなのか(理解していればではなく)知っていればとりあえずはいいのかな、と思った。
Android Architecture Component の ViewModel と画面遷移
MVVM において View の責任は、 ViewModel が表す画面の状態を実際に画面に描画をすることだけであるべきだと思う。
つまり View は、自分自身の意思で画面遷移を行ってはいけない。
ややこしいのは、 Android では View はたいてい Activity が実装するということだ。
Android で画面遷移をちゃんと行える1のは Activity のみなので、 View を実装しているクラスが画面遷移も行わなければならないことになる。
さらに困ったことに、 Android Architecture Component の ViewModel は Activity を間接的にでも参照してはならない2。
ViewModel は Configuration Changes に対応するために Activity のライフサイクルを超えたライフサイクルを持つので、たしかに Activity を保持してしまうと Configuration Changes によって再生成される前の古い Activity を保持し続けてしまうことになり、 Activity 全体を簡単にリークさせてしまう。
View は画面遷移に責任を持つべきではない。 ViewModel の指示で画面遷移が行いたい。でも ViewModel が参照してはいけない Activity しか画面遷移を行えない。困った。
AAC の Google Samples の GitHub issue3 ではこれに対して1つの解決策が提示されている。
その解決策は、 ViewModel が画面遷移などが必要なときに値を通知するような LiveData を公開しておき、 Activity がそれを監視して画面遷移などを行うという方法だ。
こんな感じで実装できそうだ:
sealed class Page { class MainPage : Page() class ListPage : Page() class DetailPage(val item: Item) : Page() } object Navigator { fun bind(activity: AppCompatActivity, navigatable: Navigatable) { navigatable.navigationEvents.observe(activity, Observer { page -> when (page) { is Page.MainPage -> Intent(activity, MainActivity::class.java) is Page.ListPage -> Intent(activity, ListActivity::class.java) is Page.DetailPage -> Intent(activity, DetailActivity::class.java).also { it.putExtra("item", page.item) } }?.let { activity.startActivity(it) } }) } } interface Navigatable { val navigationEvents: LiveData<Page> } abstract class BaseViewModel : ViewModel(), Navigatable { private val _navigationEvents = MutableLiveData<Page>() override val navigationEvents: LiveData<Page> get() = _navigationEvents } class MainViewModel : BaseViewModel() { // something } class MainActivity : AppCompatActivity() { fun onCreate() { val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java) Navigator.bind(this, viewModel) } } class ListViewModel : BaseViewModel() { // something } class ListActivity : AppCompatActivity() { fun onCreate() { val viewModel = ViewModelProviders.of(this).get(ListViewModel::class.java) Navigator.bind(this, viewModel) } }
しかし個人的にはこの方法はなにか違う気がする。
ViewModel が View の状態以外の情報を通知し、それを Activity が購読するという構成がなぜか腑に落ちない。
どうしても ViewModel から Navigator (上のサンプルとは異なる) に直接指示を出すようにして、 Activity で Navigator を実装したくなる。
そのほうが「ViewModel の LiveData = View の情報を通知するもの」「Navigator は画面遷移に責任を持つもの」「View としての Activity は ViewModel の LiveData を監視して画面を更新」「Navigator としての Activity は画面遷移を実装する」という感じで役割がきれいにわかれる気がする。
ViewModel に WeakReference で Activity (やそれに依存するオブジェクト) の参照を保持するという方法もありそうだったけど、それで大丈夫なのかどうかまだわかってないのでとりあえず保留
-
Application などの Context で startActivity することもできるが、 Activity の Back Stack などと相性が悪い。↩
-
ViewModel Overview | Android Developers に
Caution: A ViewModel must never reference a view, Lifecycle, or any class that may hold a reference to the activity context.
と書いてある。↩ -
View actions (snackbar, activity navigation, …) in ViewModel · Issue #63 · googlesamples/android-architecture-components · GitHub↩
Picasso が HTTP 504 エラーを報告して画像が取得できない
Picasso を使用していて HTTP 504 エラーが発生した場合、画像が表示できない原因が本当にサーバー側が HTTP 504 Gateway Timeout を返したからということとは限りません。
Picasso はキャッシュが有効になっている場合、画像をネットワークから取得できなかった時に数回(3回?) 自動的に取得をリトライしますが、最後のリトライではリクエストヘッダに Cache-Control: max-stale=2147483647, only-if-cache
が付与されます。
そしてサーバーがキャッシュを応答できず、ローカルにもキャッシュが存在しない場合、 Picasso (およびその内部で Downloader として使用されている OkHttp)は HTTP 504 を報告します 1。
Picasso が画像ができなかった本当の理由を隠蔽して HTTP 504 を報告してしまうため、 HTTP 504 が原因で画像が取得できなかった場合は OkHttp3Downloader に LoggingInterceptor を適用した OkHttpClient を使用するなどしてさらに詳細なログを取得する必要があります。
CoroutineContext のメモ
まだまとまってないけど雑なメモ
CoroutineContext とは、その名の通り、コルーチンの実行される文脈や条件(コンテキスト)を決定するもの。
実態は連想配列のようなもので、とあるキーとそれに対応する値のペアが複数格納されている。
キーとなれるのは CoroutineContext.Key インタフェースを実装したオブジェクトで、値となれるのは CoroutineContext.Element インタフェースを実装したオブジェクト。
Key インタフェースは型パラメータとして Element 型を取り、 Element 型のキーになれることを表す。 ちなみに Element は CoroutineContext インタフェースを継承している。
CoroutineContext は +
(plus
) メソッドで結合することができ、結合すると CombineContext クラスになる。 CombineContext クラスは CoroutineContext インタフェースを実装しているので、複数結合することが可能。
(CombineContext(CombineContext(CombineContext(context, element1), element2), element3)
のような感じ。)
CombineContext から Key に対応する Element を取り出す場合、 +
演算子の左項に指定した CoroutineContext から優先的に取得される(たぶん)。
よく使用される Element はコルーチンを実行するスレッドを決定する ContinuationInterceptor やコルーチンのライフサイクルやキャンセルを司る Job がある。
どんな Element も持たない空っぽな CoroutineContext の実装として、 EmptyCoroutineContext クラスが用意されている。
新しくコルーチンを起動する launch
メソッドはその引数に context: CoroutineContext
を取ることができるが、何も指定しない場合のデフォルト値にこの EmptyCoroutineContext が指定されている。
launch
メソッドは引数で指定された context を、自身の所属する CoroutineScope のコンテキストと結合する。
クリーンアーキテクチャとはアイデアである
クリーンアーキテクチャについて、最近自分が学んだことを雑なメモとして書き留めたものを公開します。ツッコミ歓迎。
Clean Architecture (クリーンアーキテクチャ) とは、 Robert C. Martin (Uncle Bob) 氏が提唱したアーキテクチャである。
クリーンアーキテクチャに関する記事がたくさん出ていて、殆どの記事がそれをどのように実装するかやパッケージ(名前空間)を分けるかなどについて議論されていて、そもそもクリーンアーキテクチャの考え方とはどのようなものかというところに焦点があまり当てられていないと思った。
クリーンアーキテクチャとはアイデアである。どのようにプログラムを実際にコーディングすればよいかということを指示したり、どのようなディレクトリ構成でどのような命名規則でクラス名を決定すれば良いかという話はあまり関係ない。
クリーンアーキテクチャは、本当に重要な決定のために些末な決定を遅らせることを目指している。 些末な決定とは、どのような Web サーバを使用するかや、どのような DB を使用するかといった、今作成しようとしているアプリケーションのロジックにあまり関係のない決定のことを指す。
アプリケーションの設計で最も大切なのは、そのアプリケーションが行うこと(=ビジネス)を正確にモデリングすることである。 どのような要素がどのように振る舞い、どのような出力を得るのかを考えることである。 このアプリケーションが行うビジネスの範囲のことを、事業領域としてドメインと呼んでいる。
ビジネスのモデリングが終われば、あとはそれを実際のコードに落とし込むだけである。 そしてその際、このドメインモデルをコードで実現することに最も注力すべきである。
そしてこのアプリケーションの振る舞いというのは、特定の Web サーバの詳細や DB に依存しているはずがない。 その特定の Web サーバではなくとも、それが提供する機能さえあれば別に実際はなんでもいいはずだからだ。
具体的な Web サーバや DB などを決定するのを遅らせられれば、アプリケーションがどのようなことを行うかをちゃんと設計してからそれにふさわしいものを後から選ぶことができる。
そのためクリーンアーキテクチャではコードの依存方向を単方向に制限している。
それがあの同心円の図形の外側から内側に向かう方向である。
中心となるのは、このドメインこのビジネスをモデリングしたときに現れたエンティティたちである。 例えば自動車修理受付システムであれば自動車や受付データ、通販システムであれば商品やカートなどである。
それらのエンティティに依存するのは、ビジネスを達成するためのロジック、つまりユースケースや、エンティティのデータを永続化するためのリポジトリやエンティティを外部サービスから取得するためのサービスなどである。
これらは同じ層である。そのためこれらが相互に依存することはクリーンアーキテクチャのルールを破らない。
ユースケースはサービスからデータを取得し、多少改変したりしてリポジトリに保存したり、反対にリポジトリから取得したデータをサービスを使用して外部のシステムに送信したりすることができる。
ただし、リポジトリやサービスは実際に使用する DB や HTTP クライアントに依存してはいけない。それらは今作っているアプリケーション自体ではないからだ。 全く外部のものなので、もっとも外側に位置するものだ。
ではそれらの外側のものに依存することなくリポジトリやサービスを定義することができるだろうか? できる。リポジトリやサービスを「何ができるか」だけを定義したインタフェースとして作成すれば良い。
たとえば自動車修理受付システムなのであれば、 受付リポジトリ
は 受付データを保存する
ことと 保存してある受付データを取得する
ことができれば良いわけだ。
もし外部の自動車カタログ API を使って名前から自動車の情報を取得するということが決まっているのであれば、 自動車検索サービス
というサービスを定義し、そのサービスが 指定した名前に似ている自動車の情報を返す
ことができると定義しておけば良い。
この2つがあれば、 指定した名前の自動車の修理受付を作成する
というアプリケーションの仕様を実現するために、 自動車検索サービス
から名前に一致する自動車の情報を受け取り、 受付リポジトリ
に新しく受付を保存するようなユースケースを作成すれば、画面のボタンが押下されたときにそのユースケースを実行して1つの機能を実行できるわけだ。
おそらく画面のボタンが押下されたときにそのユースケースを叩くのは MVP パターンで言うプレゼンターなどになるだろう。
当然プレゼンターはユースケースよりも外側のレイヤーだ。どのような画面にどのようなボタンが有るかということは、このアプリケーションが自動車修理の受付を行えるかどうかに関係ない。
ここで出したのは飽くまでも例である。クリーンアーキテクチャはエンティティクラスとリポジトリインタフェースとサービスインタフェースとユースケースクラスでコーディングしなければいけないということではない。これは単なる一例だ。
本質は、その依存関係だ。プレゼンターはユースケースに依存し、ユースケースはリポジトリとサービスに依存する。その逆はありえない。このアプリケーションのドメインモデルの中核をなすエンティティを中心として、それを受け取ったり返したりするリポジトリやサービス、そのリポジトリやサービスを使用してビジネスロジックを実現するユースケースクラスがその外側にある。そしてそのユースケースクラスを実行するプレゼンターがさらに外側にあるのだ。
では受付データを保存するための実際の DB や自動車カタログ API を使用するための HTTP クライアントはどこにいったのか?
これらは、リポジトリやサービスの外側にある。そして定義した 受付リポジトリ
を実装し、実際に DB にアクセスして値を読み書きする。 HTTP クライアントを使用する 自動車検索サービス
を実装するクラスもその層に置く。外側の層が内側の層のインタフェースを実装しているのだから、外側から内側への依存だ。
ちなみに、この 層
というのは、ディレクトリのことでもパッケージや名前空間のことでもない。
理論的な境界を引いた領域のことを比喩して層と呼んでいるに過ぎない。
そのためディレクトリやパッケージを切らなくてもクリーンアーキテクチャを採用したコードは書ける。
ちなみにこの実装による依存によって内側のコードが外側のコードに依存しないようにするテクニックを、 Dependency Inversion Principle (DIP; 依存性逆転の原則) と呼ぶ。
そしてもう一つ重要なことがある。それは、誰がインスタンスを生成するのか問題である。
たとえば先程の 指定した名前の自動車の修理受付を作成するユースケース
を使用するプレゼンターをインスタンス化するには、そのユースケースのインスタンスが必要になる。このユースケースは 受付リポジトリ
と 自動車検索サービス
を使用するので、インスタンス化するためにはこれらのリポジトリのサービスの実際のインスタンスが必要となる。そして実際のインスタンスは実クラスを知っていないと作成できない。 受付リポジトリ
だけを知っていては、 受付リポジトリのMySQL実装
のインスタンスを入手できないのである。
そのインスタンスを得るには、かなり外側の層にある 受付リポジトリのMysQL実装クラス
を知っている(=依存する)必要がある。
つまり、すべての層のコードに依存しなければならないタイミングがあるのだ。
すべてのインスタンスを DI コンテナなどで注入させるとしても、では DI コンテナを初期化するのはだれかという問題が残る。
これは main 関数が行う。アプリケーションのエントリーポイントだ。
アプリケーションを実行したとき、必ず何らかのコードが実行される。ここで行えば良い。 アプリケーションには必ずエントリーポイントが必要である。これは仕方のないことである。これを回避することはできないし、する意味もない。
参考
AndroidX に移行すると android.support.v8.renderscript が使えなかった話
趣味で作っているアプリを、せっかくだから targetSdkVersion を 28 にして AndroidX で Kotlin でマルチ module で作ってやるぜ〜と思ったのですが、画像にぼかしをかけるのに使用していた android.support.v8.renderscript
が AndroidX にすると使えなくなってしまいました。
そもそも support-v8 のようなものはもともと依存関係に追加しておらず、どうやって依存が解決されていたのかわかっていませんが、検索しても androidx.legacy:legacy-support-v8:1.0.0
のようなものは見つかりません。
いろいろ調べているうちに androidx.renderscript
というパッケージに移動されたということがわかったのですが、これを import しても依存は解決できずにコンパイルが通りませんでした。
もっと調べると Android Issue Tracker に起票されている既知の不具合のようでした。
Issue: https://issuetracker.google.com/issues/111948805
詳細は issue に書いてあるので読んでください。 Build tools に不具合があったようで、修正できたようなのでそのうちアップデート版の Build tools に取り込まれるようです。 28.0.3 で来ればいいですが、さらに先になる可能性もありますね。
そのアップデートが来るまで待とうかと思ったのですが、 Android Developers の RenderScript のページに次のように有りました。
引用: https://developer.android.com/guide/topics/renderscript/compute#access-rs-apis
When developing an Android application that uses RenderScript, you can access its API from Java in one of two ways:
- android.renderscript - The APIs in this class package are available on devices running Android 3.0 (API level 11) and higher.
- android.support.v8.renderscript - The APIs in this package are available through a Support Library, which allows you to use them on devices running Android 2.3 (API level 9) and higher.
サポートライブラリを使用しない android.renderscript
パッケージは API level 11 以上がサポートされていました。
サポートライブラリを使用すれば API level の要件を 9 以上に引き下げられますが、趣味で作っているアプリなのでそもそも minSdkVersion は 21 に設定しているので…
ということで、 android.renderscipt
を直接使用するように import を書き換え、無事コンパイルできるようになりました。
new によるインスタンス化は避けるべきか?
アプリを設計していると、実装はオブジェクト指向パラダイムな言語で書くことが多いので、オブジェクト指向プログラミングについて思い出したり勉強し直したりすることが多々ある。
今回はドメインエンティティのインスタンスをプレゼンテーション層で作成する時に、条件反射的に new
キーワードによるインスタンス化を回避して Factory や Repository による抽象化を行おうとして、これは過度な抽象化なんじゃないか?と疑問を抱いたので調べたり考えたりした。そのメモ。
new によるインスタンス化
new によるインスタンスは避けるべきだと言われている。 new によるインスタンス化はインスタンス化したいオブジェクトの具象クラスに依存しなければならず、オブジェクト指向の大原則、具象ではなく抽象に依存するという原則を侵害することになるからだ。
抽象化する理由
しかし、何でもかんでも具象クラスのコンストラクタを直接使用したインスタンス化がだめだというわけではない。 たとえば String クラスや Date クラスに対する依存をやめるべきか?そんなわけない。 String は CharSeaquence を実装しているが、いちいち文字列を使用する箇所を全て CharSeaquence 型にするのは現実的だとは思わない。
これはそもそもなぜ抽象に依存すべきなのかを考えれば明確である。 抽象に依存するべきである理由はいくつかあるが、 (1) プログラムは変わりやすいものよりも変わりにくいものに依存するべきだから (2) テストをするときにモックアップしやすいから (3) オブジェクトの利用者は利用するオブジェクトの実装の詳細を知るべきではないから といったような理由が多い気がする。
抽象化しなくても良いこともある
今回はアプリのドメインモデルを成すエンティティ(ドメインエンティティ)のインスタンスの生成を抽象化すべきか?というのが問題である。 結論からいえば、単純な値オブジェクトのようなエンティティであれば、抽象化する必要はないと思う。 理由は単純で、そもそもドメインエンティティは変更されにくいはずである。ドメインエンティティに対する変更は、すなわちそのアプリケーションの仕様変更であると言える。そのような仕様変更があるのだとすれば、仮に抽象化していたところでインタフェースに変更が加えられ、大規模な変更が生じることに違いはないと思う。また、単純な振る舞いしか持たないのであればテストする際にモックアップする必要もないし、実装が複数存在することもない。
まとめ
抽象化する必要のないようなクラスであれば、 new しても良い。そもそも抽象化していないクラスを Factory にしたところで Factory#create()
メソッドの戻り値はその具象クラスになってしまうので、何の意味もない。
Factory や Repository を使うべきか?という問いの前に、そのクラスは抽象化すべきか?を考えれば良さそう。
もしその答えが No ならば、 Factory や Repository を使うべきかの答えも No である。