C#と諸々

C#がメインで他もまぁ諸々なブログです
おかしなこと書いてたら指摘してくれると嬉しいです(´・∀・`)
つーかコメント欲しい(´・ω・`)

2011/04/06 10:56
DDD ではエンティティの構築をエンティティ自身が知っているのは不自然ということでファクトリの導入が推奨されているけれど、ファクトリよりビルダの方が向いているんじゃないかなぁ?
ファクトリだと、集約ルートの生成とそれ以外のエンティティの生成はアトミックに扱えないけどビルダならアトミックに扱えますし、構築中は不正な状態も扱えます。
また、構築に関するロジックがエンティティに一切なくなります。

ビルダは特に、UI への入力直後に入力チェックする場合や、ウィザード形式で入力を進めていく場合なんかに本領発揮します。

Person.cs
public sealed partial class Person
{
    private Person(string name, IList<Address> addresses)
    {
        _name = name;
        _addresses = addresses;
    }

    private string _name;

    private readonly IList<Address> _addresses;

    public string Name
    {
        get { return _name; }
    }

    public IEnumerable<Address> Addresses
    {
        get { return _addresses; }
    }
}

Address.cs
public sealed class partial Address
{
    ...
}

Person.Builder.cs
partial class Person
{
    public sealed class Builder
    {
        [Flags]
        public enum States
        {
            Valid = 0,
            NameIsEmpty = 1,
            NameIsTooLong = 2,
            AddressListIsEmpty = 4,
            AddressListHasError = 8,
        }


        public static readonly int MaxNameLength = 20;


        public Builder()
        {
            Name = null;
            AddressBuilders = new List<Address.Builder>();
        }

        public Builder(Person target)
        {
            if (target == null)
            {
                throw new ArgumentNullException("target");
            }
            Name = target.Name;
            AddressBuilders = target._addresses.Select(address => new Address.Builder(address)).ToList();
        }


        private readonly Person _target;

        public string Name;

        public readonly List<Address.Builder> AddressBuilders;


        public Address.Builder AddAddress()
        {
            var addressBuilder = new Address.Builder();
            AddressBuilders.Add(addressBuilder);
            return addressBuilder;
        }

        public void RemoveAddress(int index)
        {
            AddressBuilders.RemoveAt(index);
        }


        public States Validate()
        {
            return (ValidateName() | ValidateAddresses());
        }

        public States ValidateName()
        {
            if (string.IsNullOrWhitespace(Name))
            {
                return States.NameIsEmpty;
            }
            if (MaxNameLength < Name.Length)
            {
                return States.NameIsTooLong;
            }
            return States.Valid;
        }

        public States ValidateAddresses()
        {
            States result = States.Valid;
            if (AddressBuilders.Length == 0)
            {
                result |= States.AddressListIsEmpty;
            }
            foreach (var addressBuilder in AddressBuilders)
            {
                if (addressBuilder.Validate() != Address.Builder.States.Valid)
                {
                    result |= States.AddressListHasError;
                    break;
                }
            }
            return result;
        }


        public Person Build()
        {
            if (Validate() != States.Valid)
            {
                throw new InvalidOperationException();
            }

            var addresses = AddressBuilders.Select(builder => builder.Build()).ToList();

            if (_target == null)
            {
                return new Person(Name, addresses);
            }
            else
            {
                _target._name = Name;
                _target._addresses.Clear();
                foreach (var address in addresses)
                {
                    _target._addresses.Add(address);
                }
                return _target;
            }
        }
    }
}

Address.Builder.cs
partial class Address
{
    public sealed class Builder
    {
        [Flags]
        public enum States
        {
            ...
        }
        internal AddressBuilder() { ... }
        public States Validate() { ... }
        internal Address Build() { ... }
    }
}


まだ色々思案中でこれが本当に良い案なのかどうか確信は持ててないのですが。。。


[関連書籍]


ついにエリック・エヴァンスの "Domain-Driven Design" の日本語版が出版されますよ

スポンサーサイト



2010/06/06 23:55
.NET Framework 4.0 から、遅延初期化を行うためのクラスが新しく追加されました。

