勉強日記

チラ裏

PoEAA ch11 Lazy Load

martinfowler.com


Lazy Load

An object that doesn't contain all of the data you need but knows how to get it.

  • DBからメモリ上のオブジェクトにデータをロードする時、関連するオブジェクトも一緒にロードするように設計すると便利
    • クライアントコードで明示的にロードしなくて済む
      • 【補】サービス層とか
  • 芋づる式にオブジェクトグラフ全部ロードするのはパフォーマンスに悪影響
  • そこで読み込みを遅延させる
    • 読み込む必要がなかったとき得する

How It Works

種類

  • 主要な実装は4つある
    • Lazy Initialization
    • Virtual Proxy
    • Value Holder
    • Ghost

Lazy Initialization

  • Kent Beckのパターン本に書いてあるらしい?
  • get時、フィールドがnullかどうかチェックする
    • nullなら算出してセットしてから返す
  • nullが有意な値の場合は、Special Caseを適用せよ
    • 【補】UnknownのnullとNot Applicableのnullがある
      • 後者には例えばNull Object Pattern等を適用せよ、ってこと
  • DBのスキーマに依存しやすい
    • Active Record, Table Data Gateway, Row Data Gatewayではうまくいく
    • Data Mapper採用時はうまくいかない

Virtual Proxy

  • Data Mapper採用時はLazy Initializationがうまくいかないので、この間接層を使う
  • ロード対象のオブジェクトをVirtual Proxyでくるむ
  • get時はVirtual Proxyを返す
  • Virtual Proxyのプロパティの初回アクセス時に読み込みを行う
  • 利点
    • ロード対象のオブジェクトそのものを返しているように見える
      • 読み込み処理がVirtual Proxyの中でカプセル化されている
      • Domain ModelはLazy Loadの存在を知らない
  • 欠点
    • 組み込みの等価判定でコケる
    • 静的型付け言語では、Lazy Load対象のオブジェクトのクラスの数だけVirtual Proxyを作らなければならない
  • コレクションに対してはこれらの欠点はあてはまらない
    • 組み込みの等価判定でコケる
      • コレクションは元々そう
    • クラスの数だけVirtual Proxyを作らなければならない
      • コレクションに対して1つ作れば良い

Value Holder

  • Virtual Proxyのラップ対象をObjectに一般化した感じのもの

Ghost

  • どういうやつ
    • 一度に全フィールドをLazy LoadするLazy Initialization
    • 自分自身のVirtual Proxy
  • ロードステートを持たせる
    • GHOSTなら、プロパティアクセス時にロードを行う
    • LOADEDなら、ロード済のフィールドを返す

  • ripple loading
    • 余計なDBアクセスが生じてしまう
    • 【補】N+1問題みたいなイメージ
    • Lazy Loadのコレクションを持つのではなく、コレクション自体をLazy Loadせよ
      • コレクションが巨大だと適用不可能
      • が、普通そんなケースはない
      • 無くはない
        • その場合はValue List Handlerを適用せよ
  • AOPを適用してgetterにLazy Load処理を差し挟むとよい
    • Domain Objectから切り離して透過的にできる
  • ユースケースにより異なるlazinessが必要なケースではどうする?
    • Data Mapper採用時は、lazinessごとにData Mapperを分ける
      • Eager LoadするMapper
      • Lazy LoadするMapper
    • アプリケーションコード(サービス層とか)側で注入し分ける
    • 何種類も作る必要は普通ない

When to Use It

  • DBから取得するデータの量と、アクセス回数による
    • 読み込み量を減らすよりは回数を減らせ
      • Serialized LOB等、データが大きい場合でさえも、余計なカラムを読み込むコストは大したこと無い
  • 複雑性を増すため、必要のない場合は使わないのが良い

Example: Lazy Initialization

class Supplier...

    public List getProducts() {
        if (products == null) products = Product.findForSupplier(getId());
        return products;
    }

