代わりに IEnumerable<T>、IList<T>、ICollection<T>、Collection<T> のいずれかを使う。
[ 理由 1 ]
List<T> はパフォーマンス向上のために使用するコレクションで、継承は想定されていない。一方、Collection<T> は、継承して独自のコレクションを定義することが可能な (想定されている) コレクション。
このため、外部に公開するプロパティや外部に公開するメソッドの戻り値・パラメータで List<T> を使用することは推奨されていない。
僕は 「List<T> は多態性を持てないから」 と勝手に解釈している。
[ 理由 2 ]
Collection<T> は、IList<T> を受け取るバージョンのコンストラクタを使用することで、コレクションのラッパーとして機能させることができる。すると、Collection<T> 型のフィールドには、仮想プロキシによるレイジーロードなど、別のコレクションを注入することができる。
# List<T> では、IEnumerable<T> を受け取るコンストラクタがあるが、これはコンストラクタ内で要素のコピーを行っており、ラッパーとしては働かない。
[ 注意点 ]
ひとつ気をつけなければならない点として、List<T> は独自の機能を豊富に提供していることが挙げられる。例えば Collection<T> のインスタンスで List<T> の機能を使いたい時は、List<T> に変換する (IEnumerable<T> を受け取るコンストラクタを使う) などしなければならない。
しかし、循環参照を伴うドメインオブジェクトでは、リッチコンストラクタの使用に問題が生じる。
例えば、次の 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 });
なお、コンストラクタを後から呼び出す代わりに、リフレクションで直接フィールドを設定するという方法も取れる。しかし、直接フィールドを設定する理由はないし、メリットもない。むしろ、循環参照の問題さえなければ普通にリッチコンストラクタでインスタンス化しているわけなのだから、リッチコンストラクタを使うべきだろう。
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 返す(利用者 対象者, 書籍 対象書籍) { ... }
}
ドメインモデル貧血症では、データと処理が分離されてしまいます。これは決してオブジェクト指向ではありません。
また、この例では、「利用者」が「借りてる書籍」を、読み取り専用コレクションのプロパティとして公開しています。「借り入れ操作」クラスは、「借りてる書籍」をどうやって変更するのでしょうか。カプセル化を崩すか、プロパティの型を読み取り専用でなくするか、あるいは利用者クラスに補助的なメソッドを用意するか。どうするにしろ、複雑さが増してしまうかと思います。
オブジェクト指向じゃないことが悪いのではありません。オブジェクト指向を適用しているつもりで貧血症を起こしているからダメなのです。
ここでは、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 は、仮想リストと仮想リストローダーを使用してレイジーロードを部署クラスに仕込みます。
IVirtualListLoader ジェネリックインターフェイスを実装した 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 : IVirtualListLoader<Employee>
{
private readonly int _id;
public MembersLoader(int id)
{
this._id = id;
}
public IList<Employee> Load()
{
return EmployeeMapper.Singleton.FindByDepartment(this._id);
}
}
}
これで、部署クラスの Members プロパティは、実際に使用されるまでロードを行いません。
仮想リストは、実装すべきインターフェイスが多いためコード量が多くなっていますが、重要なのは 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 Fields
#region private IList<T> _items
/// <summary>
/// ソースリストを取得または設定します。
/// </summary>
private IList<T> _items;
#endregion
#region private readonly IVirtualListLoader<T> _loader
/// <summary>
/// ローダーを取得します。
/// </summary>
private readonly IVirtualListLoader<T> _loader;
#endregion
#endregion
#region Properties
#region private IList<T> Items
/// <summary>
/// ソースリストを取得します。
/// </summary>
private IList<T> Items
{
get
{
if (this._items == null)
{
this._items = this._loader.Load();
}
return this._items;
}
}
#endregion
#endregion
#region Constructors
#region public VirtualList(IVirtualListLoader<T> loader)
/// <summary>
/// VirtualList<T> クラスの新しいインスタンスを初期化します。
/// </summary>
/// <param name="loader">ローダー。</param>
/// <exception cref="System.ArgumentNullException">引数 loader が null です。</exception>
public VirtualList(IVirtualListLoader<T> loader)
{
if (loader == null)
{
throw new ArgumentNullException("loader");
}
this._items = null;
this._loader = loader;
}
#endregion
#endregion
#region IList<T> メンバ
#region public T this[int index]
/// <summary>
/// 指定したインデックスにある要素を取得または設定します。
/// </summary>
/// <param name="index">取得または設定する要素の、0 から始まるインデックス番号。</param>
/// <returns>指定したインデックスにある要素。</returns>
/// <exception cref="System.NotSupportedException">このプロパティが設定されていますが、VirtualList<T> が読み取り専用です。</exception>
/// <exception cref="System.ArgumentOutOfRangeException">index が VirtualList<T> の有効なインデックスではありません。</exception>
public T this[int index]
{
get
{
return this.Items[index];
}
set
{
this.Items[index] = value;
}
}
#endregion
#region public int IndexOf(T item)
/// <summary>
/// VirtualList<T> 内での指定した項目のインデックスを調べます。
/// </summary>
/// <param name="item">IList<T> 内で検索するオブジェクト。</param>
/// <returns>リストに存在する場合は item のインデックス。それ以外の場合は -1。</returns>
public int IndexOf(T item)
{
return this.Items.IndexOf(item);
}
#endregion
#region public void Insert(int index, T item)
/// <summary>
/// VirtualList<T> の指定したインデックス位置に項目を挿入します。
/// </summary>
/// <param name="index">VirtualList<T> に挿入するオブジェクト。</param>
/// <param name="item">item を挿入する位置の、0 から始まるインデックス番号。</param>
/// <exception cref="System.NotSupportedException">VirtualList<T> は読み取り専用です。</exception>
/// <exception cref="System.ArgumentOutOfRangeException">index が VirtualList<T> の有効なインデックスではありません。</exception>
public void Insert(int index, T item)
{
this.Items.Insert(index, item);
}
#endregion
#region public void RemoveAt(int index)
/// <summary>
/// 指定したインデックス位置の VirtualList<T> 項目を削除します。
/// </summary>
/// <param name="index">削除する項目の 0 から始まるインデックス。</param>
/// <exception cref="System.NotSupportedException">VirtualList<T> は読み取り専用です。</exception>
/// <exception cref="System.ArgumentOutOfRangeException">index が VirtualList<T> の有効なインデックスではありません。</exception>
public void RemoveAt(int index)
{
this.Items.RemoveAt(index);
}
#endregion
#endregion
#region ICollection<T> メンバ
#region public int Count
/// <summary>
/// VirtualList<T> に格納されている要素の数を取得します。
/// </summary>
public int Count
{
get
{
return this.Items.Count;
}
}
#endregion
#region public bool IsReadOnly
/// <summary>
/// VirtualList<T> が読み取り専用かどうかを示す値を取得します。
/// </summary>
public bool IsReadOnly
{
get
{
return this.Items.IsReadOnly;
}
}
#endregion
#region public void Add(T item)
/// <summary>
/// VirtualList<T> に項目を追加します。
/// </summary>
/// <param name="item">VirtualList<T> に追加するオブジェクト。</param>
/// <exception cref="System.NotSupportedException">VirtualList<T> は読み取り専用です。</exception>
public void Add(T item)
{
this.Items.Add(item);
}
#endregion
#region public void Clear()
/// <summary>
/// VirtualList<T> からすべての項目を削除します。
/// </summary>
/// <exception cref="System.NotSupportedException">VirtualList<T> は読み取り専用です。</exception>
public void Clear()
{
this.Items.Clear();
}
#endregion
#region public bool Contains(T item)
/// <summary>
/// VirtualList<T> に特定の値が格納されているかどうかを判断します。
/// </summary>
/// <param name="item">VirtualList<T> 内で検索するオブジェクト。</param>
/// <returns>item が VirtualList<T> に存在する場合は true。それ以外の場合は false。</returns>
public bool Contains(T item)
{
return this.Items.Contains(item);
}
#endregion
#region public void CopyTo(T[] array, int arrayIndex)
/// <summary>
/// VirtualList<T> の要素を System.Array にコピーします。System.Array の特定のインデックスからコピーが開始されます。
/// </summary>
/// <param name="array">VirtualList<T> から要素がコピーされる 1 次元の System.Array。System.Array には、0 から始まるインデックス番号が必要です。</param>
/// <param name="arrayIndex">コピーの開始位置となる、array の 0 から始まるインデックス番号。</param>
/// <exception cref="System.ArgumentException">
/// array が多次元です。
/// またはarrayIndex が array の長さ以上です。
/// またはコピー元の VirtualList<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);
}
#endregion
#region public bool Remove(T item)
/// <summary>
/// VirtualList<T> 内で最初に見つかった特定のオブジェクトを削除します。
/// </summary>
/// <param name="item">VirtualList<T> から削除するオブジェクト。</param>
/// <returns>
/// item が VirtualList<T> から正常に削除された場合は true。それ以外の場合は false。
/// このメソッドは、item が元の VirtualList<T> に見つからない場合にも false を返します。
/// </returns>
/// <exception cref="System.NotSupportedException">VirtualList<T> は読み取り専用です。</exception>
public bool Remove(T item)
{
return this.Items.Remove(item);
}
#endregion
#endregion
#region IEnumerable<T> メンバ
#region public IEnumerator<T> GetEnumerator()
/// <summary>
/// コレクションを反復処理する列挙子を返します。
/// </summary>
/// <returns>コレクションを反復処理するために使用できる System.Collections.Generic.IEnumerator<T>。</returns>
public IEnumerator<T> GetEnumerator()
{
return this.Items.GetEnumerator();
}
#endregion
#endregion
#region IEnumerable メンバ
#region IEnumerator IEnumerable.GetEnumerator()
/// <summary>
/// コレクションを反復処理する列挙子を返します。
/// </summary>
/// <returns>コレクションを反復処理するために使用できる System.Collections.IEnumerator オブジェクト。</returns>
IEnumerator IEnumerable.GetEnumerator()
{
return ((System.Collections.IEnumerable)this.Items).GetEnumerator();
}
#endregion
#endregion
}
IVirtualListLoader<T> インターフェイス
using System;
using System.Collections.Generic;
/// <summary>
/// リストの仮想プロキシが使用する、ローダーです。
/// </summary>
/// <typeparam name="T"></typeparam>
public interface IVirtualListLoader<T>
{
#region IList<T> Load()
/// <summary>
/// リストをロードします。
/// </summary>
/// <returns>リスト。</returns>
IList<T> Load();
#endregion
}
この仮想リスト及びローダーは、そのまま使用できます。
仮想リストはシールクラスにしてあります。これは、仮想リストを継承したクラスを定義する必要がないはずだからです。null を許可しないとか重複を許可しないといった制約は、仮想リストには実装しません。
この仮想リスト及びローダーを使用したレイジーロードの実装例は次回にでも。