Lazy(T) クラス (System)

遅延初期化と聞いてすぐに連想されるのはレイジーロードパターンです。
以前、仮想プロキシによるレイジーロードについての記事を書きました。
今回、VirtualList<T> クラスの実装を、Lazy<T> クラスを使用するように変更してみます。

VirtualList<T> クラス
using System;
using System.Collections;
using System.Collections.Generic;

/// <summary>
/// リストの仮想プロキシです。
/// </summary>
/// <typeparam name="T"></typeparam>
[Serializable]
public sealed class VirtualList<T> : IList<T>
{
    #region Constructors

    /// <summary>
    /// VirtualList&lt;T> クラスの新しいインスタンスを初期化します。
    /// </summary>
    /// <param name="loader">ローダー。</param>
    /// <param name="isThreadSafeWhenLoading">
    /// このインスタンスの遅延初期化処理をスレッドセーフにする場合は true。
    /// このインスタンスの遅延初期化処理をスレッドセーフにしない場合は false。
    /// </param>
    /// <exception cref="System.ArgumentNullException">引数 loader が null です。</exception>
    public VirtualList(IListLoader<T> loader, bool isThreadSafeWhenLoading = true)
        : this(loader.Load, isThreadSafeWhenLoading)
    {
    }

    /// <summary>
    /// VirtualList&lt;T> クラスの新しいインスタンスを初期化します。
    /// </summary>
    /// <param name="loader">ローダー。</param>
    /// <param name="isThreadSafeWhenLoading">
    /// このインスタンスの遅延初期化処理をスレッドセーフにする場合は true。
    /// このインスタンスの遅延初期化処理をスレッドセーフにしない場合は false。
    /// </param>
    /// <exception cref="System.ArgumentNullException">引数 loader が null です。</exception>
    public VirtualList(Func<IList<T>> loader, bool isThreadSafeWhenLoading = true)
    {
        if (loader == null)
        {
            throw new ArgumentNullException("loader");
        }
        this._lazyLoader = new Lazy<IList<T>>(loader, isThreadSafeWhenLoading);
    }

    #endregion

    #region Fields

    /// <summary>
    /// レイジーローダーを取得します。
    /// </summary>
    private readonly Lazy<IList<T>> _lazyLoader;

    #endregion

    #region Properties

    /// <summary>
    /// ソースリストを取得します。
    /// </summary>
    private IList<T> Items
    {
        get
        {
            return this._lazyLoader.Value;
        }
    }

    #endregion

    #region IList<T> メンバ

    /// <summary>
    /// 指定したインデックスにある要素を取得または設定します。
    /// </summary>
    /// <param name="index">取得または設定する要素の、0 から始まるインデックス番号。</param>
    /// <returns>指定したインデックスにある要素。</returns>
    /// <exception cref="System.NotSupportedException">このプロパティが設定されていますが、VirtualList&lt;T> が読み取り専用です。</exception>
    /// <exception cref="System.ArgumentOutOfRangeException">index が VirtualList&lt;T>  の有効なインデックスではありません。</exception>
    public T this[int index]
    {
        get
        {
            return this.Items[index];
        }
        set
        {
            this.Items[index] = value;
        }
    }

    /// <summary>
    /// VirtualList&lt;T> 内での指定した項目のインデックスを調べます。
    /// </summary>
    /// <param name="item">IList&lt;T> 内で検索するオブジェクト。</param>
    /// <returns>リストに存在する場合は item のインデックス。それ以外の場合は -1。</returns>
    public int IndexOf(T item)
    {
        return this.Items.IndexOf(item);
    }

    /// <summary>
    /// VirtualList&lt;T> の指定したインデックス位置に項目を挿入します。
    /// </summary>
    /// <param name="index">VirtualList&lt;T> に挿入するオブジェクト。</param>
    /// <param name="item">item を挿入する位置の、0 から始まるインデックス番号。</param>
    /// <exception cref="System.NotSupportedException">VirtualList&lt;T> は読み取り専用です。</exception>
    /// <exception cref="System.ArgumentOutOfRangeException">index が VirtualList&lt;T> の有効なインデックスではありません。</exception>
    public void Insert(int index, T item)
    {
        this.Items.Insert(index, item);
    }

