RIA Services
WCF RIA Services によるエンタープライズ パターン
Michael D. Brown
PDC09 と Mix10 で大きな発表が 2 つ行われました。PDC09 では Silverlight 4 beta が、Mix10 では Silverlight 4 RC が使用可能になるというものです。そして、皆さんがこの記事をお読みになっているころには、Silverlight 4 の完全リリースが Web からダウンロードして入手できるようになるでしょう。このリリースには、広範な印刷サポート、昇格された権限、Web カメラ、マイク、トースト メッセージ、クリップボードのアクセスなどが含まれます。こうした新しい機能セットにより、Silverlight 4 は、マルチプラットフォーム対応のリッチ UI フレームワークとして、Adobe AIR に匹敵することになります。
これらのすべての機能に興味がありますが、私はそもそもビジネス アプリケーションの開発者なので、今回はビジネス データとビジネス ロジックを Silverlight アプリケーションに簡単に組み込む方法について説明したいと思います。
基幹業務アプリケーションに関する Silverlight の 1 つの懸念事項がデータへの接続です。独自の Windows Communication Foundation (WCF) サービスを作成して、Silverlight 3 から問題なくそのサービスに接続できますが、改善の余地はかなりあります。特に、ASP.NET やデスクトップ アプリケーションからデータに接続する方法が無数にあることを考えるとなおさらです。デスクトップ アプリケーションや Web アプリケーションでは、NHibernate、Entity Framework (EF)、または ADO.NET の構造体をそのまま使用してデータベースに直接接続できますが、Silverlight アプリケーションは "クラウド" によってデータから分離されています。ここでは、この分離を "データとの亀裂" と呼ぶことにします。
この亀裂を超えることは、一見簡単そうに見えるかもしれません。もちろん、このことはデータを大量に扱う既存の Silverlight アプリケーションの多くである程度実行されてきました。しかし、問題に取り組むにつれて、最初は簡単そうに見えていたものが次第に複雑になってきます。たとえば、どのようにしてネットワーク経由で変更を追跡すればよいでしょう。どのようにしてファイアウォールの両側にあるエンティティにビジネス ロジックをカプセル化すればよいでしょう。通信の詳細がビジネスの懸案事項に影響しないようにするにはどうすればよいでしょう。
こうした問題に対処するサード パーティ製ツールが登場してきていますが、マイクロソフトも解決策を提示する必要があると考え、WCF RIA Services (旧称 .NET RIA Services) を導入しました。ここでは簡単に RIA Services と呼びます (RIA Services の完全な説明については、2009 年 5 月号の MSDN Magazine 「Silverlight 3 を使用してデータ ドリブンの経費アプリケーションを作成する」(msdn.microsoft.com/magazine/dd695920) を参照してください)。私は最初にベータ プログラムを利用したときから RIA Services に強い関心があり、開発チームに提案を行ったり、自分のアプリケーションでフレームワークを活用する方法について学んできました。
RIA Services フォーラムで最もよくたずねられる質問は、RIA Services にとってベスト プラクティスとなるアーキテクチャについてです。私は、RIA Services の基本的なフォーム中心 ("forms-over-data") の機能にいつも感心していましたが、明らかに自身のアプリケーションをこれまでよりも適切に設計するチャンスと捕らえていたので、フレームワークの懸念事項がアプリケーションのロジックに影響することはありませんでした。
KharaPOS の概要
今回の記事で説明する考え方の具体例を示すために、サンプル アプリケーションとして KharaPOS を開発しました。これは、Silverlight 4 で実装する、RIA Services、Entity Framework、および SQL Server 2008 を使用した POS (販売時点情報管理) アプリケーションです。最終目標は、このアプリケーションを Windows Azure プラットフォームと SQL Azure でホストすることですが、Windows Azure プラットフォームには Microsoft .NET Framework 4 のサポートに関するちょっとした問題 (あるサポートの欠如) があります。
この問題が落ち着くまでは、KharaPOS が .NET Framework 4 を使用して現実のアプリケーションを作成する適切な例となります。このプロジェクトは CodePlex (KharaPOS.codeplex.com、英語) を通じてホストされています。このサイトにアクセスすれば、コードのダウンロード、ドキュメントの閲覧、アプリケーション開発に関するディスカッションへの参加などが可能です。
KharaPOS アプリケーションの設計と機能は、その大部分を Peter Coad、David North、Mark Mayfield 共著の『Object Models: Strategies, Patterns, and Applications, Second Edition』(英語、Prentice Hall PTR、1996 年) という書籍から引用しました。今回は、アプリケーションのサブシステムの 1 つ、カタログ管理に注目します (図 1 参照)。
図 1 カタログ管理のエンティティ データ モデル
エンタープライズ パターン
エンタープライズ アプリケーション開発のデザイン パターンについて説明している優れた書籍は数冊あります。私がいつも参考にしているのは、『エンタープライズ アプリケーション アーキテクチャ パターン』 Martin Fowler 著 (翔泳社、2005 年) です。この書籍と補足の Web サイト (martinfowler.com/eaaCatalog/、英語) では、企業のビジネス アプリケーション開発に役立つソフトウェア パターンがわかりやすくまとめられています。
Fowler 氏が分類したいくつかのパターンは、プレゼンテーションとデータの操作を扱っています。興味深いことに、これらは RIA Services が扱う分野と同じです。これらのパターンを理解すれば、ごく単純なものから最も複雑なものまで、ビジネス アプリケーションのさまざまなニーズを満たすために RIA Services をどのように活用するかに関して、より明確なイメージが得られます。ここでは、次のパターンについて説明します。
- Forms and controls (フォームとコントロール)
- Transaction script (トランザクション スクリプト)
- Domain model (ドメイン モデル)
- Application service layer (アプリケーション サービス層)
まず、これらのパターンについて簡単に説明しましょう。最初の 3 つパターンは、データを扱うロジックのさまざまな処理方法に関係しています。これらのパターンを使って作業を進めていけば、ロジックがアプリケーション全体に散らばり、不必要に反復されることがなくなり、一元化されて集中管理されるようになります。
フォームとコントロール
"フォームとコントロール" パターン ("フォーム中心" パターンとも呼びます) は、すべてのロジックを UI 内部に配置します。一見、これは不適切な考え方のように思えます。しかし、シンプルなデータ入力とマスター/詳細ビューにとっては、UI からデータを取得してデータベースに格納する、最も単純かつ直接的な手法です。多くのフレームワークでは、このパターンが組み込みでサポートされているため (Ruby on Rails のスキャフォールド、ASP.NET Dynamic Data、および SubSonic は、3 つ主要例です)、いわゆるアンチパターンにふさわしい時と場所があることは確かです。多くの開発者は、フォーム中心の手法を初期のプロトタイプ作成にしか使いませんが、最終的なアプリケーションでも確実に利用できます。
その実用性はともかく、フォーム中心の単純さや使いやすさは否定できません。ただし、開発は面倒なので、すばやいアプリケーション開発 (RAD: Rapid Application Development) とは言えません。しかし、WCF RIA Services によって Silverlight に RAD がもたらされます。Entity Framework、RIA Services、および Silverlight Designer を活用すると、次の 5 つの手順だけで、データベース テーブルに対する単純なフォーム中心のエディターを作成することができます。
- Silverlight ビジネス アプリケーションを新規作成します。
- 作成した Web アプリケーションに、新しい Entity Data Model (EDM) を追加します (ウィザードを使用してデータベースをインポートします)。
- Web アプリケーションにドメイン サービスを追加して (最初に Web アプリケーションをビルドして、EDM が適切に検出されるようにします)、データ モデルを参照します。
- データ ソースのパネルを使用して、RIA Services が公開するエンティティを、Silverlight アプリケーションのページ上またはユーザー コントロール上にドラッグします (Web アプリケーションを再度ビルドして、新しいドメイン サービスが表示されるようにします)。
- ボタンと分離コードを追加して、フォーム上での変更をデータベースに保存します。これを行うには、次の簡単なコード行を使用します。
this.categoryDomainDataSource.SubmitChanges();
これで、テーブル内の既存行の直接編集に使用できる、単純なデータ グリッドが作成されます。さらにいくつか追加を行えば、テーブルに新しい行を追加するフォームも作成できます。
このパターンは繰り返し実証されてきましたが、WCF RIA Services を使用する RAD はこのフレームワークを使った開発の基準となるため、ここでそのメリットを示しておくことには依然として意義があります。また、既に述べたように、これは RIA Services ベースのアプリケーション内の有効なパターンです。
推奨事項: ASP.NET Dynamic Data と同じように、フォーム中心のパターンは、ルックアップ テーブル内の行の追加、削除、編集といったロジックがシンプルでわかりやすい、単純な管理 UI (KharaPOS の製品カテゴリのエディターなど) で使用することをお勧めします。しかし、Silverlight と RIA Services を使えば、次のようなはるかに複雑なアプリケーションにも拡張できます。
Table Data Gateway (テーブル データ ゲートウェイ): 先ほど説明した、RIA Services アプリケーション標準の組み込み手法は、Fowler 氏の書籍の 144 ~ 151 ページに示されている、"テーブル データ ゲートウェイ" パターンの実装として見ることもできます。ここでは、2 段階の間接指定 (データベース上で EF をマッピングしてから、EF 上でドメイン サービスをマッピング) により、基本的な作成、読み取り、更新、削除 (CRUD) 操作を使用し、厳密に型指定されたデータ転送オブジェクト (DTO) を返す、データベース テーブルへの単純なゲートウェイを作成しました。
技術的には、間接指定が 2 層になっているため、純粋なテーブル データ ゲートウェイとは言えません。しかし、目を細めれば、"テーブル データ ゲートウェイ" パターンにそっくりに見えます。正直に言うと、先ほどの一覧にある残りのパターンはすべてデータ インターフェイスのパターンですが、フォーム中心パターンはほとんど UI パターンなので、RIA Services と "テーブル データ ゲートウェイ" パターンとのマッピングの説明をもっと論理的に進めることができたでしょう。しかし、基本的なシナリオから始めて、UI に注目し、データベースの説明に戻るのが賢明である感じました。
モデル - ビュー - ビューモデル (MVVM: Model-View-ViewModel): フォーム中心パターンを使用して機能的なフォームを作成するのは簡単ですが、いく分手間がかかるのも事実です。図 2 のカテゴリ管理用 XAML はこの例を示しています。
図 2 カテゴリ管理用 XAML
<Controls:TabItem Header="Categories">
<Controls:TabItem.Resources>
<DataSource:DomainDataSource
x:Key="LookupSource"
AutoLoad="True"
LoadedData="DomainDataSourceLoaded"
QueryName="GetCategoriesQuery"
Width="0">
<DataSource:DomainDataSource.DomainContext>
<my:CatalogContext />
</DataSource:DomainDataSource.DomainContext>
</DataSource:DomainDataSource>
<DataSource:DomainDataSource
x:Name="CategoryDomainDataSource"
AutoLoad="True"
LoadedData="DomainDataSourceLoaded"
QueryName="GetCategoriesQuery"
Width="0">
<DataSource:DomainDataSource.DomainContext>
<my:CatalogContext />
</DataSource:DomainDataSource.DomainContext>
<DataSource:DomainDataSource.FilterDescriptors>
<DataSource:FilterDescriptor
PropertyPath="Id"
Operator="IsNotEqualTo" Value="3"/>
</DataSource:DomainDataSource.FilterDescriptors>
</DataSource:DomainDataSource>
</Controls:TabItem.Resources>
<Grid>
<DataControls:DataGrid
AutoGenerateColumns="False"
ItemsSource="{Binding Path=Data,
Source={StaticResource CategoryDomainDataSource}}"
x:Name="CategoryDataGrid">
<DataControls:DataGrid.Columns>
<DataControls:DataGridTextColumn
Binding="{Binding Name}" Header="Name" Width="100" />
<DataControls:DataGridTemplateColumn
Header="Parent Category" Width="125">
<DataControls:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox
IsSynchronizedWithCurrentItem="False"
ItemsSource="{Binding Source=
{StaticResource LookupSource}, Path=Data}"
SelectedValue="{Binding ParentId}"
SelectedValuePath="Id"
DisplayMemberPath="Name"/>
</DataTemplate>
</DataControls:DataGridTemplateColumn.CellEditingTemplate>
<DataControls:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Parent.Name}"/>
</DataTemplate>
</DataControls:DataGridTemplateColumn.CellTemplate>
</DataControls:DataGridTemplateColumn>
<DataControls:DataGridTextColumn
Binding="{Binding ShortDescription}"
Header="Short Description" Width="150" />
<DataControls:DataGridTextColumn
Binding="{Binding LongDescription}"
Header="Long Description" Width="*" />
</DataControls:DataGrid.Columns>
</DataControls:DataGrid>
</Grid>
</Controls:TabItem>
データ グリッド内の親カテゴリの列はコンボ ボックスです。このコンボ ボックスでは、ユーザーがカテゴリの ID を暗記するのではなく、既存のカテゴリの一覧を使用して、名前で親カテゴリを選択できるようにしています。残念ながら、Silverlight では、ビジュアル ツリー内で同じオブジェクトが 2 回目に読み込まれるときは、このような処理が行われません。したがって、2 つのドメイン データ ソースを宣言する必要がありました。1 つはグリッド用、もう 1 つは参照のコンボ ボックス用です。また、カテゴリを管理する分離コードもやや複雑です (図 3 参照)。
図 3 カテゴリを管理する分離コード
private void DomainDataSourceLoaded(object sender, LoadedDataEventArgs e)
{
if (e.HasError)
{
MessageBox.Show(e.Error.ToString(), "Load Error", MessageBoxButton.OK);
e.MarkErrorAsHandled();
}
}
private void SaveButtonClick(object sender, RoutedEventArgs e)
{
CategoryDomainDataSource.SubmitChanges();
}
private void CancelButtonClick(object sender, RoutedEventArgs e)
{
CategoryDomainDataSource.Load();
}
void ReloadChanges(object sender, SubmittedChangesEventArgs e)
{
CategoryDomainDataSource.Load();
}
ここで MVVM に関する完全なチュートリアルを行うつもりはありません。このトピックの詳細については、2009 年 2 月号の記事『Model-View-ViewModel デザイン パターンによる WPF アプリケーション』(msdn.microsoft.com/magazine/dd419663) を参照してください。図 4 に、RIA Services アプリケーション内で MVVM を活用する方法の 1 つを示します。
図 4 ビュー モデルによるカテゴリ管理
public CategoryManagementViewModel()
{
_dataContext = new CatalogContext();
LoadCategories();
}
private void LoadCategories()
{
IsLoading = true;
var loadOperation= _dataContext.Load(_dataContext.
GetCategoriesQuery());
loadOperation.Completed += FinishedLoading;
}
protected bool IsLoading
{
get { return _IsLoading; }
set
{
_IsLoading = value;
NotifyPropertyChanged("IsLoading");
}
}
private void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged!=null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
void FinishedLoading(object sender, EventArgs e)
{
IsLoading = false;
AvailableCategories=
new ObservableCollection<Category>(_dataContext.Categories);
}
public ObservableCollection<Category>AvailableCategories
{
get
{
return _AvailableCategories;
}
set
{
_AvailableCategories = value;
NotifyPropertyChanged("AvailableCategories");
}
}
ご覧のとおり、ViewModel の役割は、ドメイン コンテキストを初期化して、読み込みが行われるタイミングを UI に通知すると共に、UI からの要求を処理して、新しいカテゴリを作成し、既存のカテゴリへの変更を保存して、ドメイン サービスからデータを再読み込みすることです。これにより、UI と、UI を機能させるロジックが明確に分離されます。MVVM パターンでは多くの処理が必要なように見えるかもしれませんが、データを UI に取り込むロジックを変更しなければならないときに初めて、このパターンのメリットが明らかになります。また、カテゴリを読み込むプロセスを ViewModel に移行すると、ビューを大幅に整理できます (XAML も分離コードも同様です)。
推奨事項: MVVM を使用して、複雑な UI ロジックによって、UI (あるいはビジネス オブジェクト モデル) に不要な情報が表示されないようにします。
トランザクション スクリプト
さて、アプリケーションにロジックを追加し始めると、フォーム中心パターンが厄介になってきます。データを扱うロジックが UI (または、ViewModel の手順を実行した場合は、ViewModel) に埋め込まれ、アプリケーション全体に散在するためです。分散型ロジックのもう 1 つの問題点は、固有の機能がアプリケーション内に既に存在していることに開発者が気が付かず、機能が重複する可能性があることです。その結果、ロジックに変更を加えるときに、(そのロジックを実装しているすべての場所が適切にカタログ化されていると仮定すると) すべての場所にあるロジックを更新しなければならなくなるので厄介です。
"トランザクション スクリプト" パターンは、いくつかの解決策を提供します (このパターンの詳細については、Fowler 氏の書籍の 110 ~ 115 ページを参照してください)。このパターンでは、データを管理するビジネス ロジックと UI を分離できます。
Fowler 氏の定義によると、トランザクション スクリプトは、「ビジネス ロジックをプロシージャ別に編成し、各プロシージャがプレゼンテーション層からの 1 つの要求を処理します」。トランザクション スクリプトは、CRUD 操作よりもはるかに単純です。実際には、トランザクション スクリプトはテーブル データ ゲートウェイの前に配置され、CRUD 操作を処理します。極端に言うと、個別のトランザクション スクリプトがデータベースからの取得とデータベースへの送信をすべて処理します。しかし、論理的な人々は、何事にも適切な時と場所があることを知っています。
トランザクション スクリプトは、アプリケーションで 2 つのエンティティ間のやりとりを調整しなければならない場合 (たとえば、異なるエンティティ クラスの 2 つのインスタンス間の関連付けを作成する場合など) に役に立ちます。たとえば、カタログ管理システムでは、カタログのエントリを作成することにより、製品インベントリに注文して、製品を購入できることをビジネス部門に示します。このエントリは、製品、ビジネス部門、製品 SKU、社内外で製品を注文できる時間を特定します。カタログのエントリの作成を簡素化するために、ドメイン サービスにメソッドを作成しました (次のコード スニペットを参照してください)。このメソッドでは、UI でカタログのエントリを直接操作することなく、ビジネス部門が製品の購入できるかどうかを変更するトランザクション スクリプトを提供します。
実際、カタログのエントリは、ドメイン サービスを通じて公開されることもありません (下図参照)。
public void CatalogProductForBusinessUnit(Product product, int businessUnitId)
{
var entry = ObjectContext.CreateObject<CatalogEntry>();
entry.BusinessUnitId = businessUnitId;
entry.ProductId = product.Id;
entry.DateAdded = DateTime.Now;
ObjectContext.CatalogEntries.AddObject(entry);
ObjectContext.SaveChanges();
}
RIA Services は、クライアント側ドメイン コンテキストの関数として公開されるのではなく、呼び出されたときに対象のエンティティ (この場合は Product) で関数を生成し、変更通知をオブジェクトに発行します。サーバー側では、このオブジェクトがドメイン サービスのメソッド呼び出しと解釈されます。
Fowler 氏は、トランザクション スクリプトを実装する手法として、次の 2 つを推奨しています。
- 操作をカプセル化するコマンドを用意し、このコマンドを渡す。
- トランザクション スクリプトのコレクションを保持する 1 つのクラスを用意する。
ここでは 2 つ目の手法を使用しましたが、コマンドを使用しても何の問題もありません。カタログ エントリを UI 層に公開しないメリットは、トランザクション スクリプトがカタログ エントリを作成する唯一の方法になることです。コマンド パターンを使用する場合は、表記法によって規則を強制します。ただし、開発者がコマンドの存在を忘れてしまえば、ロジックの断片化と重複について説明したところまで戻ることになります。
ドメイン サービスにトランザクション スクリプトを配置するもう 1 つのメリットは、(既に説明したように) ロジックがサーバー側で実行されることです。独自のアルゴリズムを使用する場合や、ユーザーが悪意を持ってデータを操作したのではないことを確信したい場合は、ドメイン サービスにトランザクション スクリプトを配置することをお勧めします。
推奨事項: フォーム中心のビジネス ロジックが複雑になりすぎたときにトランザクション スクリプトを使用して、サーバー側、またはサーバー側とクライアント側で操作ロジックを実行することをお勧めします。
ビジネス ロジックと UI ロジック: ここでは、UI ロジックとビジネス ロジックをいくつか参考にします。一見すると、違いがわかりにくいかもしれませんが重要です。UI ロジックは、プレゼンテーション層に関連するロジックです。つまり、画面に表示する要素とその表示方法 (コンボ ボックスに設定するアイテムなど) に関連します。一方、ビジネス ロジックは、アプリケーションそのものを機能させます (オンライン購入に適用される割り引きなど)。どちらもアプリケーションにとっては重要な側面です。両方のロジックの混在を許可する場合は、別のパターンが必要になります。詳細については、Brian Foote と Joseph Yoder による論文『Big Ball of Mud (大きな泥だんご)』(laputan.org/mud、英語) を参照してください。
複数のエンティティをドメイン サービスに渡す: 既定では、ドメイン サービスのカスタム メソッドに渡すことができるエンティティは 1 つだけです。たとえば、次のメソッド
public void CatalogProductForBusinessUnit(Product product, int businessUnitId)
は、整数値の引数を置き換えて、次のシグネチャを使用しようとすると、うまく機能しません。
public void CatalogProductForBusinessUnit(Product product, BusinessUnit bu)
RIA Services では、この関数のクライアント プロキシが生成されません。なぜなら、そういう規則だからです。カスタム サービス メソッドは、1 つのエンティティのみを受け取ることができます。ほとんどの場合は、これで問題が生じることはありません。というのも、エンティティを受け取れば、エンティティのキーがあることになるので、バックエンドで再度取得することができるためです。
ここでは、エンティティの取得は負荷の高い操作になるということだけお伝えしておきます (おそらく、エンティティは Web サービスとは反対側にあります)。ドメイン サービスには、特定のエンティティのコピーを保持するよう指示することができます (下図参照)。
public void StoreBusinessUnit(BusinessUnit bu)
{
HttpContext.Current.Session[bu.GetType().FullName+bu.Id] = bu;
}
public void CatalogProductForBusinessUnit(Product product, int businessUnitId)
{
var currentBu = (BusinessUnit)HttpContext.Current.
Session[typeof(BusinessUnit).FullName + businessUnitId];
// Use the retrieved BusinessUnit Here.
}
ドメイン サービスは ASP.NET の管理下で実行されているため、一定時間経過後にメモリからオブジェクトを自動的に削除する場合に、ASP.NET のセッションやキャッシュに完全にアクセスできます。私は、複数のリモート Web サービスから顧客関係管理 (CRM) データを取得して、そのデータを統合 UI によってユーザーに提供するプロジェクトで、この手法を実際に使用しています。この際、キャッシュする価値があるデータとそうでないデータがあるため、明示的なメソッドを使用します。
ドメイン モデル
ときどき、ビジネス ロジックが複雑になりすぎて、トランザクション スクリプトでもビジネス ロジックを適切に管理できないことがあります。これは、多くの場合、トランザクション スクリプト内の複雑な分岐ロジックを使用するようになったり、ロジックの微妙な差異を扱う複数のトランザクション スクリプトを使用するようになることで明らかになります。トランザクション スクリプトが実用的でなくなるほどアプリケーションが拡張されることを示すもう 1 つの兆候は、急激に変化するビジネス要件に対処するために、頻繁に更新を行わなければならなくなることです。
こうした兆候に気が付いたら、リッチ ドメイン モデルを検討することをお勧めします (詳細については、Fowler 氏の書籍の 116 ~ 124 ページを参照してください)。これまで紹介してきたパターンには、1 つの共通点があります。それは、エンティティが DTO と大差ないということです。つまり、エンティティにはロジックが含まれていません (これは、Anemic Domain Model (ドメイン モデル貧血症) と呼ばれるアンチパターンになると考えられています)。オブジェクト指向開発の大きなメリットの 1 つは、エンティティに関連するデータとロジックをカプセル化できることです。リッチ ドメイン モデルでは、もともと属していたエンティティにロジックを戻すことによって、このメリットを活用します。
ドメイン モデルのデザインを詳しく説明することがこの記事の目的ではないため、ここでは触れません。このトピックの詳細については、Eric Evans が執筆した『Domain-Driven Design: Tackling Complexity in the Heart of Software』(英語、Addison-Wesley、2004 年) か、先ほど紹介した『Object Models: Strategies, Patterns, and Applications, Second Edition』(英語) のオブジェクト モデルに関する章を参照してください。ただし、ドメイン モデルを使用して、このストレスをいくらか管理できる方法を示すのに役立つシナリオをご紹介することはできます。
KharaPOS アプリケーションのユーザーの中には、特定の製品ラインの過去の売り上げを調べて、この製品ラインが特定期間内に拡大 (この製品ラインのより多くの製品を購入できるようにする) するのか、減少するのか、販売を中止するか、横ばいのまま続けるのかを、市場単位で判断することを希望するユーザーもいます。
販売データは KharaPOS の別のサブシステムに既に存在し、他に必要なすべてのものはカタログ システム内にあります。製品の売上高を表示する読み取り専用のビューを、エンティティ データ モデルに追加します (図 5 参照)。
図 5 販売データを使用して更新したエンティティ データ モデル
ここで必要な作業は、製品選択のロジックをドメイン モデルに追加することだけです。1 つの市場向けの製品を選択するので、BusinessUnit クラスにロジックを追加します (shared.cs または shared.vb という拡張子を持つ部分クラスを使用して、RIA Services に、この関数を使用してクライアントとやりとりすることを指示します)。図 6 にそのコードを示します。
図 6 ビジネス部門向けに製品を選択するドメイン ロジック
public partial class BusinessUnit
{
public void SelectSeasonalProductsForBusinessUnit(
DateTime seasonStart, DateTime seasonEnd)
{
// Get the total sales for the season
var totalSales = (from sale in Sales
where sale.DateOfSale > seasonStart
&& sale.DateOfSale < seasonEnd
select sale.LineItems.Sum(line => line.Cost)).
Sum(total=>total);
// Get the manufacturers for the business unit
var manufacturers =
Catalogs.Select(c =>c.Product.ManuFacturer).
Distinct(new Equality<ManuFacturer>(i => i.Id));
// Group the sales by manufacturer
var salesByManufacturer =
(from sale in Sales
where sale.DateOfSale > seasonStart
&& sale.DateOfSale < seasonEnd
from lineitem in sale.LineItems
join manufacturer in manufacturers on
lineitem.Product.ManufacturerId equals manuFacturer.Id
select new
{
Manfacturer = manuFacturer,
Amount = lineitem.Cost
}).GroupBy(i => i.Manfacturer);
foreach (var group in salesByManufacturer)
{
var manufacturer = group.Key;
var pct = group.Sum(t => t.Amount)/totalSales;
SelectCatalogItemsBasedOnPercentage(manufacturer, pct);
}
}
private void SelectCatalogItemsBasedOnPercentage(
ManuFacturer manufacturer, decimal pct)
{
// Rest of logic here.
}
}
製品の自動選択を行って期間を持ち越すことは、BusinessUnit の新しい関数を呼び出してから、DomainContext で SubmitChanges 関数を呼び出すだけの簡単な作業です。今後、ロジックにバグが見つかった場合やロジックを更新しなければならない場合に、確認すべき場所が正確にわかります。ロジックを一元化しただけでなく、オブジェクト モデルで意図がより明確に表現されるようにしました。『Domain-Driven Design: Tackling Complexity in the Heart of Software』(英語) の 246 ページで、Evans 氏は、オブジェクト モデルで意図が明確に表現されることのメリットについて次のように述べています。
「開発者がコンポーネントを使用するために、コンポーネントの実装について考慮しなければならないとしたら、カプセル化の価値は失われます。本来の開発者以外が、オブジェクトや操作の目的をその実装に基づいて推測しなければならないとしたら、その新しい開発者が操作やクラスで実現される目的を推測できるのは偶然に頼るしかありません。もし意図を間違えていたら、コードはしばらくの間は機能しても、設計の土台になっている考え方が損なわれ、両開発者が互いに誤解したまま作業することになります。」
つまり、関数の目的に合わせて名前を付け、ロジック (といくつかのコメント) をカプセル化 (して行っている処理を明確に) することによって、次の開発者が (それが 5 か月後の自分であっても)、実装を行う前に、行っている処理を簡単に判断できるようにします。このロジックとデータを、必然的に収まる場所に追加すれば、オブジェクト指向言語の表現に富んだ性質を活すことができます。
推奨事項: ロジックが複雑で難しく、一度に複数のエンティティが関係する可能性がある場合は、ドメイン モデルの使用をお勧めします。ロジックと、最も関係の深いオブジェクトをまとめて、操作に意味のある意図的な名前を付けます。
RIA Services におけるドメイン モデルとトランザクション スクリプトの違い: トランザクション スクリプトとドメイン モデルはどちらも、呼び出しがエンティティに対して直接行われていたことにお気付きかもしれません。しかし、この 2 つのパターンでは、ロジックがそれぞれ別の場所にあります。トランザクション スクリプトの場合、エンティティの関数を呼び出すことで、次に変更の送信を呼び出すときに、ドメイン サービスの対応する関数を呼び出す必要があることを、ドメイン コンテキストまたはドメイン サービスに示しています。ドメイン モデルの場合は、ロジックがクライアント側で実行されてから、変更の送信が呼び出されるときに変更が確定します。
Repository (リポジトリ) と Query Objects (クエリ オブジェクト): ドメイン サービスは、その性質上必然的に "リポジトリ" パターンを実装します (Fowler 氏の書籍の 322 ページ参照)。WCF RIA Services Code Gallery (code.msdn.microsoft.com/RiaServices、英語) では、RIA Services チームにより、DomainContext 全体にわたって、このパターンの明示的な実装を作成するすばらしい例が提供されています。これにより、サービス層やデータベースに実際にアクセスすることなく、アプリケーションを簡単にテストできるようになります。また、私のブログ ( azurecoding.net/blogs/brownie、英語) では、リポジトリ全体にわたる "クエリ オブジェクト" パターン (Fowler 氏の著書の 316 ページ参照) の実装を提供しています。このパターンでは、実際の列挙までサーバー側クエリの実行が延期されます。
アプリケーション サービス層
ここで質問です。リッチ ドメイン モデルを利用することを考えてはいても、そのロジックを UI 層に公開することは望んでいないとしたら、どうしますか。"アプリケーション サービス層" パターンの本領が発揮されるのが、このような状況です (Fowler 氏の著書の 133 ページ参照)。ドメイン モデルを作成したら、ドメイン ロジックを shared.cs から別の部分クラスに移動して、エンティティの関数を呼び出す関数をドメイン サービスに配置することで、このパターンを簡単に実装できます。
アプリケーション サービス層は、ドメイン モデル全体にわたる簡略化されたファサードとして機能し、操作を公開します。ただし、その詳細は公開しません。もう 1 つのメリットは、サービス層のクライアントどうしが内部依存関係を持つことなく、ドメイン オブジェクトが内部依存関係を持つことができます。場合によっては、ドメイン サービスは、ドメインに対して 1 つの単純な呼び出しを行います (図 6 の期間によって製品を選択する例を参照してください)。ときどき、少数のエンティティを調整することはありますが、調整しすぎるとトランザクション スクリプトに戻ってしまい、ドメイン内でロジックをカプセル化するメリットが失われてしまうので注意してください。
推奨事項: アプリケーション サービス層を使用して、ドメイン モデル全体にわたる簡単なファサードを提供し、エンティティが持つことができる依存関係を UI 層が持たなければならない必要性をなくします。
おまけ: 境界コンテキスト
RIA フォーラムで、よく、「複数のドメイン サービスにまたがる非常に大きなデータベースを分割して、もっと管理しやすくするにはどうすればよいでしょうか」と参加者からたずねられました。これに関連して、「複数のドメイン サービスに存在していることが必要なエンティティをどのように処理すればよいでしょうか。」ともたずねられました。最初は、このようなことは必要ないと考えました。というのも、ドメイン サービスはドメイン モデル全体にわたるサービス層として機能し、1 つのドメイン サービスがドメイン全体にわたるファサードとして機能すべきだからです。
しかし、この記事を執筆するための調査中に、bounded-context (境界コンテキスト) パターン (Evans 氏の著書の 336 ページ参照) に遭遇しました。このパターンに関する記事を以前に読んだことはありましたが、先ほどの質問に回答したときは覚えていませんでした。このパターンの原理は、あらゆる大規模プロジェクトには、それぞれ役割を果たす複数のサブドメインが存在するというものです。KharaPOS を例にとってみると、Catalog にドメインが存在し、Sales に別のドメインが存在するということです。
境界コンテキストは、こうしたドメインを円滑に共存させることができます。これは、ドメイン間で共有される要素がある場合でも可能です (たとえば、Sale、BusinessUnit、Product、LineItem などは Sales と Catalog の両方のドメインに含まれます)。エンティティに適用される規則は、それらの要素を操作しているドメインによって異なります (たとえば、Catalog ドメイン内では、Sale と LineItem は読み取り専用になります)。最終的な規則として、操作が複数のコンテキストにまたがることはありません。Silverlight では複数のドメイン サービス全体にわたるトランザクションはサポートされないため、ドメインを円滑に共存させられることで、作業が楽になります。
推奨事項: 境界コンテキストを使用して、大規模なシステムを複数の論理サブシステムに分割します。
成功の落とし穴
今回の記事では、RIA Services によって、ほとんど作業を必要としない主要エンタープライズ パターンがサポートされるしくみについて見てきました。フレームワークは、移行のために多くの作業を必要としないで、スプレッドシートにデータを入力するようなごく単純なアプリケーションから、最も複雑なビジネス アプリケーションまで、あらゆるアプリケーションをサポートするほどの柔軟性を備えていますが、フレームワークが非常にわかりやすいということはまずありません。Brad Abrams は自身のブログへの投稿で、このことを「Pit of Success (成功の落とし穴)」と呼んでいます (blogs.msdn.com/brada/archive/2003/10/02/50420.aspx、英語)。
Mike Brown は、KharaSoft Inc. (kharasoft.com、英語) の代表取締役兼共同設立者です。KharaSoft Inc. は、トレーニング、カスタム ソフトウェア開発、および SaaS (Software as a Service) を専門とするテクノロジ企業です。彼は熟練のテクノロジ スペシャリストであり、この業界で 14 年以上の経験があります。MVP を連続受賞し、Indy Alt.NET ユーザー グループ (indyalt.net、英語) の共同設立者でもある Mike は、筋金入りのシカゴ ベアーズ ファンです。
この記事のレビューに協力してくれた技術スタッフの Brad Abrams に心より感謝いたします。