読者です 読者をやめる 読者になる 読者になる

@numa08 猫耳帽子の女の子

明日目が覚めたら俺達の業界が夢のような世界になっているとイイナ。

Realm は抽象化をしない方が良いのかなってポエム

f:id:numanuma08:20160127214419j:plain

先日、年末から進めていたAndroid版featherのリファクタリングが終わりました。リファクタリングはRealmを利用する部分についてなのですが、ちょっと紆余曲折が長かったので自戒の意味を込めてポエムを書きます。

Realmを使うなら変な抽象化はしないほうが良いと思った

Androidに限らず、OOPな設計ってコントローラとかのレイヤーにはインターフェースのみを提供し、実装はコントローラから完全に分離をすることで、お互いに変更があっても影響をあんまり受けない仕組みを提供するのが基本だと思う。

ここでfeatherはDBMSにRealmを使っているけれど、開発初期のころはもしかしたらRealmを利用しない可能性があった。そこで、データベースへのアクセスを行うインターフェースを作って、Realmを隠蔽、Realmを使わない実装になっても大丈夫な設計を目指していた。

実はAndroidは ContentProvider を使えばDBとかのデータへアクセスする部分を抽象化できる・・・ということになっている。だけど実際は ContentProvider を自前で実装するのは面倒くさいし、クエリーも独自のインターフェースを利用した物なので大抵のORMとの相性は悪いっぽい。普通にSQLiteを利用する時や、アプリを超えてデータを提供するなら有りだけど、そうでないならあんまり使うメリットは無いと思ってる。

そんなわけでRealmの抽象化は自前のインターフェースを利用することにしてた。

コードの雰囲気は、こんな感じ

public interface TweetRepository {

  // ネットワーク、あるいはキャッシュから最新の
  // ツイートを取得するやつ
  // 非同期なのでコールバックを取る
  void getLatestTweets(List<Tweet> -> void callback); 

}

あら良いじゃない。これはポエムなので、このコードはコンパイルできないのだけど、雰囲気は分かって欲しい。

当時の俺の気分としては、ツイートの取得とかをネットワークからやってるのか、DBからやっているのかを意識させたくなかった。そうすれば、テストもし易いしキャッシュポリシーとかも変えられるしね。ところが、Realm はそういうのを許さない制約があった。「スレッドを超えられない」と「close した Realm から取り出したRealmObjectにはアクセスができない」と言う物。

Realm は closable なクラスなので、雰囲気としてはアクセスをしたらシュッとcloseをしたい。つまり、

List<Tweet> tweets;
try(Realm realm = Realm.getInstance(config)) {
  tweets = realm.allObject(Tweet.class);
}
tweets.get(0); // crash

上のコードは、アクセス違反を犯しているのでクラッシュする。一度 close をした Realm から取り出したデータへのアクセスは許可されないのだ。と、なると取り出した RealmObject を画面に描画するときは、 Realm のインスタンスをActivityやFragmentのライフサイクルに合わせて管理する必要があるわけだ。

あと、スレッドを超えることができない制約もまた辛い。という訳で、さっきの TweetRepositoryインターフェースを修正してみる。

public interface TweetRepository {

 void getLatestTwwt(List<Tweet> -> void callback);

 // これがActivity/Fragment の onResume から呼ばれることを期待する
 // この辺りで Realm のインスタンスが作られると思う
 void resume();

 // これがActivity/Fragment の onPause から呼ばれることを期待する
 // ここで Realm.close が呼ばれる
 void pause();
}

おお・・・まじか・・・。この時点で自分はRealmの抽象化を辞めようと思った。Realmを隠蔽しているはずなのに、resumepauseをコントローラから呼び出さなければならない制約があるため、抽象化ができていると言えないと思ったからだ。

今にして思えば、finalizeでRealm.closeするのはアリなのかな?とも思うけど・・・。

あと、TweetRepositoryを使う時は「Realmをスレッドを超えて使ってはならない」という制約を意識して使う必要がある。当然ながら、そんな制約を意識する以上Realmを抽象化できているとは言えないと思う・・・。

そんなわけで、Realmを抽象化するのをやめた。良い判断だったと思う。

ここからが反省点

Realmの抽象化を辞めようと決意したのが、昨年の8月か9月くらいだったと思う。完全にRealmを抽象化するのはやめて、直に使っていこう、そう思った。

そう思ったのにもかかわらず、変なインターフェースを残してしまった。

public interface DataManager extends Closable {

  Realm getReam();

}

public class TweetRepository {

  List<Tweet> getLatestTweet(DataManager manager) {
    return manager.getRealm().allObject(Tweet.class);
  }

}

どうしてこうなったのか・・・。何のためにDataManagerなるインターフェースを用意したのか分からない。実際、これのお陰で冗長で意図のよく分からないコードが増えた。

秋くらいから @mironalと一緒に開発をする体勢を作ったときに、「え?なにこれ??」ってなったわけ。

で、リファクタリングをやろうねって話になって年末の冬休みに入る前に一気にリファクタリング、年があけてからコードレビューとマージを行った。

まとめ

なんというか「Realmが」とかと言うより「ちゃんとレビューをしようね」とか「ちゃんとドキュメント読んだり、事前調査をしてから使えよ」って話だなぁ。

とは言え、Realmが若干トリッキーなのは否めない・・・?。

ただ、今ならRealmもRxAndroidに対応をしているので、Retrofitと組み合わせて「マジでネットワークとDBアクセスを意識しなくていいインターフェース」を作れる気もする。

良い感じのやつができたら、Qiitaに書こう・・・。