    /// <summary>
    /// 指定したインデックス位置の VirtualList&lt;T> 項目を削除します。
    /// </summary>
    /// <param name="index">削除する項目の 0 から始まるインデックス。</param>
    /// <exception cref="System.NotSupportedException">VirtualList&lt;T> は読み取り専用です。</exception>
    /// <exception cref="System.ArgumentOutOfRangeException">index が VirtualList&lt;T> の有効なインデックスではありません。</exception>
    public void RemoveAt(int index)
    {
        this.Items.RemoveAt(index);
    }

    #endregion

    #region ICollection<T> メンバ

    /// <summary>
    /// VirtualList&lt;T> に格納されている要素の数を取得します。
    /// </summary>
    public int Count
    {
        get
        {
            return this.Items.Count;
        }
    }

    /// <summary>
    /// VirtualList&lt;T> が読み取り専用かどうかを示す値を取得します。
    /// </summary>
    public bool IsReadOnly
    {
        get
        {
            return this.Items.IsReadOnly;
        }
    }

    /// <summary>
    /// VirtualList&lt;T> に項目を追加します。
    /// </summary>
    /// <param name="item">VirtualList&lt;T> に追加するオブジェクト。</param>
    /// <exception cref="System.NotSupportedException">VirtualList&lt;T> は読み取り専用です。</exception>
    public void Add(T item)
    {
        this.Items.Add(item);
    }

    /// <summary>
    /// VirtualList&lt;T> からすべての項目を削除します。
    /// </summary>
    /// <exception cref="System.NotSupportedException">VirtualList&lt;T> は読み取り専用です。</exception>
    public void Clear()
    {
        this.Items.Clear();
    }

    /// <summary>
    /// VirtualList&lt;T> に特定の値が格納されているかどうかを判断します。
    /// </summary>
    /// <param name="item">VirtualList&lt;T> 内で検索するオブジェクト。</param>
    /// <returns>item が VirtualList&lt;T> に存在する場合は true。それ以外の場合は false。</returns>
    public bool Contains(T item)
    {
        return this.Items.Contains(item);
    }

    /// <summary>
    /// VirtualList&lt;T> の要素を System.Array にコピーします。System.Array の特定のインデックスからコピーが開始されます。
    /// </summary>
    /// <param name="array">VirtualList&lt;T> から要素がコピーされる 1 次元の System.Array。System.Array には、0 から始まるインデックス番号が必要です。</param>
    /// <param name="arrayIndex">コピーの開始位置となる、array の 0 から始まるインデックス番号。</param>
    /// <exception cref="System.ArgumentException">
    /// array が多次元です。
    /// またはarrayIndex が array の長さ以上です。
    /// またはコピー元の VirtualList&lt;T> の要素数が、arrayIndex からコピー先の array の末尾までに格納できる数を超えています。
    /// または型 T をコピー先の array の型に自動的にキャストすることはできません。
    /// </exception>
    /// <exception cref="System.ArgumentNullException">array が null です。</exception>
    /// <exception cref="System.ArgumentOutOfRangeException">arrayIndex が 0 未満です。</exception>
    public void CopyTo(T[] array, int arrayIndex)
    {
        this.Items.CopyTo(array, arrayIndex);
    }

    /// <summary>
    /// VirtualList&lt;T> 内で最初に見つかった特定のオブジェクトを削除します。
    /// </summary>
    /// <param name="item">VirtualList&lt;T> から削除するオブジェクト。</param>
    /// <returns>
    /// item が VirtualList&lt;T> から正常に削除された場合は true。それ以外の場合は false。
    /// このメソッドは、item が元の VirtualList&lt;T> に見つからない場合にも false を返します。
    /// </returns>
    /// <exception cref="System.NotSupportedException">VirtualList&lt;T> は読み取り専用です。</exception>
    public bool Remove(T item)
    {
        return this.Items.Remove(item);
    }

    #endregion

    #region IEnumerable<T> メンバ