Example: Virutal Proxy

  • 本物のクラスに見えるプロキシで包むのがキモ
  • pp.204-205のコードを一部改変・コメント付した
class Supplier...

    /** ProductsのリストのVirtual Proxy */
    private VirtualList products;
/**
 * コレクション読み込みインタフェース
 * 具体的に何のクラスのオブジェクトをどのMapperで読み込むかは実装クラスで決める
 */
public interface VirutalListLoader {
    List load();
}

/**
 * Productのコレクションを読み込むクラス
 * ProductMapperに読み込みを委譲
 */
public static class ProductLoader implements VirtualListLoader {
    private Long id;
    public ProductLoader(Long id) {
        this.id = id;
    }
    public list Load() {
        return ProductMapper.create().findForSupplier(id);
    }
}
/**
 * SupplierのMapper
 * Supplier構築時にProductのコレクションのLazy Loadも仕込む
 */
class SupplierMapper...

    protected DomainObject doLoad(Long id, ResultSet rs) throws SQLException {
        String nameArg = rs.getString(2);
        Supplier result = new Supplier(id, nameArg);
        result.setProduct(new VirtualList(new ProductLoader(id)));
        return result;
    }
/**
 * ListのVirtual Proxy
 */
class VirtualList...
{
    /** コレクション実体 */
    private List source;
    
    /** データ読み込みクラスをコンストラクタ注入 */
    private VirtualListLoader loader;
    public VirtualList(VirtualListLoader loader) {
        this.loader = loader;
    }
    
    /**
     * Lazy Load実部
     */
    private List getSource() {
        if (source == null) source = loader.load();
        return source;
    }
    
    /**
     * Listが持っているメソッドぜんぶ委譲
     */
    public int size() {
        return getSource().size();
    }
    
    public boolean isEmpty() {
        return getSOurce().isEmpty();
    }
    
    ...
}
  • Domain Modelオブジェクトは、単にVirtualListを返せばいい
    • Lazy Loadのことを知らない

Example: Using a Value Holder (Java)

class Supplier...

    private ValueHolder products;
    public List getProducts() {
        // ValueHolder.getValue()はObjectを返すので
        // ここでダウンキャストして返す
        return (List) products.getValue();
    }
/**
 * ObjectのVirtual Proxy
 */
class ValueHolder...
{
    /** オブジェクト実体 */
    private Object value;
    
    /** データ読み込みクラスをコンストラクタ注入 */
    private ValueLoader loader;
    public VirtualList(ValueLoader loader) {
        this.loader = loader;
    }
    
    /**
     * Lazy Load実部
     */
    private List getValue() {
        if (value == null) value = loader.load();
        return value;
    }
  • Domain ModelはLazy Loadの存在を意識する必要がある
    • getterの中でValueHolderから値を取り出す必要がある
      • 汎用のObjectが返るためダウンキャストが必要

Example: Using Ghosts (C#)

  • Domain ModelのLayer Supertypeに実装すると良い
class Employee
    public String Name {
        get {
            Load();
            return _name;
        }
        set {
            Load();
            _name = value;
        }
    }
    String _name;

class DomainObject...

    protected void Load() {
        if (IsGhost)
            DataSource.Load(this);
    }
  • 各getter/setterにLoad()を書くのがかったるい
    • AOPを適用すると良い
  • DataSource.Load(DomainObject obj)で適当なMapperに処理を委譲し、データの読み込み・フィールドのセットを行う
    • ドメイン層をデータソース層に依存させたくないので、Separated Interfaceを適用する
  • Mapper.Find(Long id)では、Ghost Stateのオブジェクトを返す
  • Mapper.Load(Object obj)の中身は?
    • スカラ型: 単にsetするだけ
    • オブジェクト: 対応するMapperのFindでGhostをsetする
      • 再帰的なGhostパターンに成る
    • コレクション: 複雑
      • pp.211-214
      • 必要なレコードを1クエリで読み込みたい
      • ListLoaderにSQLクエリをセットする

英語

  • in one fell swoop
    • 一気に