ひさしぶりの PoEAA、今日は Lazy Load がテーマです。
Lazy Load、その和訳には "遅延ロード" っていうのが挙げられることが多いんですが、必要な全部のデータを最初から取得するのではなくて、本当に必要になるときまでその取得を待つっていうオブジェクトになります。PoEAA 的にはこの「オブジェクトである」っていうところが特徴的かもしれません。普通は、オブジェクトに限らず、そういう技巧のことを指すのが一般的なんじゃないかしら。
実装方法
PoEAA で述べられている Lazy Load の実装パターンは以下の 4 つになります。
- Lazy initialization
- Virtual proxy
- Value holder
- Ghost
1. Lazy initialization
Lazy initizalization は一番単純な実装パターンで、最初に遅延ロード対象のフィールドが null か否かを確認し、
- null であれば、実際の値を計算してキャッシュした上で、その値を返却する
- null でなければ、キャッシュしている値をそのまま返却する
っていうことを実施します。
public List getProducts() { if (products == null) { products = Products.findForSupplier(getId()); } return products; }
これを使うためには、対象フィールドが self-encapsulated であることが必要です。言い変えると、自分自身がそのフィールドを参照するときも、上記のような処理を行うメソッド経由で使いましょうね、ということですね。getter 経由とか。
2. Virtual proxy
Virtual proxy は、あたかも遅延ロード対象のフィールドのように振る舞うオブジェクトで、実際にそのフィールドの値が virtual proxy に対して要求されたときに初めて、その値の取得を行うというものになります。 Lazy initialization は単純な実装となるが故に、「実際の値を計算」するロジックをそのまま記述することになりますが、Virtual proxy はそのロジックを抽象化するレイヤとして機能するので、テーブルへの依存を隠すことができる等、Data Mapper などとの併用に向いています。 一方、その課題としては、実際のオブジェクトと Virtual proxy のオブジェクトが論理的に同じ値を持っているとして、その同一性をどうやって判断させるのか (equals の実装をどうする?) という問題が発生することです。
public interface VirtualListLoader { List load(); } // これが Virtual Proxy の例 class VirtualList { private List source; private VirtualListLoader loader; public VirtualList(VirtualListLoader loader) { this.loader = loader; } private List getSource() { if (source == null) { source = loader.load(); } return source; } public int size() { return getSource().size(); } ... }
3. Value holder
Value holder は、遅延ロード対象のフィールドを wrap するクラスです。こいつの概念がかなり紛らわしいので、wikipedia:Lazy loading からサンプルを引用します。
private ValueHolder<Widget> valueHolder; public Widget MyWidget { get { return valueHolder.GetValue(); } }
4. Ghost
Ghost は、一部の情報(テーブルの主キー値とか)だけを保持しておき、実際の値が必要になった場合はその情報をキーにして当該の値を取得するというオブジェクトです。
難しさ
Hibernate あたりでも発生するのですが、例えば外部キーを張り巡らせたデータベースにおいて、1 つのテーブルの情報を取得すると、外部キーを経由して他のテーブルの情報も取得する、というケースがあります。 こういったときに、オブジェクトグラフのどこまでを遅延ロードで取得させるのかという設計がたいへん煩雑だと思います。適切なオブジェクトグラフはユースケース毎に異なるので。