    /// <summary>
    /// コレクションを反復処理する列挙子を返します。
    /// </summary>
    /// <returns>コレクションを反復処理するために使用できる System.Collections.Generic.IEnumerator&lt;T>。</returns>
    public IEnumerator<T> GetEnumerator()
    {
        return this.Items.GetEnumerator();
    }

    #endregion

    #region IEnumerable メンバ

    /// <summary>
    /// コレクションを反復処理する列挙子を返します。
    /// </summary>
    /// <returns>コレクションを反復処理するために使用できる System.Collections.IEnumerator オブジェクト。</returns>
    IEnumerator IEnumerable.GetEnumerator()
    {
        return ((System.Collections.IEnumerable)this.Items).GetEnumerator();
    }

    #endregion
}



IListLoader<T> インターフェイス
using System;
using System.Collections.Generic;

/// <summary>
/// リストのローダーです。
/// </summary>
/// <typeparam name="T"></typeparam>
public interface IListLoader<T>
{
    /// <summary>
    /// リストをロードします。
    /// </summary>
    /// <returns>リスト。</returns>
    IList<T> Load();
}



VirtualList<T> クラスに Lazy<T> クラスを導入したことで、状態管理が不要になりました。
そして、遅延初期化処理をスレッドセーフにすることができるようになりました。

以前の実装のように、Lazy<T> クラスを使わずとも遅延初期化処理を実装することは容易です。
しかし、 Lazy<T> クラスを使用すれば、遅延初期化処理のスレッドセーフも容易に保証できるようになります。

2009/05/26 13:03

こだかたろうです : New EntityFramework

不信任投票で挙がっていた点がかなり改善されそうですね。
レイジーロードがサポートされることはなおきさんから聞いていましたが、まさか PI もサポートされるとは。
気になるのは PI レベルがどのくらいなのかですね。
How to: Define Custom Persistence-Ignorant Objects (Entity Framework) を見る限り POCO は余裕でクリアしてますが、Requirements for Using Persistence-Ignorant Objects (Entity Framework) を見るとやはりいくつか制約があります。

以下、英語苦手なのできっと間違ってる所があります。(つか、自分で意味がよくわかってない所もあるので…。誰かつっこみお願いします T-T)
  • カスタムデータクラスは public でなければならない。
  • カスタムデータクラスは sealed にできない。
  • カスタムデータクラスは abstract にできない。
  • カスタムデータクラスは IEntityWithChangeTracker や IEntityWithRelationships を実装できない。
  • 概念モデル内のエンティティ型のプロパティにマッピングされた各プロパティは public でなければならない。
  • カスタムデータクラスはデフォルトコンストラクタを提供しなければならない。
  • 概念モデル内で定義されたナビゲーションプロパティは、対応するナビゲーションプロパティがカスタムデータクラスになければならない。関連オブジェクトの遅延ロードをサポートするためには、このプロパティを virtual として宣言する必要があり、sealed と宣言することはできない。
  • 一対多または多対多の関連を表すナビゲーションプロパティは、ICollection<T> (T は関連端の型) を実装する型を返さなければならない。
  • データモデル内のエンティティ型のプロパティにマッピングされたプロパティは、virtual で、getter 及び setter を実装しなければならない。
  • 概念モデル内の複合プロパティは、参照型を返すカスタムデータクラスのプロパティにマッピングしなければならない。複合プロパティを、値型や列挙型にマッピングすることはできない。
NHibernate に比べると若干 PI レベルが低そうですが、許容範囲内ですかねー。
2009/05/20 01:55
Castle Project の ActiveRecord をちょっと触ってみた。
NHibernate をベースに作られていて、至る所で NHibernate の知識を求められる。というか、NHibernate の基本がわかっていれば簡単に使えるけれど、わかっていなきゃ厳しいという感じ。

