実践的なパターン
永続化のパターン
Jeremy Miller
目次
データベースへのオブジェクトのマッピング
Active Record
Data Mapper
Repository の使用
Identity Map
Lazy Loading と Eager Loading
Virtual Proxy パターン
次のステップ
データ アクセスは、開発者の間では一般的なテーマです。確かに、特定のデータ アクセス テクノロジと永続化のフレームワークに関する意見は多数ありますが、各自のプロジェクトでこれらのツールを使用する最善の方法は何でしょうか。プロジェクトに対して正しいツールを選択するには、どのような基準を使用する必要がありますか。ツールを使用する前に知っておく必要がある概念は何ですか。十分に時間があり、永続化ツールを独自に記述する場合、何を知っておく必要がありますか。
当然ながら、すべての質問に対する回答は、永続化の基になる設計パターンを調べることです。
ドメイン モデル
システムでビジネス ロジックを構築して表すには、主要な選択肢がいくつかあります。この記事では、ビジネス ロジックをエンティティ オブジェクトとして編成するためにドメイン モデルの手法を選択したと仮定します。
文字通りに解釈すれば、ドメイン モデルは動作とデータの両方が組み込まれているドメイン オブジェクト モデルです。
たとえば、筆者が現在手がけているプロジェクトには、カスタマ リレーションシップ マネジメント (CRM) が含まれます。データおよびそのデータを含むビジネス ルールの両方を実装する Case、User、および Solution に対するエンティティ オブジェクトがあります。ドメイン モデルは、データ構造セットのみの単純なモデルからナロー インターフェイス (ハードコア ドメイン駆動開発) の背後で生データを保護するリッチなモデルまでの幅広い範囲にわたります。各自のドメイン モデルがこの範囲のどこに該当するかは、主にシステムのビジネス ロジックの複雑さ、およびシステム要件におけるレポート作成やデータ入力の頻度に基づきます。
Domain Model パターンについては、この記事では取り上げません。ビジネス ロジックの編成に関する主要な設計パターンについては、Martin Fowler 著『エンタープライズ アプリケーション アーキテクチャ パターン』の第 2 章を読むことをお勧めします。
説明を始める前に、システムにおけるデータベースおよびデータ アクセス コードの役割を理解するための 2 つの主要な考え方を確認しましょう。
- データベースが、アプリケーションとビジネス資産の基本です。データ アクセス コード、およびアプリケーションまたはサービス コードでさえ、データベースを外部の世界に接続するメカニズムに過ぎません。
- 中間層のビジネス オブジェクトおよびユーザー インターフェイスまたはサービス レイヤは、アプリケーションです。データベースは、セッション間でビジネス オブジェクトの状態を確実に永続化する手段です。
個人的には、最初のデータ中心の視点は .NET コミュニティで広く理解されていると考えているので、ここでは 2 番目の視点に焦点を合わせます。2 番目の視点では、一般に中間層のエンティティ オブジェクトを操作します。いずれにしても、多くの場合、ビジネス エンティティとデータベース テーブルの間でデータをマップするオブジェクト/リレーショナル マッピング (O/RM) を実行します。これは、手動で実行することもできますが、多くの場合は何らかのツールを使用します。このようなツールは有用で、多くの開発時間を節約できる可能性がありますが、注意する必要がある問題がいくつかあるので、いずれの場合も、ツールが背後でどのように動作しているかを理解することをお勧めします。この記事のパターンを確認することは、どちらの視点においても役に立ちます。
この記事では、テスト駆動開発、振舞駆動開発、リーン プログラミング、継続的デザインなどのアジャイル手法を強く推奨します。この記事で説明する一連のパターンを強く推奨する理由については、後で詳しく説明します。
データベースへのオブジェクトのマッピング
ここでは、ID、データ、およびそれに関連する動作を含むエンティティ オブジェクトを使用して、中間層でシステムをモデル化します。ビジネス ロジックは、エンティティ オブジェクトまたはそれらを使用するドメイン サービスに実装されます。ただし、問題は、データベースとエンティティ オブジェクトの間でシームレスにデータをやり取りする方法です。
リレーショナル データベースの場合は、オブジェクトのフィールドとプロパティをデータベースのテーブルとフィールドに移動する必要があります。このコード全体を手動で記述することも、INSERT、UPDATE、SELECT、および DELETE SQL の各ステートメントを個別に記述することもできますが、同じコードの記述を何度も繰り返していることにすぐに気付くはずです。つまり、オブジェクトのプロパティまたはフィールドのデータをデータベース テーブルの特定の列に格納することを繰り返し指定しています。
ここでオブジェクト リレーショナル マッピング (O/RM) を使用できます。O/RM を使用してオブジェクト プロパティからデータベース テーブルへのマッピングを作成するだけで、O/RM ツールはメタデータを使用して、SQL ステートメントの実行内容と、オブジェクトから SQL ステートメントにデータを移動する方法を特定できます。
それでは、実際に基本的な例を見てみましょう。筆者が現在手がけているプロジェクトには、次のような Address クラスがあります。
public class Address
{
public long Id { get; set; }
public string Address1 { get; set; }
public string Address2 { get; set; }
public string City { get; set; }
public string StateOrProvince { get; set; }
public string Country { get; set; }
public string PostalCode { get; set; }
public string TimeZone { get; set; }
}
Address クラスのマッピングを設定する場合、Address クラスをマップするテーブル、Address オブジェクトを識別する方法 (主キー)、データベース テーブルにマップするプロパティを指定する必要があります。
この例では、Address のマッピングに Fluent NHibernate ツールを使用しています。
ここでは、詳細をすべて示すために意図的に手動でマッピングをしています。実際は、「設定より規約」の方法を使用して反復処理のかなりの部分を省略できます。コードを以下に示します。
public class AddressMap : ClassMap<Address>
{
public AddressMap()
{
WithTable("Address");
UseIdentityForKey(x => x.Id, "id");
Map(x => x.Address1).TheColumnNameIs("address1");
Map(x => x.Address2).TheColumnNameIs("address2");
Map(x => x.City).TheColumnNameIs("city");
Map(x => x.StateOrProvince).TheColumnNameIs("province_code");
Map(x => x.Country).TheColumnNameIs("country_name");
Map(x => x.PostalCode).TheColumnNameIs("postal_code");
}
}
オブジェクト モデルからデータベース モデルへのマッピングが作成されたので、マッピングを実際に実行する必要があります。それには、主に Active Record と Data Mapper の 2 つの方法があります。
Active Record
永続化の戦略を選択する場合に必要な最初の決定は、マッピングを実行する主体をどこにするかということです。それには 2 つの非常に異なる選択肢があります。それぞれのエンティティ クラス自体にマッピングを担当させるか、または、まったく別個のクラスを使用してデータベースへのマッピングを行うことができます。
最初の方法は、Active Record パターンと呼ばれます。このパターンでは、データベース テーブルまたはビューの行をラップし、データベース アクセスをカプセル化し、そのデータのドメイン ロジックを追加するオブジェクトを使用します。Active Record の手法では、永続化メソッドをエンティティ オブジェクトに直接配置します。この場合、Address クラスには、多くの場合、Address オブジェクトのためにデータベースに対してクエリを実行する静的 Load メソッドに加えて、Save、Update、Delete などのメソッドがあります。
Active Record パターンの典型的な用法は、基本的にデータベース テーブルの単一の行に対して厳密に型指定されたクラス ラッパーを作成することです。したがって、Active Record のクラスは、一般にデータベース構造を正確にミラー化したものです (ツールによって異なります)。Active Record の多くの実装では、データベース構造から直接エンティティ オブジェクトが生成されます。
最も有名な Active Record の実装は、Ruby on Rails Web 開発フレームワークの一部として提供されている ActiveRecord ツールです。これは、.NET プログラミングで IronRuby と共に使用できます。「IronRuby と RSpec の概要 (第 1 部)」を参照してください。ActiveRecord を使用する場合は、筆者ならまずデータベースに addresses という名前のテーブルを作成します。その後は、アドレス データの検索、保存、挿入のためにアドレス データとメソッドにアクセスすることのみを考える場合は、Address クラス全体は次の Ruby クラスとまったく同じになります。
class Address < ActiveRecord::Base
end
Ruby を実装するには、メタプログラミングを使用して、実行時に Address というクラス名の複数形である addresses という名前のデータベース テーブルに対応する、フィールドとクエリ メソッドを動的に作成します。
いくつかの例外を除いて、Active Record パターンの .NET の実装では、一般にデータベース テーブルに直接マップされる .NET のクラスを作成するコード生成を使用します。この手法の利点は、非常に簡単にデータベースとオブジェクト モデルを同期できることです。
リーン プログラミング
リーン プログラミングを使用すると、開発プロジェクトで "プッシュ" 設計より "プル" 設計を優先することによる無駄を排除できることがわかります。つまり、永続化などのインフラストラクチャ上の問題については、アプリケーションが今後必要とすると考えられるデータ アクセス レイヤ コードを構築する (プッシュ設計) のではなく、ビジネス要件を満たすように設計して構築する (オンデマンドのプル設計) だけでいいということです。
経験的にはこれは、システムを一度に水平的なレイヤ上に構築する代わりに機能を 1 つずつ構築することにより、垂直的に追加して開発することを意味します。この手法では、システムに組み込まれる機能にすべてのインフラストラクチャ コードを連結することによって、インフラストラクチャの開発を絶対的に必要な機能のみに絞り込むことができます。ユーザー インターフェイス、サービス、またはビジネス ロジック レイヤを記述する前に、データ アクセス レイヤまたはデータベースを構築して水平的に開発すると、次のようなリスクがあります。
- 実際に動作を提示する機能がないので、早い時期にプロジェクト関係者から適切なフィードバックを得ることができない
- 実際に構築しない可能性がある機能のために不要なインフラストラクチャ コードを記述する
- 早い時期にビジネス要件を把握できなかったり、他のレイヤが作成される前にビジネス要件が変化したりするので、正しいデータ アクセス コードを構築できない
プル設計では、アプリケーションのニーズに基づいてデータ アクセス戦略を決定する必要もあります。できれば、ドメイン モデルのビジネス ロジックをモデル化するシステムに O/RM を選択することをお勧めします。レポート アプリケーションでは、SQL とデータセットのみを使用することで、永続化ツール全体を省略できます。ほとんどデータ入力のみのシステムでは、Active Record ツールを選択することもあります。選択のポイントは、データ アクセスと永続化が使用者のニーズに左右されるということです。
Data Mapper
Active Record パターンと同様に役に立ちます。特に、データベース モデルと異なるオブジェクト構造を使用する場合に有効です。複数のクラスを同じデータベース テーブルにマップしたい場合があります。ビジネス ロジックによっては、使用したいオブジェクトの形状に、所有している古いデータベース スキーマが適合しない場合もあります (個人的には最近のいくつかのプロジェクトでこのようなことがありました)。
そのような場合は、Data Mapper パターンを選択します。Data Mapper パターンでは、オブジェクトとデータベース間でデータをやり取りするマッパ オブジェクトのレイヤを使用します。やり取りの際、オブジェクトとデータベースは、相互に独立しており、マッパ クラス自身からも独立しています。Data Mapper パターンでは、クラスをエンティティの外部に置くことにより、エンティティ オブジェクトから永続化のほとんどの部分を排除できます。Data Mapper パターンでは、ある種のリポジトリを使用してエンティティ オブジェクトにアクセスし、クエリを実行し、保存します (この記事の後半で説明します)。
どちらを選択しますか。個人的には、Data Mapper ソリューションを推奨します。それはともかく、Active Record は、より簡単なドメイン ロジックを含むシステム、CRUD を多く使用するアプリケーション (つまり、作成、読み込み、更新、削除)、およびドメイン モデルがデータベース構造から大きく異なる必要がない状況に最も適しています。Active Record は、.NET チームにとってより一般的なデータ中心的な方法を意味し、.NET 自身が明確に強くサポートしているので、多くの .NET 開発チームにとって Active Record の方が快適に使用できるかもしれません。.NET スペースのほとんどの永続化ツールは、Active Record ツールではないかと思います。
他方で、Data Mapper はドメイン モデルの形状がデータベース モデルから大きく異なる複雑なドメイン ロジックを含むシステムに適しています。Data Mapper は、永続化ストアからドメイン モデルのクラスを分離します。これは、異なるデータベース エンジンまたはスキーマ、さらには異なるストレージ メカニズムでドメイン モデルを再利用する必要がある場合に効果を発揮する場合があります。
アジャイルの使用者にとって最も重要なことは、Data Mapper の手法では、テスト駆動型の開発ツールを使用して、オブジェクト モデルを、データベースから独立した状態で、Active Record ソリューションより効率的に設計できることです。このことは、追加的な手法を使用して設計するチームにとって重要です。なぜなら、一般にリファクタリング ツールと単体テスト テクニックをより効果的に使用できるオブジェクト モデルの設計を最初に完了し、オブジェクト モデルが安定した後でデータベース モデルを作成することができるからです。Data Mapper パターンは、.NET コミュニティで普及しつつあるドメイン駆動設計アーキテクチャの手法にも適しています。
Repository の使用
Windows DNA の開発者として経験を積んでいたころは、コードで ADO Connection オブジェクトを閉じるのを忘れることへの恐怖がありました。初めて大きなエンタープライズ プロジェクトを担当したとき、ADO にコードを直接記述しました。データベースにデータを要求するか、または保存する必要がある場合は、必ず以下の作業を行う必要がありました。
- 特定の構成から接続文字列を見つける
- データベースに対する新しい接続を開く
- コマンド オブジェクトまたはレコードセット オブジェクトを作成する
- SQL ステートメントまたはストアド プロシージャを実行する
- 接続を閉じてデータベースのリソースを解放する
- データ アクセス コードに関して適切なエラー処理を配置する
1 つ目の問題は、同じコードを大量に繰り返し記述していることでした。2 つ目の問題は、1 つでも接続を閉じるのを忘れると (残念ながら実際にあったことです)、負荷が高いミッション クリティカルなシステムがオフラインになってしまうので、常に正しいコードを記述する必要があることでした。データベース接続の不具合によってコードが動作不良に陥ることはだれも望まなかったのですが、それでもそれは頻繁に起こっていました。
次のプロジェクトで、チームのメンバは多少賢くなりました。ADO Connection オブジェクトのセットアップ、破棄、およびエラー処理のすべてを行う単一のクラスを実装しました。システムの他のすべてのクラスも、この中央のクラスを使用してストアド プロシージャを呼び出してデータベースと対話できます。データベース接続を管理するコードは一度記述するだけでよくなったので、記述するコードが少なくなり、接続を閉じることを忘れることによる問題の発生に苦しむこともなくなりました。
チームが実行したことは、ドメイン オブジェクトにアクセスするためのコレクションのようなインターフェイスを使用してドメインとデータ マッピング レイヤを調整する Repository パターンの大まかな実装を作成することでした。
Repository パターンは、基本的にアプリケーション コードの残りの部分が永続化の動作を知らなくても済むように、永続化システムに覆いをかける役目を果たします。筆者が現在手がけているプロジェクトでは、図 1 のようなパブリック インターフェイスで Repository を使用しています。
図 1 Repository インターフェイス
public interface IRepository
{
// Find an entity by its primary key
// We assume and enforce that every Entity
// is identified by an "Id" property of
// type long
T Find<T>(long id) where T : Entity;
// Query for a specific type of Entity
// with Linq expressions. More on this later
IQueryable<T> Query<T>();
IQueryable<T> Query<T>(Expression<Func<T, bool>> where);
// Basic operations on an Entity
void Delete(object target);
void Save(object target);
void Insert(object target);
T[] GetAll<T>();
}
システム内のクラスがエンティティ オブジェクトにアクセスする必要がある場合は、IRepository を使用して ID によってエンティティを簡単に取得したり、LINQ 式を使用してエンティティ オブジェクトの一覧を照会したりできます。
O/RM の NHibernate ライブラリを使用する IRepository の具象クラスを使用する場合は、メモリから接続文字列を取得し、アセンブリからマッピング定義を読み込み、NHibernate SessionFactory を構築し (パフォーマンスに影響するので一度だけ)、NHibernate から低レベルの ISession インターフェイスをラップしています。Repository クラスの機能を使用することにより、NHibernate はデータベース接続のライフサイクルの問題を管理できます。
このように、背後で多くの処理が実行されています。IRepository インターフェイスの背後で、NHibernate との直接的なすべての対話を排除できたことは朗報です。オブジェクトの読み込み、保存、クエリの実行だけのために、NHibernate をブートストラップする方法のすべてを知る必要はありません。さらに、すべてのクラスがデータ アクセスと永続化のために抽象的な IRepository インターフェイスに依存するので、LINQ to Objects を内部的に使用してテスト中にデータベースをスタブする IRepository の InMemoryRepository 実装を滑り込ませることができます。
Identity Map
ここでは、次のような一般的なシナリオを見てみます。ある配送システムにおける単一の論理トランザクションで独立して動作する 2 つのまったく別個のクラスがありますが、両方のクラスはトランザクションの処理中に同じ Customer エンティティを取得する必要があるとします。理想的なことに、必要とするのは、各論理 Customer の単一のトランザクション内の単一の Customer オブジェクトのみなので、各オブジェクトで同じデータが使用されます。
永続化ツールでは、論理参照の重複の防止は Identity Map パターンが行います。Martin Fowler が説明しているように、Identity Map では、各オブジェクトは一度だけ読み込まれます。読み込んだすべてのオブジェクトはマップに格納し、参照するときはマップを使用してオブジェクトを検索します。
Customer クラスに対しては、図 2 のような Identity Map の単純な実装を構築できます。ここでは、コードを簡素化するために、スレッド ロックを意図的に省略しています。実際の実装には、適切なスレッドの安全対策が必要になります。
図 2 Identity Map の Customer クラス
public class CustomerRepository
{
public IDictionary<long, Customer> _customers =
new Dictionary<long, Customer>();
public Customer FindCustomer(long id)
{
if (_customers.ContainsKey(id))
{
return _customers[id];
}
var customer = findFromDatabase(id);
_customers.Add(id, customer);
return customer;
}
private Customer findFromDatabase(long id)
{
throw new System.NotImplementedException();
}
}
この例では、Customer オブジェクトは ID によって識別されます。ID によって Customer のインスタンスを要求すると、CustomerRepository は、まず内部辞書にその Customer があるかどうかを確認します。見つかった場合、既存の Customer オブジェクトが返されます。見つからない場合、CustomerRepository はデータベースからデータを取得し、新しい Customer オブジェクトを構築し、その後の要求に備えて辞書に格納してから Customer オブジェクトを返します。
優れた永続化ツールであれば、この機能が含まれているはずなので、一般にこのコードを手動で記述する必要はありません。このような処理が背後で実行されていることを把握し、それに応じて永続化サポート オブジェクトのスコープを決定する必要があります。多くの場合、StructureMap、Windsor、Ninject などの Inversion of Control ツールのライフサイクル管理機能を使用して、単一の HTTP 要求またはスレッドのすべてのクラスが基になる同じ Identity Map を使用するようにします。Unit of Work パターンは、同じ論理トランザクションの複数のクラスすべてに対応する単一の Identity Map を管理するためのもう 1 つの方法です。
このパターンをさらに説明するために、図 3 に、Identity Map の動作の例を示します。このコードは、筆者が現在手がけているプロジェクトのアーキテクチャに対して記述されたものです。以下のコードに示されている IRepository インターフェイスのインスタンスは単一の NHibernate ISession をラップし、それが Identity Map パターンを実装します。このテストの実行結果の出力は、次のとおりです。
1 passed, 0 failed, 0 skipped, took 5.86 seconds.
図 3 Identity Map の使用
[Test]
public void try_out_the_identity_map()
{
// All I'm doing here is getting a fully formed "Repository"
// from an IoC container and letting an IoC tool bootstrap
// NHibernate offstage.
IRepository repository = ObjectFactory.GetInstance<IRepository>();
// Find the Address object where Id == 1
var address1 = repository.Find<Address>(1);
// Find the Address object where Id == 1 from the same Repository
var address2 = repository.Find<Address>(1);
// Requesting the same identified Address object (Id == 1) inside the
// same Repository / Identity Map should return the exact same
// object
address1.ShouldBeTheSameAs(address2);
// Now, let's create a completely new Repository that has a
// totally different Identity Map
IRepository secondRepository = ObjectFactory.GetInstance<IRepository>();
// Nothing up my sleeve...
repository.ShouldNotBeTheSameAs(secondRepository);
var addressFromSecondRepository = secondRepository.Find<Address>(1);
// and this is a completely different Address object, even though
// it's loaded from the same database with the same Id
addressFromSecondRepository.ShouldNotBeTheSameAs(address1);
}
Lazy Loading と Eager Loading
永続化ツールを使用する利点の 1 つは、ルート オブジェクト (多くの場合 Invoice) を読み込み、親クラスのプロパティを使用するだけで、その子 (InvoiceLineItem) および関連するオブジェクトに直接アクセスできることです。ただし、やがてアプリケーションのパフォーマンスを考慮する必要性が出てきます。トップ レベルのオブジェクトのみを必要とするのがほとんどである場合、またはオブジェクト グラフの一部がなくても済む場合に、オブジェクト グラフ全体を取得するのは効率的ではありません。
そのような場合、オブジェクトが必要とされるまで初期化を延期する Lazy Loading パターンを使用できます。
それを具体的に説明すると、次のようになります。Address オブジェクトを参照する Customer というクラスがあるとします。
public class Customer : DomainEntity
{
// The "virtual" modifier is important. Without it,
// Lazy Loading can't work
public virtual Address HomeAddress { get; set; }
}
Customer オブジェクトに関するほとんどのユース ケースでは、コードに Customer.HomeAddress プロパティが必要になることはありません。その場合、この Fluent NHibernate マッピングのように、Customer.HomeAddress プロパティを遅延読み込みするようにデータベース マッピングを設定できます。
public class CustomerMap : DomainMap<Customer>
{
public CustomerMap()
{
// "References" sets up a Many to One
// relationship from Customer to Address
References(x => x.HomeAddress)
.LazyLoad() // This marks the property as "Lazy Loaded"
.Cascade.All();
}
}
Lazy Loading を有効にすると、Customer オブジェクトは Address データなしで取得されます。ただし、呼び出し元が初めて Customer.HomeAddress プロパティにアクセスしようとすると、データが即座に透過的に読み込まれます。
Customer.HomeAddress プロパティの仮想修飾子に注目してください。すべての永続化ツールがこれを行うわけではありませんが、NHibernate は、HomeAddress プロパティをオーバーライドして遅延読み込みの対象にする Customer の動的なサブクラスを作成することによって、遅延読み込みプロパティを実装します。HomeAddress プロパティは、サブクラスがプロパティをオーバーライドできるように virtual として指定する必要があります。
もちろん、オブジェクトを要求する際に、それと同時に子オブジェクトが必要になる可能性が高いことがわかっている場合もあります。このような場合は、Eager Loading を選択し、親と同時に子のデータを読み込みます。多くの永続化ツールには、Eager Loading シナリオを最適化し、単一のデータベースを反復処理してデータ階層を取得するための何らかの機能があります。Customer.HomeAddress データを必要とする場合、ほとんどの場合は Customer オブジェクトを使用し、次に Eager Loading を実行して Customer と Address のデータを同時に取得するのが良いでしょう。
この時点では、「アプリケーションのパフォーマンスを確実に向上する唯一の方法は、プロファイラを使用して実証的にパフォーマンスを測定することだ」という古い格言を繰り返すべきでしょう。
Virtual Proxy パターン
Lazy Loading は、多くの場合、後で読み込む実際のオブジェクトに類似する仮想プロキシ オブジェクトを使用して実装されます。Customer オブジェクトの一覧を参照する CustomerRepresentative というクラスが含まれるドメイン モデルがあるとします。このクラスの一部を図 4 に示します。
図 4 CustomerRepresentative
public class CustomerRepresentative
{
// I can create a CustomerRepresentative directly
// and use it with a normal List
public CustomerRepresentative()
{
Customers = new List<Customer>();
}
// Or I can pass an IList into it.
public CustomerRepresentative(IList<Customer> customers)
{
Customers = customers;
}
// It's not best practice to expose a collection
// like I'm doing here, but it makes the sample
// code simpler ;-)
public IList<Customer> Customers { get; set; }
}
システムでは、CustomerRepresentative のインスタンスを使用する際に、Customers の一覧を必要としないことがよくあります。その場合、IList<Customer> オブジェクトに類似する仮想プロキシ オブジェクトを使用して簡単に CustomerRepresentative を構築し、CustomerRepresentative に変更を加えずに仮想プロキシ クラスを使用できます。仮想プロキシ クラスは、図 5 のようになります。次に、CustomerRepresentative オブジェクトを、図 6 のように Lazy Loading を使用して作成できます。
図 5 CustomerRepresentative の仮想プロキシ
public class VirtualProxyList<T> : IList<T>
{
private readonly Func<IList<T>> _fetcher;
private IList<T> _innerList;
private readonly object _locker = new object();
// One way or another, VirtualProxyList needs to
// find the real list. Let's just cheat and say
// that something else will pass it a closure
// that can find the real List
public VirtualProxyList(Func<IList<T>> fetcher)
{
_fetcher = fetcher;
}
// The first call to
private IList<T> inner
{
get
{
if (_innerList == null)
{
lock (_locker)
{
if (_innerList == null)
{
_innerList = _fetcher();
}
}
}
return _innerList;
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return inner.GetEnumerator();
}
public IEnumerator<T> GetEnumerator()
{
return inner.GetEnumerator();
}
public void Add(T item)
{
inner.Add(item);
}
// and the rest of the IList<T> implementation
}
図 6 CustomerRepresentative の Lazy Loading
public class CustomerRepresentativeRepository
{
private readonly ICustomerRepository _customers;
public CustomerRepresentativeRepository(
ICustomerRepository customers)
{
_customers = customers;
}
// This method will "find" a CustomerRepresentative, and
// set up the Virtual Proxy for the Customers
public CustomerRepresentative Find(long id)
{
var representative = findRepresentative(id);
representative.Customers =
new VirtualProxyList<Customer>(() =>
_customers.GetCustomersForRepresentative(id));
return representative;
}
}
この記事の大半のパターンと同様に、仮想プロキシは手動で記述する内容ではなく、あくまで永続化ツールの背後にあるということに注意してください。
次のステップ
Windows DNA テクノロジを使用してプログラミングを開始したときは、半分以上の時間を ADO コードを直接記述することに費やしていました。現在は、永続化およびそのインフラストラクチャのコードに費やす時間が非常に少なくなっています。この数年で何が変わったのでしょうか。永続化ツールおよびこれらの設計パターンを使用することにより、反復的なコードの記述をかなり排除することができました。
これらのパターンの詳細については、Martin Fowler 著『エンタープライズ アプリケーション アーキテクチャ パターン』を参照してください。エンタープライズ アプリケーションの記述に携わる場合は、この本を読むことをお勧めします。スペースが限られているので、この記事では、Unit of Work、Specifications、および Persistence Ignorance などのその他の重要なパターンについて説明できませんでした。Repository パターンを使用する方法については、多くの設計上の考慮事項があります (単一のジェネリック リポジトリ対固有リポジトリ、"ナロー" リポジトリ クラス、リポジトリが "Save" メソッドも公開すべきかどうかなど)。このようなトピックについてもさらに調べることをお勧めします。また、このテーマをフォローアップする記事を書くことができるかもしれません。
最後に、.NET エコシステムは、Entity Framework や LINQ to SQL よりさらに充実していることを言っておきます。永続化に関しては多少劣りますが、NHibernate は Data Mapper としては十分なものです。SubSonic は、.NET プログラミング用に普及している Active Record の実装です。既存のデータベースまたは環境で SQL ステートメントを完全に制御する必要がある場合は、iBatis.Net が最適です。LLBLGen Pro は、独自のクエリ機能を搭載する優れたツールです。その他の多くのツールでも、LINQ クエリを使用できます。
ご意見やご質問は、mmpatt@microsoft.com まで英語でお送りください。
Jeremy Miller は、C# の Microsoft MVP であり、.NET での依存関係のインジェクション用に公開されているオープン ソースの StructureMap (structuremap.sourceforge.net) ツール、および .NET での効率的な FIT テスト用に近日公開予定の StoryTeller (storyteller.tigris.org) ツールの作成者でもあります。彼のブログ「The Shade Tree Developer (日陰の開発者)」は、CodeBetter サイト上で公開されています。