気になるのは、バージョンアップが久しくされていないっぽいこと。当然、使用されている NHibernate のバージョンも 1.2 と古い。ただ、SVN リポジトリの方をちらっと覗いてみたら、ファイル更新日時がかなり最近の日付になっていたので、開発自体は継続しているようだ。
2008/06/19 00:54
データマッパー (PofEAA) がドメインオブジェクトをロードする際、リッチコンストラクタを用いることで最初から完成されたドメインオブジェクトを生成することができる。すると、ロードのためだけにセッターを提供したりする必要がないため、スマートなインターフェイスを実現できる。
しかし、循環参照を伴うドメインオブジェクトでは、リッチコンストラクタの使用に問題が生じる。
例えば、次の Hoge クラスと Fuga クラスのような場合だ。

class Hoge
{
    readonly Fuga _f;
    public Hoge(Fuga f)
    {
        if (f == null)
        {
            throw new ArgumentNullException("f");
        }
        this._f = f;
    }
}

class Fuga
{
    readonly Hoge _h;
    public Fuga(Hoge h)
    {
        if (h == null)
        {
            throw new ArgumentNullException("h");
        }
        this._h = h;
    }
}

Hoge のコンストラクタでは Fuga のインスタンスを必要とする。Fuga のコンストラクタでは Hoge のインスタンスを必要とする。これは通常、決してインスタンス化することができない。
しかし、.NET では、コンストラクタを呼び出さずにインスタンス化することができる。そして、リフレクションを使用すればコンストラクタを後から呼び出すこともできる。
つまり、次のようなコードでリッチコンストラクタの循環参照問題は克服できる。

// using System;
// using System.Runtime.Serialization;
// using System.Reflection;

Hoge h = (Hoge)FormatterServices.GetUninitializedObject(typeof(Hoge));
Fuga f = new Fuga(h);
ConstructorInfo ctor = typeof(Hoge).GetConstructor(new[] { typeof(Fuga) });
ctor.Invoke(h, new[] { f });

なお、コンストラクタを後から呼び出す代わりに、リフレクションで直接フィールドを設定するという方法も取れる。しかし、直接フィールドを設定する理由はないし、メリットもない。むしろ、循環参照の問題さえなければ普通にリッチコンストラクタでインスタンス化しているわけなのだから、リッチコンストラクタを使うべきだろう。
2007/10/19 22:12

Martin Fowler's Bliki in Japanese - ドメインモデル貧血症

有名なアンチパターンです。僕も昔やってしまったことがあります。

例えば、図書の貸出システムを作るとします。このシステムは利用者が直接操作し、借り入れ手続きや返却手続きを行います。

ドメインモデルを適切に適用した設計では、「書籍」クラスや「利用者」クラスを用意し、「利用者」クラスに「借りる」メソッドや「返すメソッド」を用意するでしょう。

public sealed class 書籍
{
    public int 管理番号 { get { ... } }
    public string タイトル { get { ... } }
    public string 著者 { get { ... } }
}

public sealed class 利用者
{
    public int 利用者ID { get { ... } }
    public ReadOnlyCollection<書籍> 借りてる書籍 { get { ... } }
    public void 借りる(書籍 対象書籍) { ... }
    public void 返す(書籍 対象書籍) { ... }
}


しかし、ドメインモデル貧血症に陥っている設計では、「利用者」クラスに「借りる」メソッドや「返す」メソッドがありません。代わりに、「借り入れ操作」クラスといった感じの、ロジックのみのクラスが存在します。

public sealed class 利用者
{
    public int 利用者ID { get { ... } }
    public ReadOnlyCollection<書籍> 借りてる書籍 { get { ... } }
}

public sealed class  借り入れ操作
{
    public void 借りる(利用者 対象者, 書籍 対象書籍) { ... }
    public void 返す(利用者 対象者, 書籍 対象書籍) { ... }
}


ドメインモデル貧血症では、データと処理が分離されてしまいます。これは決してオブジェクト指向ではありません。
また、この例では、「利用者」が「借りてる書籍」を、読み取り専用コレクションのプロパティとして公開しています。「借り入れ操作」クラスは、「借りてる書籍」をどうやって変更するのでしょうか。カプセル化を崩すか、プロパティの型を読み取り専用でなくするか、あるいは利用者クラスに補助的なメソッドを用意するか。どうするにしろ、複雑さが増してしまうかと思います。


オブジェクト指向じゃないことが悪いのではありません。オブジェクト指向を適用しているつもりで貧血症を起こしているからダメなのです。
2007/10/13 23:00
前回の 仮想プロキシによるレイジーロード で書いた仮想リストと仮想リストローダーの使用例です。

ここでは、Department ( 部署 ) クラス と Employee ( 社員 ) クラスを使った簡単な例を挙げます。
部署には ID, 名前, メンバーといったフィールドがあることにします。

public sealed class Department
{
    private readonly int _id;

    private readonly string _name;

    private readonly ReadOnlyCollection<Employee> _members;

    public int Id
    {
        get
        {
            return this._id;
        }
    }

    public string Name
    {
        get
        {
            return this._name;
        }
    }

    public ReadOnlyCollection<Employee> Members
    {
        get
        {
            return this._members;
        }
    }

    public Department(int id, string name, ReadOnlyCollection<Employee> members)
    {
        this._id = id;
        this._name = name;
        this._members = members;
    }
}


社員には ID, 名前といったフィールドがあることにします。

public sealed class Employee
{
    private readonly int _id;

    private readonly string _name;

    public int Id
    {
        get
        {
            return this._id;
        }
    }

    public string Name
    {
        get
        {
            return this._name;
        }
    }

    public Employee(int id, string name)
    {
        this._id = id;
        this._name = name;
    }
}


部署クラスの _members フィールドこそが、今回レイジーロードを行うことになるフィールドです。見ての通り、仮想プロキシによるレイジーロードでは、レイジーロードの為の特別な仕組みがドメインクラスに一切必要ありません。

DepartmentMapper は、仮想リストと仮想リストローダーを使用してレイジーロードを部署クラスに仕込みます。
IListLoader ジェネリックインターフェイスを実装した MembersLoader クラスはインナークラスとして定義してあります。この仮想リストローダーは EmployeeMapper クラス ( 今回コードは用意していませんが ) から部署メンバーを取得します。

public sealed class DepartmentMapper
{
    public Department Find(int id)
    {
        DepartmentsDataSet.DepartmentsRow row = this.FindDataRow(id);
        if (row == null)
        {
            return null;
        }

        string name = row.Name;
        MembersLoader membersLoader = new MembersLoader(id);
        VirtualList<Employee> membersVirtualList = new VirtualList<Employee>(membersLoader);
        ReadOnlyCollection<Employee> members = new ReadOnlyCollection<Employee>(membersVirtualList);

        return new Department(id, name, members);
    }

    private DepartmentsDataSet.DepartmentsRow FindDataRow(int id)
    {
        using (DepartmentsTableAdapter adapter = new DepartmentsTableAdapter())
        {
            DepartmentsDataSet.DepartmentsDataTable table = adapter.GetDataById(id);
            if (table.Count == 0)
            {
                return null;
            }
            return table[0];
        }
    }

    private sealed class MembersLoader : IListLoader<Employee>
    {
        private readonly int _id;

        public MembersLoader(int id)
        {
            this._id = id;
        }

        public IList<Employee> Load()
        {
            return EmployeeMapper.Singleton.FindByDepartment(this._id);
        }
    }
}


これで、部署クラスの Members プロパティは、実際に使用されるまでロードを行いません。
2007/10/08 21:38
仮想プロキシによるレイジーロード ( PofEAA ) を実装する際に利用できる、汎用的な仮想リスト及び仮想リストローダーを C# で書いてみました。 ( 特に変わったことはしてません。単に、コードを記録する目的で記事にしました。 )

仮想リストは、実装すべきインターフェイスが多いためコード量が多くなっていますが、重要なのは Items プロパティだけです。他のプロパティやメソッドは全て、Items プロパティから取得したリストに処理を委譲しているだけです。

VirtualList<T> クラス
using System;
using System.Collections;
using System.Collections.Generic;

/// <summary>
/// リストの仮想プロキシです。
/// </summary>
/// <typeparam name="T"></typeparam>
[Serializable]
public sealed class VirtualList<T> : IList<T>
{
    #region Constructors

    /// <summary>
    /// VirtualList&lt;T> クラスの新しいインスタンスを初期化します。
    /// </summary>
    /// <param name="loader">ローダー。</param>
    /// <exception cref="System.ArgumentNullException">引数 loader が null です。</exception>
    public VirtualList(IListLoader<T> loader)
    {
        if (loader == null)
        {
            throw new ArgumentNullException("loader");
        }
        this._items = null;
        this._loader = loader;
    }

    #endregion

    #region Fields

    /// <summary>
    /// ソースリストを取得または設定します。
    /// </summary>
    private IList<T> _items;

    /// <summary>
    /// ローダーを取得します。
    /// </summary>
    private readonly IListLoader<T> _loader;

    #endregion

    #region Properties

    /// <summary>
    /// ソースリストを取得します。
    /// </summary>
    private IList<T> Items
    {
        get
        {
            if (this._items == null)
            {
                this._items = this._loader.Load();
            }
            return this._items;
        }
    }

    #endregion

    #region IList<T> メンバ

    /// <summary>
    /// 指定したインデックスにある要素を取得または設定します。
    /// </summary>
    /// <param name="index">取得または設定する要素の、0 から始まるインデックス番号。</param>
    /// <returns>指定したインデックスにある要素。</returns>
    /// <exception cref="System.NotSupportedException">このプロパティが設定されていますが、VirtualList&lt;T> が読み取り専用です。</exception>
    /// <exception cref="System.ArgumentOutOfRangeException">index が VirtualList&lt;T>  の有効なインデックスではありません。</exception>
    public T this[int index]
    {
        get
        {
            return this.Items[index];
        }
        set
        {
            this.Items[index] = value;
        }
    }

    /// <summary>
    /// VirtualList&lt;T> 内での指定した項目のインデックスを調べます。
    /// </summary>
    /// <param name="item">IList&lt;T> 内で検索するオブジェクト。</param>
    /// <returns>リストに存在する場合は item のインデックス。それ以外の場合は -1。</returns>
    public int IndexOf(T item)
    {
        return this.Items.IndexOf(item);
    }

    /// <summary>
    /// VirtualList&lt;T> の指定したインデックス位置に項目を挿入します。
    /// </summary>
    /// <param name="index">VirtualList&lt;T> に挿入するオブジェクト。</param>
    /// <param name="item">item を挿入する位置の、0 から始まるインデックス番号。</param>
    /// <exception cref="System.NotSupportedException">VirtualList&lt;T> は読み取り専用です。</exception>
    /// <exception cref="System.ArgumentOutOfRangeException">index が VirtualList&lt;T> の有効なインデックスではありません。</exception>
    public void Insert(int index, T item)
    {
        this.Items.Insert(index, item);
    }

    /// <summary>
    /// 指定したインデックス位置の VirtualList&lt;T> 項目を削除します。
    /// </summary>
    /// <param name="index">削除する項目の 0 から始まるインデックス。</param>
    /// <exception cref="System.NotSupportedException">VirtualList&lt;T> は読み取り専用です。</exception>
    /// <exception cref="System.ArgumentOutOfRangeException">index が VirtualList&lt;T> の有効なインデックスではありません。</exception>
    public void RemoveAt(int index)
    {
        this.Items.RemoveAt(index);
    }

    #endregion

    #region ICollection<T> メンバ

    /// <summary>
    /// VirtualList&lt;T> に格納されている要素の数を取得します。
    /// </summary>
    public int Count
    {
        get
        {
            return this.Items.Count;
        }
    }

    /// <summary>
    /// VirtualList&lt;T> が読み取り専用かどうかを示す値を取得します。
    /// </summary>
    public bool IsReadOnly
    {
        get
        {
            return this.Items.IsReadOnly;
        }
    }

    /// <summary>
    /// VirtualList&lt;T> に項目を追加します。
    /// </summary>
    /// <param name="item">VirtualList&lt;T> に追加するオブジェクト。</param>
    /// <exception cref="System.NotSupportedException">VirtualList&lt;T> は読み取り専用です。</exception>
    public void Add(T item)
    {
        this.Items.Add(item);
    }

    /// <summary>
    /// VirtualList&lt;T> からすべての項目を削除します。
    /// </summary>
    /// <exception cref="System.NotSupportedException">VirtualList&lt;T> は読み取り専用です。</exception>
    public void Clear()
    {
        this.Items.Clear();
    }

    /// <summary>
    /// VirtualList&lt;T> に特定の値が格納されているかどうかを判断します。
    /// </summary>
    /// <param name="item">VirtualList&lt;T> 内で検索するオブジェクト。</param>
    /// <returns>item が VirtualList&lt;T> に存在する場合は true。それ以外の場合は false。</returns>
    public bool Contains(T item)
    {
        return this.Items.Contains(item);
    }

    /// <summary>
    /// VirtualList&lt;T> の要素を System.Array にコピーします。System.Array の特定のインデックスからコピーが開始されます。
    /// </summary>
    /// <param name="array">VirtualList&lt;T> から要素がコピーされる 1 次元の System.Array。System.Array には、0 から始まるインデックス番号が必要です。</param>
    /// <param name="arrayIndex">コピーの開始位置となる、array の 0 から始まるインデックス番号。</param>
    /// <exception cref="System.ArgumentException">
    /// array が多次元です。
    /// またはarrayIndex が array の長さ以上です。
    /// またはコピー元の VirtualList&lt;T> の要素数が、arrayIndex からコピー先の array の末尾までに格納できる数を超えています。
    /// または型 T をコピー先の array の型に自動的にキャストすることはできません。
    /// </exception>
    /// <exception cref="System.ArgumentNullException">array が null です。</exception>
    /// <exception cref="System.ArgumentOutOfRangeException">arrayIndex が 0 未満です。</exception>
    public void CopyTo(T[] array, int arrayIndex)
    {
        this.Items.CopyTo(array, arrayIndex);
    }

    /// <summary>
    /// VirtualList&lt;T> 内で最初に見つかった特定のオブジェクトを削除します。
    /// </summary>
    /// <param name="item">VirtualList&lt;T> から削除するオブジェクト。</param>
    /// <returns>
    /// item が VirtualList&lt;T> から正常に削除された場合は true。それ以外の場合は false。
    /// このメソッドは、item が元の VirtualList&lt;T> に見つからない場合にも false を返します。
    /// </returns>
    /// <exception cref="System.NotSupportedException">VirtualList&lt;T> は読み取り専用です。</exception>
    public bool Remove(T item)
    {
        return this.Items.Remove(item);
    }

    #endregion

    #region IEnumerable<T> メンバ

    /// <summary>
    /// コレクションを反復処理する列挙子を返します。
    /// </summary>
    /// <returns>コレクションを反復処理するために使用できる System.Collections.Generic.IEnumerator&lt;T>。</returns>
    public IEnumerator<T> GetEnumerator()
    {
        return this.Items.GetEnumerator();
    }

    #endregion

    #region IEnumerable メンバ

    /// <summary>
    /// コレクションを反復処理する列挙子を返します。
    /// </summary>
    /// <returns>コレクションを反復処理するために使用できる System.Collections.IEnumerator オブジェクト。</returns>
    IEnumerator IEnumerable.GetEnumerator()
    {
        return ((System.Collections.IEnumerable)this.Items).GetEnumerator();
    }

    #endregion
}


IListLoader<T> インターフェイス
using System;
using System.Collections.Generic;

/// <summary>
/// リストのローダーです。
/// </summary>
/// <typeparam name="T"></typeparam>
public interface IListLoader<T>
{
    /// <summary>
    /// リストをロードします。
    /// </summary>
    /// <returns>リスト。</returns>
    IList<T> Load();
}


この仮想リスト及びローダーは、そのまま使用できます。
仮想リストはシールクラスにしてあります。これは、仮想リストを継承したクラスを定義する必要がないはずだからです。null を許可しないとか重複を許可しないといった制約は、仮想リストには実装しません。


この仮想リスト及びローダーを使用したレイジーロードの実装例は次回にでも。


// 追記 (2010/06/06)
Lazy クラスを使用した仮想プロキシを実装してみました。