BT

最新技術を追い求めるデベロッパのための情報コミュニティ

寄稿

Topics

地域を選ぶ

InfoQ ホームページ アーティクル ドメインモデル管理のあらゆる側面

ドメインモデル管理のあらゆる側面

ブックマーク

はじめに

『Domain Driven Design (ドメイン駆動設計)』 [Evans DDD] や『Applying Domain Driven Design and Patterns (ドメイン駆動設計とパターンの適用)』 [Nilsson ADDDP]などの本から学ぶことができるように、ドメインモデルパターン (『Domain Model pattern (Patterns of Enterprise Application Architecture (エンタープライズアプリケーションアーキテクチャのパターン) 』 [Fowler PoEAA]) をアプリケーションアーキテクチャに導入すると、数多くのメリットが約束されるが、何もせずに手に入れることはできない。

ドメインモデルの使用は、ドメインモデルクラスを作成してそれらを使用するのと同じくらい簡単だということは、めったにない。ドメインモデルをサポートするかなりの量のインフラストラクチャコードも必要になることが、すぐに分かる。

ドメインモデルに伴うインフラストラクチャ要求の中でも際立っているのは、もちろん永続性である。一般に、リレーショナルデータベースに対する永続性だ。そこで、O/Rマッパー (Object/Relational Mapper) が重要になってくる。しかし、ことは永続性だけではない。複雑なアプリケーションにおけるインフラストラクチャの大部分は、多くの場合、ランタイム時にドメインモデルオブジェクトの管理に専念する。私は、このインフラストラクチャの部分を、ドメインモデル管理 (Domain Model Management) [Helander DMM]、略してDMMと呼ぶ。

インフラストラクチャコードをどこに配置するか?

インフラストラクチャコードの量が増えるにつれ、そのすべてに対処する優れたアーキテクチャを見つけることがますます重要となる。質問の大部分は、次のことだ。インフラストラクチャコードをドメインモデルクラスに配置できるか、それともそれは是が非でも避けるべきか?

ドメインモデルクラスでインフラストラクチャコードを避けるべきとの主張は強い。ドメインモデルは、アプリケーションが対処するコアビジネス概念を表す。これらのクラスをクリーンで軽量かつ保守可能に保つことは、ドメインモデルを大いに利用しようとするアプリケーションにとっての、優れたアーキテクチャ上の目標である。

その一方で、次に確認していくとおり、ドメインモデルクラスをインフラストラクチャコードから完全に分離するよう保つという過激な道を選ぶことは (多くの場合、Plain Old Java/CLR Object  (POJO/POCO) ドメインモデルを使用することと見なされている) 、問題があるともいえる。これはたいてい、無骨であまり効果的でない回避策になる。そして、一部の機能はそのような実装が不可能である。

たいていの場合これは、絶対に必要であるのとまったく同じだけのインフラストラクチャコードをドメインモデルクラスに配置すべきだという妥協の状況を抱えていることを示しているようだ。我々は、必要なドメインモデル管理機能の効率性や使用可能性を向上させるために (そうしないと機能しないため) 、ドメインモデルにおけるいくらかの膨張を妥協している。良い妥協点を交渉することは、結局のところ、ソフトウェアアーキテクチャに関するところの大部分を占めている。

リファクタリングすべき時

残念ながら、ここで提供した妥協点は、長期的にあなたを持ちこたえさせるには不十分かもしれない。非常に有用かつ強力な機能の多くをサポートするためには、大量のインフラストラクチャコードをドメインモデルクラスに配置する必要がある。その量は、本番環境に使うレベルのシステムに達する前にビジネスロジックコードがおぼれてしまうほど、本当に大きい。

つまり、同時に2つの利益を享受する方法が見つからない限りはそうだ。この記事では、実際にインフラストラクチャコードをドメインモデルクラス自体に散らかすことなく、ドメインモデル上に必要なインフラストラクチャコードを分散させる方法がないか、検証を試みる。

まず、関連するすべてのインフラストラクチャコードをドメインモデルクラスに配置するアプリケーションを考察する。その後、よく知られた実証済みのオブジェクト指向の設計パターンのみを使用して、アプリケーションを以前と同じ機能性を持つように、ただしインフラストラクチャコードをドメインモデルクラス自体に散らかすことなく、リファクタリングする。最後に、さらに簡単な方法で同じ効果を達成するために、どのようにAOP  (Aspect Oriented Programming; アスペクト指向プログラミング) を使用できるかについて考察する。

ただし、DMM要件に対処する際になぜAOPが我々にとって役に立つのかを確認するために、まずはAOPなしでコードがどのように見えるかを考察する。最初は、すべてのインフラストラクチャコードがドメインモデルクラス自体に配置されている「最も未加工な」形式で、その次に、インフラストラクチャコードがドメインドメインモデルクラスから取り除かれたが依然としてドメインモデル上に分散されている「リファクタリングした」形式で、考察する!

ドメインモデル肥満症のリファクタリング

ドメインモデルのランタイム管理の多くは、インターセプションに基づいている。つまり、コード内のドメインモデルオブジェクトにアクセスするとき、オブジェクトに対するすべての呼び出しが、必要な機能によってインターセプトされる。

その明らかな例が、ダーティ (dirty) 追跡である。アプリケーションの多くの部分にとって、オブジェクトが変更されたがまだ保存されていない時 (「ダーティ」状態) を知ることは有益といえる。ユーザーインターフェイスはこの情報を使用して、保存されていない変更をユーザーが破棄しようとした場合に、ユーザーに警告することができる。一方、永続性メカニズムはその情報を使用して、どのオブジェクトを実際に永続媒体に保存する必要があるのかを検出することで、すべてのオブジェクトを保存しなければならない状況を回避できる。

これを行う1つの方法は、現存しているドメインオブジェクトの元の変更されていないバージョンのコピーを取っておき、オブジェクトが変更されたかどうかを知りたいたびに、それらと比較することだ。この方法の問題点は、メモリの無駄遣いと処理の遅さである。より効果的なアプローチは、ドメインオブジェクトのsetterメソッドに対するすべての呼び出しをインターセプトして、オブジェクトのsetterメソッドが呼び出されるときはいつでも、そのオブジェクトに対しダーティフラグが立てられるようにすることである。

ダーティフラグをどこに配置するか?

我々は今、ダーティフラグの配置場所に関する疑問に直面している。1つの選択肢は、オブジェクトをキーとして、フラグを値として使用し、何らかのディクショナリ構造にダーティフラグを配置することだ。この方法の問題点は、そのディクショナリを、必要とする可能性のあるアプリケーションのすべての部分にアクセスできるようにしなければならないことである。我々は、ユーザーインターフェイスや永続性メカニズムといったアプリケーションのさまざまな部分を含めることができるということを、すでに目の当たりにしている。.


図1 

ディクショナリをこれらのコンポーネントのいずれかの内部にすることは、他の部分へのアクセスを困難にする。下位層が上位層を呼び出すことができない (他のすべての層が呼び出すことのできる共有の垂直層に属することが多い中央ドメインモデルは除く) 階層構造では、ダーティフラグを、それにアクセスする必要がある最下層に配置するか (図1) 、または共有の垂直層に配置するか (図2) 、いずれかが必要となる。どちらの選択肢も、アプリケーションのコンポーネント間に必要以上の結合と不均等な責任配分を取り込むため、あまり魅力的ではない。


図2

より魅力的で、オブジェクト指向の考えに合う選択肢は、各ドメインオブジェクトがダーティかどうかを示すBoolean dirtyフィールドを運ぶように、ダーティフラグをドメインオブジェクト自体に配置することだ (図3) 。これで、ドメインオブジェクトがダーティかどうかを知る必要があるどんなコンポーネントも、それを簡単に尋ねることができる。


図3

したがって、インフラストラクチャ機能のコードのいくつかをドメインモデルに配置したい理由の一部は、必要以上に結合を増やすことなく、アプリケーションのさまざまな部分からこれらの機能にアクセスできるようにしたいということだ。UIはダーティフラグについて永続コンポーネントに尋ねる方法を知らずに済み、我々は階層化アプリケーションアーキテクチャにおいてできるだけ少ない垂直層を作り出すことを好む。

これは重要な理由で、我々がこの記述でこれから検証しようとしているアプローチを考えるうえで、ある人にとってはそれだけで十分なものかもしれないが、ここでは他にもあることを見ていこう。しかしその前に、この主張の裏側、つまり我々がインフラストラクチャコードをドメインモデルクラスに配置することについて制限的でありたいと思う理由について少しのぞいてみよう。

ドメインモデル肥満症アンチパターン

ダーティフラグの導入後にドメインクラスがどのように見えるかについてと、適切な時にフラグを立てるために必要なインターセプションについて注目しよう。この例はC#コードである。

public class Person : IDirty
{
protected string name;
public virtual string Name
{
get { return name; }
set
{
if (value != name)
((IDirty)this).Dirty = true;
name = value;
}
}

private bool dirty;
bool IDirty.Dirty
{
get { return dirty; }
set { dirty = value; }
}
}

public interface IDirty
{
bool Dirty { get; set; }
}
リスト1

この例から分かるように、ダーティフラグはインターフェイス (IDirty) に定義され、それが、その後クラスに明示的に実装される。明示的なインターフェイスの実装は、インフラストラクチャ関連のメンバーを持つクラスに対しデフォルトAPIを散らかすのを避けることができる優れたC#機能である。

これはたとえば、明示的にIDirtyインターフェイスにキャストしない限りDirtyフラグがコード補完ドロップダウンに表示されないVisual Studio IDEで役立つ。事実、リスト2で分かるように、Dirtyプロパティにアクセスするために、オブジェクトをIDirtyインターフェイスにキャストする必要がある。

例の中のインターセプションコードは、2行のコードに制限されている (リスト2) 。しかしそれは、今までのところ例の中で1つのプロパティしか保持していないからである。より多くのプロパティを保持する場合、比較を適切なフィールドに対して行われるよう変更するたび、各セッターで2行のコードを繰り返す必要がある。

if (value != name)
((IDirty)this).Dirty = true;
リスト2

これは記述できないほど困難ではなかった。我々のドメインモデルは、ドメインモデルを使用するアプリケーションのすべてのコンポーネントにアクセスしやすい方法で、かつ、変更されていないバージョンのオブジェクトのコピーを必要としないためリソース効率が良く処理が早い方法で、ダーティ追跡をサポートしている。

このアプローチのマイナス面は、ドメインモデルクラスが表すことになっていたビジネス上の事項に厳密に焦点を合わせられないということだ。急に多くのインフラストラクチャコードをドメインモデルクラス内に持つため、実際のビジネスロジックコードを識別しにくくし、クラス自体をもろく、変化に抵抗し、一般に保守しにくくする。

ダーティ追跡が、ドメインモデルクラスに挿入された何らかのインフラストラクチャコードを必要とする (または、それを持つことで少なくとも利益を得ることができる) 唯一のインフラストラクチャ機能であったなら、過剰に心配することはないかもしれない。しかし残念ながら、これはそうではない。このような機能のリストは続くが、例には表1に一覧したものが含まれる。:

ダーティ追跡 オブジェクトは、オブジェクトが変更されたが保存されていないかどうかを示すDirtyフラグを運ぶ。インターセプションおよび新しいメンバー (dirtyフィールドとそのgetterおよびsetterメソッド) の導入に基づく。
遅延ローディング オブジェクト状態は、最初にオブジェクトがアクセスされるまでロードされない。インターセプションおよび新しいメンバーの導入に基づく。
オリジナル値追跡 プロパティが変更されると (ただし保存されていない) 、オブジェクトは変更されていない値のコピーを持ち運ぶ。インターセプションおよび新しいメンバーの導入に基づく。
逆プロパティ管理 双方向関係を示すプロパティペアは自動的に同期状態に保たれる。インターセプションに基づく。
データバインディング オブジェクトはデータバインディングインターフェイスをサポートし、そのためデータバインディングシナリオで使用できる。新しいメンバーの導入 (データバインディングインターフェイスの実装) に基づく。
複製 オブジェクトへの変更は待機しているシステムに配信される。インターセプションに基づく。
キャッシング オブジェクト (または少なくともその状態、Mementoパターン[GoF Design Patterns]を参照) は、ローカルストレージにコピーされ、それが、その後のリクエストでリモートデータソースの代わりにクエリー (問い合わせ) される。インターセプションおよび新しいメンバーの導入に基づく。
表1

これらすべての機能 (およびそれ以上の可能性もある) が、ドメインクラスに追加されるインフラストラクチャコードに依存しているため、クリーンで保守可能なドメインモデルの夢が、急に遠くに見えてくる。

私はこの問題を、ドメインモデルクラス内のコードが扱いにくくなるくらい大きくなるという点で、Obese Domain Model Anti-Pattern (ドメインモデル肥満症アンチパターン) [Helander Obese Domain Model]のように説明した。これは、Martin Fowler氏が説明した、ドメインモデルクラスにほとんどロジックが含まれないという Anemic Domain Model Anti-Pattern (ドメインモデル貧血症アンチパターン) [Fowler AnemicDomainModel] に対する、ある種のカウンタウェイトを提供する。

ドメインモデル貧血症の記述で、Fowler氏は、ドメインモデルの外にビジネスロジックを置くことは、オブジェクト指向の概念と構造を十分に活用できないため、いかに間違いであるかを説明している。

私は彼の主張に賛成する。私はさらに、この主張がビジネスロジックコードとまったく同様にインフラストラクチャコードにも適用されるということを示唆する。また、インフラストラクチャコードは、ドメインモデル上に分散されることによってオブジェクト指向の概念と構造を利用することも可能だ。インフラストラクチャコードをドメインモデルに配置することを激しく避けることで、我々はドメインモデル貧血症アンチパターンに関連した問題にとらわれてこのコードを持つという危険を冒すことになる。

インフラストラクチャコードをドメインモデルに追加することを許可しない (完全にオブジェクト指向を利用しない) ことから生じるこうした制限は、我々がすでに見てきた方法で明らかだ。インフラストラクチャ機能はアプリケーション内のさまざまなコンポーネント間で共有しにくくなり、インターセプションベースのダーティ追跡を行わずにオリジナルの値と比較するなどの、あまり効果的でない「回避策」に終わることが多い。遅延ローディングや逆プロパティ管理などの機能もあるが、インターセプションなしではまったく動作できない。

では、その解決策は何か? すべてのインフラストラクチャコードをドメインモデルクラスに配置すると、「肥満症」になり、作業および保守しにくくなる。しかし、インフラストラクチャコードをドメインモデルクラスの外に配置すると、インフラストラクチャコードがオブジェクト指向で提供される可能性を十分に利用できない、ドメインモデル「貧血症」になってしまう。

我々は、板ばさみになっているように見える。より具体的に言うと、保守できないドメインモデル肥満症と、ドメインモデル貧血症に伴う効果的でない回避策の間で板ばさみになっている。これは明らかに、特別に良い場所ではない。この窮地を脱してリファクタリングする方法がないかという昔からの質問に、もう対処すべきときである。

インフラストラクチャベースクラスの使用

1つのアイデアは、このインフラストラクチャコードをできるだけ多く共通のベースクラスに移動し、ドメインモデルクラスがそれを継承できるようにすることだ。しかし、これに関する主な問題点は、新しいインフラストラクチャメンバー (ダーティフラグなど) をドメインモデルクラスに導入する場合に便利な一方で、サポートしたい多くの機能に必要となるインターセプションが提供されないということだ。

ベースクラスのアプローチに関する別の問題は、機能がどのように適用されるかを制御するようになる場合に必要なレベルの粒度が提供されない可能性があることだ。さまざまなドメインモデルクラスがさまざまなインフラストラクチャ要件を持つ場合、我々は問題に直面する。これは、一連の異なるベースクラスを使用する (おそらく互いに継承する) ことで解決できるかもしれないが、C#やJavaの場合のように単一のクラス継承がある場合は、ドメインモデル自体で継承を使用することも望む場合、ベースクラスを使用できない可能性がある。

なぜこれが問題になるのかを確認するために、例を拡大してもう少し「ジューシー」にしてみよう。ダーティ追跡を可能にすべきPersonクラスと、ダーティ追跡および遅延ローディングの両方を可能にすべきEmployeeクラスがあると仮定しよう。最後に、EmployeeクラスはPersonクラスを継承する必要がある。

もし、コードをドメインモデル内に直接配置することに満足していたら、Personクラスはリスト1のように、Employeeクラスはリスト3のようになっていただろう。

public class Employee : Person, ILazy
{
public override string Name
{
get
{
if (!loaded)
{
((ILazy)this).Loaded = true;

//この永続コンポーネントを呼び出し
//これをデータソースからデータと共に
//オブジェクトをロードさせる
//(コードが簡略化されている)
}
return base.Name;
}
set
{
if (!loaded)
{
((ILazy)this).Loaded = true;

}
base.Name = value;
}
}

private decimal salary;
public decimal Salary
{
get
{
if (!loaded)
{
((ILazy)this).Loaded = true;

}

return salary;
}
set
{
if (!loaded)
{
((ILazy)this).Loaded = true;

}

if (value != salary)
((IDirty)this).Dirty = true;

salary = value;
}
}

private bool loaded;
///
///呼び出されたプロパティは"write-once"である
///これをセットした後、二度と間違ったものに
///セットすることはできない

///
bool ILazy.Loaded
{
get { return loaded; }
set
{
if (loaded)
return;

loaded = value;
}
}
}
リスト3

お分かりのように、ドメインモデルクラスにおける実際のビジネス上の事項をおぼれさせる恐れのあるインフラストラクチャコードの膨張は、すでに我々に忍び寄り始めている。この、未来に潜むドメインモデル肥満症の兆しは、我々を他のアプローチを試みる気にさせる。たとえそれが、前もってもう少し努力を要するアプローチだとしてもだ。我々は、クリーンなドメインモデルが長期的に保守性とユーザビリティへの投資を返すことを願っているので、これを実行に移す。

ダーティフラグを提供できるDirtyBaseベースクラスを作成することから始め、その後、ロードされたフラグを提供できるLazyBaseベースクラスを作成する。1つの言語と複数の実装継承で作業して、図4に描かれているように、PersonクラスがDirtyBaseクラスを継承でき、EmployeeがDirtyBaseとLazyBaseクラスを継承できるようにする。


図4

しかし、もし使用したい言語が複数の実装継承をサポートしていなかったらどうなるか? 我々が最初に実行できるのは、LazyBaseがDirtyBaseを継承できるようにすることだ (図5を参照) 。このようにして、EmployeeクラスがLazyBaseクラスを継承できるようにし、ダーティフラグとロードされたフラグの両方を得ることができる。最適な解決策ではないかもしれないが (遅延ローディングを必要とするがダーティ追跡を必要としないオブジェクトを持つ場合はどうなるか?) 、少なくともこの事例では1つの選択肢になる。


図5

しかし、それはまだ、EmployeeクラスがC#やJavaのような言語でPersonクラスとLazyBaseクラスの両方を継承できないという問題を残している。これらの言語における残りの選択肢は、ダーティフラグとロードされたフラグの両方を含むベースクラスを作成することと (図6) 、Personクラスが実際に必要としないダーティフラグを継承できるようにすることだ。


図6

ベースクラスのアプローチには、次の2つの問題がある。多くの機能に必要なインターセプション (ダーティ追跡や遅延ローディングを含む) を提供しないことと、 (少なくとも単一の実装継承プラットフォームで) ドメインクラスに必ずしも必要というわけではない機能を継承させることだ。

インフラストラクチャプロキシサブクラスの使用

幸い、オブジェクト指向は一石二鳥の素晴らしい方法を提供する。インターセプションと制御の粒度の両方を提供するために実行すべきことは、ドメインモデルクラスを継承するサブクラスを作成することだ。つまり、各 (明確な) ドメインモデルクラスに1つの新しいサブクラスを作成することである。

サブクラスは、インターセプション関連のアクティビティを実行することから開始し、次に実行をベースクラスメンバーに伝えることに取り掛かるバージョンでメンバーをオーバーライドすることにより、ベースクラスのメンバーへのアクセスをインターセプトできる。

すべてのサブクラスで共通のインフラストラクチャメンバー (ダーティフラグなど) を作成しなければならない状態を避けるために、すべてのドメインクラスが継承する共通のベースクラスにそれらを配置できる一方で、いくつかのクラスに特定の機能が、必要なメンバーを関連したサブクラスに配置できる (図7) 。


図7

共通のベースクラスはすべてのドメインモデルクラスに共通のインフラストラクチャメンバーを実装する。すべてのドメインクラスに共通でない機能はインターフェイスを取得する。

ドメインモデルクラスはインフラストラクチャコードからクリーンな状態である。

プロキシサブクラスは、インターセプションを提供するためにドメインクラスメンバーをオーバーライドする。また、すべてのドメインクラスに共通でないインフラストラクチャメンバーを実装する。

このようにサブクラスを使用してインターセプションを提供することは、本質的にはProxyパターン[GoF Design Patterns]の実装である。サブクラスの使用に代わる手段は、プロキシクラスとドメインクラスの両方が実装できる、ドメインモデルインターフェイス (各ドメインモデルクラスに1つ) を使用することだ (図8) 。


図8

プロキシクラスは、内部フィールドにドメインクラスのインスタンスを保持し、隠れたドメインオブジェクトにすべての呼び出しを転送することでドメインインターフェイスを実装する。これは事実、ProxyパターンがGang of Four [GoF Design Patterns]によるオリジナルの設計パターン本で説明されている方法である。しかし、サブクラスを使用するのは同じくらい有効なアプローチだ。

サブクラスを使用するメリットは、必要でないかもしれない多くのドメインインターフェイスを作成しなくてもよいことである。また、プロキシサブクラスのコードが「this」キーワード (VB.NETでの「Me」) を使用してドメインクラスを継承したコードを要求できるメリットがある一方で、インターフェイスベースプロキシのコードは、ドメインオブジェクトへの参照を保持している内部フィールドを通じて、ドメインオブジェクトを参照する必要がある。また、サブクラスは、リフレクションによってインターフェイスベースプロキシが到達すべき保護されたメンバーに到達できる。

この問題に関するさらなるねじれは、インターフェイスベースプロキシで、「this」への参照を返すドメインクラスのメソッドを持つ場合、その効果はドメインオブジェクトの「非プロキシ」インスタンスが呼び出し元のコードに返されることであり、つまり、ダーティ追跡や遅延ローディングなどの機能は返されたインスタンスで機能しないということである。

こうした問題のため、私は個人的にサブクラスベースプロキシのアプローチを好んでおり、これは、この記事を通して考察を続けていく方式である。ただし、この記事で議論するすべての手法は、インターフェイスベースプロキシのアプローチを使用して実装することが好都合であるということを覚えておいてほしい。

POJO/POCOドメインモデル

先ほど議論したProxyパターンベースのアプローチを、前のC#でのコード例と比較すると、リスト4の最初に見られるように、ドメインクラスは完全にクリーンで、すべてのインフラストラクチャコードから解放される。リストの最後に見られるように、すべてのインフラストラクチャコードが、ベースクラスとプロキシサブクラスに移動された。

//Domain modelクラス
public class Person : DomainBase
{
protected string name;
public virtual string Name
{
get { return name; }
set { name = value; }
}
}

public class Employee : Person
{
protected decimal salary;
public virtual decimal Salary
{
get { return salary; }
set { salary = value; }
}
}

//Infrastructure Baseクラス
public class DomainBase : IDirty
{
private bool dirty;
bool IDirty.Dirty
{
get { return dirty; }
set { dirty = value; }
}
}

//Infrastructure Proxyサブクラス
public class PersonProxy : Person
{
public override string Name
{
get { return base.Name; }
set
{
if (value != this.name)
((IDirty)this).Dirty = true;
base.Name = value;
}
}
}

public class EmployeeProxy : Employee, ILazy
{
public override string
{
get
{
if (!loaded)
{
((ILazy)this).Loaded = true;

//この永続コンポーネントを呼び出し
//これをデータソースからデータと共に
//オブジェクトをロードさせる
//(コードが簡略化されている)

}
return base.Name;
}
set
{
if (!loaded)
{
((ILazy)this).Loaded = true;

}

if (value != this.name)
((IDirty)this).Dirty = true;

base.Name = value;
}
}

public override decimal Salary
{
get
{
if (!loaded)
{
((ILazy)this).Loaded = true;

}
return base.Salary;
}
set
{
if (!loaded)
{
((ILazy)this).Loaded = true;

}

if (value != this.salary)
((IDirty)this).Dirty = true;

base.Salary = value;
}
}

private bool loaded;
///
///呼び出されたプロパティは"write-once"である
///これをセットした後、二度と間違ったものに
///セットすることはできない

///
bool ILazy.Loaded
{
get { return loaded; }
set
{
if (loaded)
return;

loaded = value;
}
}
}
リスト4

Proxyパターンを潜在的に共通のベースクラスと組み合わせて使用することで、我々が見てきた問題、そしてこれまでドメインモデル貧血症または肥満症アンチパターンの方向から我々をあざけていた問題のすべてにうまく対処できる。

  • アプリケーションのすべての部分から簡単にアクセスでき、オブジェクト指向かつ効果的な方法で実装できるように、インフラストラクチャの関連部分をドメインモデル上に分散できる。
  • ドメインモデル肥満症に陥る危険を冒さずに、好きなだけ多くのインフラストラクチャコードをドメインモデル上に分散できる。事実、実際のドメインモデルクラスのコードを完全にクリーンなままに保ち、得ようとしているビジネス面に完全に焦点を合わせることができる。
  • 各ドメインモデルクラスに必要なインフラストラクチャコードだけを追加するよう、うまく組み合わせることができる。
  • インターセプションに基づいた機能を構築できる。

POJO/POCOの厳密な定義を使用すると、ドメインモデルクラスはインフラストラクチャベースクラスを継承すべきではないということに注意する必要がある。しかし、ロジックのすべてを共通のベースクラスからプロキシサブクラスに移動することで、これを回避できる。そうすれば完全にPOCJO/POCOドメインモデルになる。それが我々のアプローチで容易に実現可能であるというよりも必要条件であるなら、もう少し作業がある。それが必要条件でないなら、ベースクラスを使用して、ドメインモデルクラスのコードが実際のインフラストラクチャコードから完全に解放されることを確認できる。

この時点で我々は、POJO/POCOドメインモデルクラスと、インフラストラクチャコードをドメインモデル上に分散させる目標を、完全に組み合わせることができるアーキテクチャを有している。また、我々の目標が単にドメインモデル肥満症を回避することだけで、必ずしもPOJO/POCOの定義を満たすわけではない場合に、自分たちの作業を減らすため、共通のベースクラスを使用する「半POJO/POCO」の道を選ぶこともできる。

Abstract Factoryパターンの使用

素晴らしく聞こえますか? このアプローチには少しも問題がないのか、と不思議に思うかもしれない。ソフトウェア産業は人々をシニカルにすることがあるため、多分あなたは、ちょうど今頃、頭をもたげる1つか2つの難問を疑っているだろう。

そして、あなたは正しいだろう。2つの懸念がすぐに浮かぶ。1つ目の懸念はかなり小さい方である。サブクラスがドメインモデルクラスのメンバーをオーバーライドしてインターセプションを提供できるためには、すべてのドメインモデルメンバー (または、少なくともインターセプトできることを望むすべてのもの) が仮想にされなくてはならない。

2つ目の問題は潜在的により深刻だ。アプリケーションがプロキシサブクラスのインスタンスを扱うようにしたいので、あなたは、ドメインモデルクラスの1つの新しいインスタンスを作成する、アプリケーションコードのすべての場所を訪問し、関連するプロキシサブクラスのインスタンスが代わりに作成されるようにそれを変更する必要がある。

それが半分悪夢であり、既存のアプリケーションで行う変更のように思えるなら、それはまさしくその通りだからである。この大規模な検索と置換の操作を回避する唯一の方法があり、それは、最初からドメインモデルクラスのインスタンスを作成するために「new」キーワードを使用することを回避することである。

「new」を回避する従来からの方法は、Abstract Factoryパターン[GoF Design Patterns]を利用することである。クライアントコードに「new」を使用してインスタンスを作成させる代わりに、FactoryクラスでCreate () メソッドを呼び出す。Create () メソッドは「new」に対する呼び出しを行うことを担い、また、新しい (new) インスタンスに追加の設定関連の操作を行う場合もある。

ドメインモデルに対する呼び出しでずっとAbstract Factoryパターンを使用した場合の素晴らしい部分は、ドメインクラスのインスタンスを返すことから、プロキシサブクラス (さらに言えば、インターフェイスベースのプロキシ) のインスタンスを返すことに変更するために、コード内のたった1箇所、すなわちFactoryクラスを変更できるようになることである。

これは、少なくとも、C++のようにメンバーアクセス演算子をオーバーライドしたり、ObjectiveCのように「new」キーワードの動作を変更したり、Rubyのようにランタイム時にクラスを変更するなどの特殊な機能を許可しないJavaやC#などの言語では、すべてのドメインオブジェクトインスタント化に対しAbstract Factoryパターンを使用するという強い主張である。

継承の検討

Proxyサブクラスのアプローチについて言及する価値のある問題がもう1つある。それは本当にめったに発生しない厄介なシナリオであるが、かなり微妙な問題であるため、何がそれを引き起こしているかを知らずにその問題に陥ると、ひどくこたえる恐れがある。

図9を見ると、ちょうど、EmployeeクラスがPersonクラスを継承していることが分かる。メソッドがPersonオブジェクトを期待するときはいつでも、Employeeオブジェクトを渡すことが同様にうまくいく。さらに、PersonProxyクラスはPersonクラスを継承する。これは、Personオブジェクトを期待するメソッドにPersonProxyオブジェクトをパラメータとして渡すことが「合法的」であることを意味するので、良い。


図9

同様に、EmployeeProxyはEmployeeを継承し、これはつまり、Employeeオブジェクトを期待するメソッドにEmployeeProxyオブジェクトを渡すことができることを意味する。最後に、EmployeeProxyはEmployeeを継承し、EmployeeはPersonを継承するため、つまり、EmployeeProxyがPersonを継承することを意味する。したがって、Personオブジェクトを期待するメソッドは、EmployeeProxyオブジェクトを受け入れることもできる。

これはすべて我々が希望し、期待するとおりのことだ。Abstract Factoryがドメインモデルクラスのプレーンなインスタンスではなくプロキシオブジェクトを突然返し始めた場合、クライアントコードがタイプ階層の処理方法を考え直す必要なく、通常どおり機能し続けることは、我々にとって重要である。

言い換えれば、Employeeオブジェクトを (合法的に) 渡したPersonオブジェクトを期待するメソッドを持っている場合、代わりにEmployeeProxyオブジェクトを突然渡した場合にコードはコンパイル (および機能) し続けなければならない。幸い、EmployeeProxyはEmployeeを継承し、EmployeeはPersonを継承するので、これはうまく機能し続けるだろう。

事実、ドメインモデルオブジェクトの代わりにプロキシを使用し始めると、希望どおり、すべてが素晴らしく機能し続けるだろう。つまり、ほんの小さな1つのことを除く、すべてがである。

図9の赤い継承線は、EmployeeProxyがPersonProxyを継承しないことを表している。これはいつ問題を起こす可能性があるのか?

では、2つのオブジェクトを受け入れ、どちらのオブジェクトがもう一方のオブジェクトのサブクラスに属するかを決定するためにリフレクションを使用するメソッドについて考えてみよう。PersonオブジェクトとEmployeeオブジェクトをこのメソッドに渡すと、trueが返されるだろう。しかし、PersonProxyオブジェクトとEmployeeProxyオブジェクトをこのメソッドに渡すと、突然falseが返されるだろう。

継承階層を検査するリフレクションベースのコードを持っていない限り、安全である。しかしリフレクションベースのコードを持っている場合は、この警告に注意しても損はない (プロキシを検出し非プロキシタイプが見つかるまでタイプ階層をステップアップするよう、リフレクションコードを変更することで回避できるだろう) 。

MixinおよびInterceptorクラスの使用

我々は、ランタイムドメインモデル管理に対し有能で効果的かつアクセス可能なインフラストラクチャを実現する、保守可能なドメインモデルアーキテクチャへと長い道のりをやって来た。

さらにできることがあるか?

まずは、すべてではなく一部のドメインクラスだけに適用すべき機能、すなわち必要なインフラストラクチャメンバーを共有のベースクラスではなくプロキシサブクラスに配置する必要がある機能に注目しよう。我々の例では、遅延ローディング機能用のロードされたフラグは、遅延ローディングをサポートする必要があるすべてクラスのサブクラスに追加されなくてはならない。

複数のサブクラスに同じコードを配置することが必要になる可能性もあるため、コード複製となる明らかな危険を冒している。1つのフラグの場合はそれほど悪くないかもしれないが、複数のフィールドや複雑な実装を持つメソッドさえも必要とする機能ではどうだろうか?

例では、ロードされたフラグは、trueに切り換えられた後で元に切り換え戻すことができないように、1度しか記述できない (write-once) 。したがって、この規則を実施するためにsetterメソッドにコードを含めている。それは、遅延ローディングを必要とした1つ1つのドメインモデルクラスのサブクラス内のロードされたフラグに対し、setterメソッドで繰り返す必要があるコードである。

我々が行いたいことは、このロジックを含む再利用可能なクラスを作成することだ。しかし、すでに継承は使い果たした (単一の継承プラットフォームで作業していると仮定する) 。では、どうしたらこの遅延ローダークラスを再利用できるのか? 

1つの適切な答えは、これに対して継承ではなくComposite パターン [GoF Design Patterns] を使用することだ。このパターンを使用すると、EmployeeProxyサブクラスには、再利用可能な遅延ローダークラスのインスタンスへの参照を持つ内部フィールドが含まれる (図10) 。


図10

再利用可能なクラスは、実装が継承階層の一部になるのではなくタイプに混ぜ込まれる (mix) という事実を反映して、mixinと呼ばれることが多い。mixinを使用する効果は、単一の実装継承言語で (または、COM+などのインターフェイス継承のみをサポートするプラットフォーム上でも) 複数の実装継承を使用できるというようなことが多い。

この議論についての興味深い追記は、複合アプローチを使用すると、mixinの形式の動作と状態は、 (問題となっている動作が継承されるといった場合のように) ターゲットに静的に結合されるのではなく、 (Dependency Injectionを使用して) ターゲットクラスに動的に混ぜ込まれるということだ。この記事ではこの可能性をどのように利用できるかについてこれ以上考察しないが、それは非常にフレキシブルでダイナミックなシナリオをサポートする幅広い機会を与える。

再利用可能なmixinクラスにサブクラスによって取り込まれるメンバーを準備することで、結合性が低くコード再利用の高い可能性を持った真にモジュール式の、首尾一貫した、まとまりのあるアーキテクチャに、さらにもう一歩踏み込む。我々はこの方向にさらにもっと進むことができるか?

さて、次にすることは、サブクラスからインターセプションコードを準備して、それを再利用可能なクラスにも配置することである。これらのinterceptorクラスは、図11に示したように、mixinと同じようにプロキシサブクラスに含まれる。


図11

Person、Employee、およびDomainBaseクラスは最後のバージョンのコード以降変化していないが、PersonProxyとEmployeeProxyクラスは変化しており、我々は2つの新しいinterceptorクラスと1つの新しいmixinクラスを取り入れた。リスト5は、このようなリファクタリングを実行した後にコードがどのように見えるかを示している (変更されていないクラスは除く) 。

//これらのproxyサブクラスは退屈な、決まり切ったコードだけである。
//全ての実際のロジックは、mixinとインターセプタクラスへ
//リファクタリングされた。

public class PersonProxy : Person
{
private DirtyInterceptor
dirtyInterceptor = new DirtyInterceptor();

public override string Name
{
get { return base.Name; }
set
{
dirtyInterceptor.OnPropertySet(this, this.name, value);
base.Name = value;
}
}
}

public class EmployeeProxy : Employee, ILazy
{
//このmixinにはILazy interfaceの実装が入っている。
private ILazy lazyMixin = new LazyMixin();

private LazyInterceptor lazyInterceptor = new LazyInterceptor();
private DirtyInterceptor dirtyInterceptor = new DirtyInterceptor();

public override string Name
{
get
{
lazyInterceptor.OnPropertyGetSet(this, "Name");
return base.Name;
}
set
{
lazyInterceptor.OnPropertyGetSet(this, "Name");
dirtyInterceptor.OnPropertySet(this, this.name, value);
base.Name = value;
}
}

public override decimal Salary
{
get
{
lazyInterceptor.OnPropertyGetSet(this, "Salary");
return base.Salary;
}
set
{
lazyInterceptor.OnPropertyGetSet(this, "Salary");
dirtyInterceptor.OnPropertySet(this, this.name, value);
base.Salary = value;
}
}

//ILazy interfaceはmixinへの呼び出しが転送されたものが
//実装されている。

bool ILazy.Loaded
{
get { return lazyMixin.Loaded; }
set { lazyMixin.Loaded = value; }
}
}

//以下のmixinとinterceptorクラスは
//トラッキングとロード機能に関連した
//すべての実際のインフラ関連のロジックが
//含まれている。

public class LazyMixin : ILazy
{
private bool loaded;
///
///呼び出されたプロパティは"write-once"である
///これをセットした後、二度と間違ったものに
///セットすることはできない

///
bool ILazy.Loaded
{
get { return loaded; }
set
{
if (loaded)
return;

loaded = value;
}
}
}

public class DirtyInterceptor
{
public void OnPropertySet(
object obj,
object oldValue,
object newValue)
{
if (!oldValue.Equals(newValue))
{
IDirty dirty = obj as IDirty;
if (dirty != null)
dirty.Dirty = true;
}
}
}

public class LazyInterceptor
{
public void OnPropertyGetSet(object obj)
{
ILazy lazy = obj as ILazy;
if (lazy != null)
{
if (!lazy.Loaded)
{
lazy.Loaded = true;

}
}
}
}
リスト5

実際のすべてのインフラストラクチャロジックがmixinおよびinterceptorクラスになるようにリファクタリングすることで、プロキシサブクラスは、interceptorとmixinに呼び出しを転送することに集中する、かなりスリムで軽量のクラスに変わる。実際、この時点でプロキシサブクラスは、コード生成ツールで容易に生成できる、ボイラープレートコード (繰り返し使用する定型コード) で構成されている。

1つの選択肢は、設計時にプロキシサブクラス用のコードを生成することであるが、C#やJavaなどの言語では、ランタイム時にコードを生成し、コンパイルし、実行することが許可される。したがって、ランタイム時にプロキシサブクラスを生成する選択肢もある。この慣習はランタイムサブクラス化として一般に知られている。

これまでに我々が進んできた過程を振り返るための時間をとろう。最初のステップでは、インフラストラクチャコードをドメインモデル上に分散できるようにプロキシサブクラス (および任意で、ベースクラス) を使用して、ドメインモデル肥満症からPOJO/POCOドメインモデルへ方法をリファクタリングすることができた。2つ目のステップでは、すべての実際のインフラストラクチャロジックをプロキシサブクラスから離して再利用可能なmixinに実装し、プロキシサブクラスにボイラープレートコードだけが残るようにリファクタリングした。

最後のステップでは、ランタイムコードジェネレータを使用してボイラープレートプロキシサブクラス用のフルコードを生成することに向かう。そしてここに至り、アスペクト指向プログラミングの地へはるばるやって来た。

AOP (アスペクト指向プログラミング) の使用

とても長い間、私はあらゆるAOP騒動に興味を抱き、AOPとは何なのか (主に、AOPコミュニティがそのような奇妙な用語を使用することにこだわる理由) について研究しようとするたび、結局ただ完全にけむに巻かれたようになっていた。また、一部である (すべてではない!) かのように感じることは何の救いにもならない。AOP支持者は、その事柄を心騒がせるほど異なるものであり脅威であるかのように見せることに、ことさら努力しているようにみえる。

したがって、この記事をさらに読み、実際にすでにアスペクト指向プログラミングの主要な概念すべてを理解しており、AOPフレームワークが起動のためにボンネットの下でどのように機能できるかについて理解していたならば、それを学ぶことはあなたにとって意外であるかもしれない。

アスペクト指向プログラミングは、単に導入 (mixin) とアドバイス (interceptor) の集合であるというアスペクトの主要概念を使用する。アスペクト (interceptorとmixin) は、いくつかの可能な手法の中で特にランタイムサブクラス化を使用して、ランタイム時に既存のクラスに適用できる。

お分かりのように、あなたはすでに、奇妙だが重要なAOP用語の大半を理解している。どのアスペクトが良いかを説明するためによく使われる横断的関心事 (crosscutting concerns) という用語は、多数のクラスに適用すべき機能、たとえばドメインモデルにまたがる関心事であるダーティ追跡や遅延ローディング機能などがある (必ずしも継承階層を共有するわけではない) ことを単に意味している。

今までのところ我々が取り上げていない唯一の本当に重要なAOP用語は、ジョイントポイント (join point) とポイントカット (pointcutting) である。ジョイントポイントとは、interceptorまたはmixinを適用することができるターゲットクラス内の場所である。通常、interceptorに関するジョイントポイントはメソッドであるのに対し、mixinに関するジョイントポイントはクラス自体である。ジョイントポイントは、一言で言えば、あなたがアスペクトに参加できるポイントである。

どのアスペクトをジョイントポイントに適用すべきかを定義するために、ポイントカット言語を使用する。この言語は、単にxmlファイルでアスペクトとジョイントポイントの組み合わせを指定する、宣言型言語のような単純なものにもなるが、時に、正規表現や本格的なDomain Specific Languages (ドメイン特化言語) を使用した、より高度な「ポイントカット」 (ジョイントポイントのターゲット) も可能にする。

今までは、アスペクト (mixinおよびinterceptor) を希望の場所に正確に手動で適用してきたため、ポイントカットは我々にとって関心事ではなかった。しかし、AOPフレームワークを使用してアスペクトを適用するための最終ステップに進みたいと思うなら、この事についても考慮する必要がある。

しかしその前に、立ち止まって提案されていることについて考えてみよう。リファクタリングを行った後の、この時点でAOPフレームワークを使用することは、実際少しも思い切った措置ではない。すでにinterceptor (アドバイス) とmixin (導入) の形式でアスペクトを使用して効果的にアプリケーションをリファクタリングしたため、それは非常に小さなステップである。

実際、AOPを事実上使用している時点で、まだAOPフレームワークを使用してボイラープレート部分を自動化していない。しかし、AOP (interceptionとmixin) からのすべての本質的な概念を使用しており、横断的関心事に対処する再利用可能な方法でそれを行っている。それはまさしくAOPの定義のように聞こえるため、AOPフレームワークが、手動で行わなければならない作業の自動化に役立つということは決して驚くことではない。

現在interceptorとmixinクラス内にあるコードは、最低限の変更でアスペクトとして機能するようにできる。それはすでに、一般的で再利用可能な形式にリファクタリングしてあるため、アスペクトを記述することになるのだ。AOPフレームワークに必要な唯一のことは、何らかの方法でアスペクトをドメインモデルクラスに適用することを助けることだ。述べたとおり、AOPフレームワークに対するその方法は数多くあるが、1つの方法はランタイム時にプロキシサブクラスを生成することだ。

ランタイムサブクラス化フレームワークの場合では、生成したプロキシサブクラス用のコードは、我々の例のプロキシサブクラス内のボイラープレートコードによく似たようなものになる。言い換えれば、容易に生成できるということだ。自分にコードジェネレータを記述する能力があると思っているなら、半ば自身のAOPフレームワークのためにエンジンを実装することに向かっている。

アスペクトへのリファクタリング

最後のリファクタリングで、AOPフレームワークを使用してinterceptorとmixinを適用した場合に我々の例のアプリケーションがどのように見えるかに注目しよう。この例では、Roger Johansson氏と私が一緒に記述した.NET用のオープンソースのAOPフレームワークである、NAspectを使用する。NAspectは、ランタイムサブクラス化を使用してアスペクトを適用する。

これまでにすでに読んでいるため、NAspectが披露する「マジック」を理解するのにまったく苦労しないであろう。それは、NAspectが、mixinとinterceptorに呼び出しを転送するために必要なボイラープレートコードでプロキシサブクラスを生成するということと、標準的なオブジェクト指向によるドメインクラスメンバーのオーバーライドを使用してインターセプションを提供するということだ。

多くの人々がAOPを悪いブードゥーマジックと関連付ける。その奇妙な用語が失望させるのに十分でないなら、物事がinterceptorで「目につかないほど」突然に生じ、開発者がドメインクラスコードでそれを確認できないと言えば、多くの人は非常に不快でほぼ攻撃的と感じるだろう。

それは、用語が意味することを具体的に理解する助けとなる。また、アスペクトを適用できる少なくとも1つの方法を理解し、さらにそれが大したマジックではなく、いくつかの優れた、古くよく知られたオブジェクト指向の設計パターンにすぎないということを確認する助けとなる。

AOPは、横断的関心事に対処できる方法で、よく知られたOO (オブジェクト指向) パターンを使用することだ。AOPフレームワークは、多数のプロキシサブクラスを手動で記述しなければならない方法よりも楽にアスペクトを適用できることだ。

もはやAOPに恐れを感じなくなった今、それをどのように使用してアプリケーションのゴールラインへの最後のステップをもたらすことができるか見てみよう。

最初に行うことは、この場合、NAspectフレームワークアセンブリへの参照を含めることだ。それを適切に行うと、アスペクトの作成とポイントカットの定義を開始できる。また、先へ進みPersonProxyとEmployeeProxyクラスを削除できる。これから先、NAspectがそれらのクラスを生成することになる。

必要なら、DomainBaseベースクラスを削除することさえできる。すべてのベースクラスに共通のmixinを適用するためにベースクラスを使用する必要はないし、それはAOPフレームワークを使用して行ったほうがよい。つまり、AOPフレームワークを使用することにより、追加作業なしでPOJO/POCOの厳しい要件さえ満たすことができることを意味している。

ベースクラスを削除するためには、単に機能性をmixinに移動する。我々の例では、単にもうひとつmixinを作成すればよいだけだ。それは、ベースクラスに以前存在していたダーティフラグを保持するDirtyMixinだ。

LazyMixinクラス内のコードはまったく変更されないままのため、リストされない。事実、本当にすべきことは、新しいDirtyMixinクラスを作成することと、2つのinterceptorクラス内のコードを、NAspectによってそれらに渡されるインターセプトされたメソッドを示すデータ構造で機能させるために、若干変更することである。

また、factoryクラスを、NAspectを使用してプロキシサブクラスを作成するように変更する必要がある。これは、アプリケーションのいたる所でAbstract Factoryパターンを使用してすでに乗り越えたと仮定している。もしまだ乗り越えていなかったら、Abstract Factoryパターンなしで、コード内の各場所、すなわちプロキシサブクラスがインスタンス作成されNAspectランタイムサブクラス化エンジンに対する呼び出しに変化する場所を乗り越えるために、膨大な検索と置換の操作に取り組まなければならなかったため、今確実に教訓を得たであろう。

リスト6に、NAspectとともに使用する準備ができた、リファクタリングしたコードを示す。

public class DirtyMixin : IDirty
{
private bool dirty;
bool IDirty.Dirty
{
get { return dirty; }
set { dirty = value; }
}
}

public class DirtyInterceptor : IAroundInterceptor
{
public object HandleCall(MethodInvocation call)
{
IDirty dirty = call.Target as IDirty;

if (dirty != null)
{
//新しい値を呼び出し物から抽出する
object newValue = call.Parameters[0].Value;

//実行結果から現在の値を抽出する
object value = call.Target.GetType().GetProperty(
call.Method.Name.Substring(4)).GetValue(
call.Target, null);

//新しい値が古い値と異なっていれば
//
dirtyとマークする
if (!value.Equals(newValue))
dirty.Dirty = true;
}

return call.Proceed();
}
}

public class LazyInterceptor : IAroundInterceptor
{
public object HandleCall(MethodInvocation call)
{
ILazy lazy = call.Target as ILazy;

if (lazy != null)
{
if (!lazy.Loaded)
{
lazy.Loaded = true;

}
}

return call.Proceed();
}
}

public class Factory
{
public static IEngine engine = ApplicationContext.Configure();

public static Domain.Person CreatePerson()
{ return engine.CreateProxyPerson>();
}

public static Domain.Employee CreateEmployee()
{
return engine.CreateProxyEmployee>();
}
}
リスト6

アスペクトを適用するクラスを知るために、NAspectは (他にも選択肢はあるが) アプリケーション構成ファイル内のxml構成セクションを使用する。この時点で、mixinおよびinterceptorクラスのタイプと、それらに適用したいタイプを指定することにより、xmlでアスペクトを定義することが可能だ。原理上は、AOPアプリケーションを試してみる準備が整っている。

しかし、多くの開発者が、AOPの理論と用語をよく理解していてもAOPの使用をためらう要因が1つある。それは、弱く定義されたポイントカットシステムに伴う脆弱性である。

アスペクトを適用したいクラスとメンバーだけをターゲットにできるいくつかの巧みな正規表現を定義できるが、ドメインモデルが変化した場合におそらくあなたは製品仕様を更新することを忘れるだろう。そのため突然、正規表現はアスペクトが適用されていないクラスをターゲットにすることになる。これは単にAOPにブードゥー技術としての悪評を与えるものだ。

このポイントカットの不正確性に対処する1つの方法はカスタムの.NET属性 (Javaの注釈) を作成することだ。カスタム属性でドメインモデルクラスをデコレートし、その属性をポイントカットとして使用することで、アスペクトを適用するために正規表現などを使用するような奇術を回避できる。アスペクトは、属性を配置するにより意識的に適用することを決定した場所にのみ適用される。

我々の例では、クラスをデコレートするために使用する2つのカスタム属性、LazyAttributeとDirtyAttributeを作成することから始める (リスト7) 。

public class DirtyAttribute : Attribute
{
}

public class LazyAttribute : Attribute
{
}

[Dirty]
public class Person : DomainBase
{
protected string name;
public virtual string Name
{
get { return name; }
[Dirty]
set { name = value; }
}
}

[Dirty]
[Lazy]

public class Employee : Person
{
protected decimal salary;
public virtual decimal Salary
{
[Lazy]
get { return salary; }
[Dirty]
[Lazy]

set { salary = value; }
}

public override string Name
{
[Lazy]
get { return name; }
[Dirty]
[Lazy]

set { name = value; }
}
}
リスト7

最終的に、アスペクトがカスタム属性をターゲットとするようにアプリケーション構成ファイルでポイントカットを定義することに取り掛かる (リスト8)。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>

<section
name="naspect"
type="Puzzle.NAspect.Framework.Configuration.NAspectConfigurationHandler, Puzzle.NAspect.Framework.NET2"/>

</configSections>

<!-- Puzzle.NAspect.Framework settings -->
<naspect>
<configuration>

<aspect
name="DirtyAspect"
target-attribute="InfoQ.AspectsOfDMM.Attributes.DirtyAttribute, InfoQ.AspectsOfDMM" >

<pointcut
target-attribute="InfoQ.AspectsOfDMM.Attributes.DirtyAttribute, InfoQ.AspectsOfDMM" >

<interceptor
type="InfoQ.AspectsOfDMM.Aspects.DirtyInterceptor, InfoQ.AspectsOfDMM" />

</pointcut>

<mixin type="InfoQ.AspectsOfDMM.Aspects.DirtyMixin, InfoQ.AspectsOfDMM"/>

</aspect>

<aspect
name="LazyAspect"
target-attribute="InfoQ.AspectsOfDMM.Attributes.LazyAttribute, InfoQ.AspectsOfDMM" >

<pointcut
target-attribute="InfoQ.AspectsOfDMM.Attributes.LazyAttribute, InfoQ.AspectsOfDMM" >

<interceptor
type="InfoQ.AspectsOfDMM.Aspects.LazyInterceptor, InfoQ.AspectsOfDMM" />

</pointcut>

<mixin type="InfoQ.AspectsOfDMM.Aspects.LazyMixin, InfoQ.AspectsOfDMM"/>

</aspect>

</configuration>
</naspect>
</configuration>

これでついに、AOPアプリケーションを試してみる準備が整った。アプリケーションを実行すると、NAspectがランタイム時にプロキシサブクラスを生成し、ボイラープレートコードをサブクラスに挿入してinterceptorおよびmixinクラスに対するすべての呼び出しを転送する。

アプリケーションの最終的なアーキテクチャは図12で確認できる。このバージョンとAOPでない以前のバージョンの間にある唯一の実際の違いは、2つのプロキシクラス (点線で印されている部分) がAOPフレームワークによって生成されるのに対し、以前はそれらを手動でコード化する必要があった点である。

図11と比較すると、DomainBaseベースクラスの代わりに追加のDirtyTrackerMixinクラスと新しいIDirtyTrackerインターフェイスが存在するが、これは、 (厳しいPOJO/POCO要件を満たす) 共通のインフラストラクチャベースクラスなしで進めることを決定していたらAOPを使用しなくても存在していたものである。言い換えれば、共通のベースクラスを使用したくない場合は、AOPフレームワークを使用するしないに関わらず、まったく同じアーキテクチャ (図12に示したもの) になるということだ。

図12

ポイントカットのオプション

新しいドメインモデルクラスを加える場合、それらに対して遅延ローディングとダーティ追跡を有効にするために必要なことは、それらのクラスをカスタム属性でデコレートすることだ。すると、これらの機能はそのクラスに「魔法のごとく自動的に」適用される。

属性/注釈をポイントカットターゲットとして使用した場合、適用したい機能ごとに1つの属性を使用するとドメインモデル内の各メンバーに膨大な量の属性をもたらす傾向があることにすぐに気付くだろう。したがって、属性の数を減らすためには、それらの間の注釈を探すとよい。

正規表現ベースのポイントカットアプローチに抵抗がなく、ドメインモデルを属性で散らかすことはドメインモデルをすべてのインフラストラクチャの懸念から完全に免れるように保つという目標を損なうと感じる場合、別の選択肢として、関連するターゲットクラスに一致するようxml構成ファイルを設定することができ、ドメインクラスをカスタム属性でデコレートする必要がなくなる。

また、(他の多くのAOPフレームワークのように) NAspectのxmlポイントカット言語は、アスペクトを適用すべきタイプの完全なリストを簡単に提供することを可能にするため、適切なクラスに何とか一致する正規表現を探す必要がない。このため、より冗長な構成ファイルになるが、ドメインモデルを完全にクリーンな状態に保つことができる。.

ポイントカットを使用してアスペクトを動的に適用するAOPフレームワークを使用することのさらなるメリットは、さまざまなユースケースで使用する場合に異なるアスペクトを同じクラスに簡単に適用できるようになることだ。あるユースケースでは特定のドメインクラスに遅延ローディングを必要とし、別のユースケースでは必要としない場合、最初のユースケースのみが、遅延ローディングアスペクトを適用する構成ファイルを使用する必要がある。

この動的なアスペクトの適用は特に、たとえばあざけるような機能を提供するインスタンスに対し、追加テストアスペクトを適用できるテストシナリオにおいて、非常に強力である。

アスペクト指向プログラミングの使用による結論

最初、インフラストラクチャコードをドメインモデルに配置することが許されるかどうかの質問から開始した。そして、これは多くの機能のより効果的な実装を可能にするため望ましいことだが、ドメインモデルを「肥満症」にさせる恐れがあるということを確認した。

この問題を解決するために、ドメインモデル肥満症をリファクタリングした。まず、インフラストラクチャコードをプロキシサブクラスとベースクラスに移動して、その後実際のロジックをプロキシサブクラスからmixinおよびinterceptorクラスに移動した。これによりドメインモデルを鍛え上げたが、たくさんのボイラープレートコードをプロキシサブクラスに記述するはめになった。これを解決するために、アスペクト指向プログラミングに目を向けた。

AOPフレームワークを使用することへのステップは非常に小さいと判明した。事実、アプリケーションアーキテクチャは少しも変わらなかった。これは、一部の読者にとっては恐らく驚きであっただろう。そして読者は、初めて設計パターンを理解し、名前があることを知らずに頻繁に行っていたことであると気付いたときによく生じる、身近な感覚すら経験するかもしれない。

多分、あなたはAOPを多くのアプリケーションで、それとは気付かずに、言わば、ボイラープレートコード生成の一部を自動化できるAOPフレームワークを使用せずに、行っていただろう。

この記事の終わりに到達した今、あなたが次のことを同意してくれることを願う。:

  • AOPは見掛けほど困難でもややこしくもなく、AOPフレームワークの使用は簡単である。
  • AOPフレームワークを使用するかどうかに関係なく、AOPの概念とモデル化手法を利用することは、アプリケーションインフラストラクチャにおけるドメインモデル管理の横断的関心事に対処するための優れた方法である。

それは単に、優れた昔ながらのオブジェクト指向パターンを使用して、アプリケーションアーキテクチャを着実に向上させる方法をリファクタリングするだけのことだ。そのときは、恐れずにアスペクト指向と呼ぶことのできるアプリケーションアーキテクチャに移行し、プロキシを使用してinterceptorとmixinを適用してほしい。

アスペクトの適用にAOPフレームワークの助けを借りるかどうかは、AOPを行っているかどうかに影響を与えない。なぜなら、この記事で説明した方法に沿ってアプリケーションをリファクタリングしている場合、フレームワークの助けがあるなしに関わらず、AOPを行っているからである。

まとめ

Proxyパターン、Abstract Factoryパターン、Compositeパターンなどの従来からのよく知られたオブジェクト指向設計パターンを使用してドメインモデル肥満症をリファクタリングすることにより、スリムなドメインモデルと、オブジェクト指向の概念と構造をフル活用できるインフラストラクチャを優雅に組み合わせたアプリケーションアーキテクチャに到達した。これにより、ドメインモデル肥満症とドメインモデル貧血症のいずれの罠にも陥らないようにきちんと回避した。

この記事に説明した方法でパターンを採用すると、十分にアスペクト指向と呼ぶことのできるアプリケーションアーキテクチャがもたらされる。これは、横断的関心事に対処するmixinとinterceptorクラスが、アスペクト指向プログラミングに見られる導入とアドバイスの概念に厳密に一致するからである。

AOPの使用が本当に楽しくて簡単であることを自分の目で確かめたいが、すべてのコードを入力しなくてもよいと思っている場合は、付随のVisual Studio 2005プロジェクトでこの記事での完全なコードをダウンロードできる。

要約すれば、この記事で私は、アスペクト指向の概念とツールが、多くのドメインモデル管理の関心事に関する優れた考え方と適用方法をどのように提供できるかについてと、それらがドメインモデル肥満症アンチパターンのリスクを軽減するのにどう大きく役立つかについて示そうとしてきたのである。

著者について

Mats Helanderは、Avanade Netherlandsでシニアソフトウェア開発コンサルタントとして勤務している。彼は自由時間に、Roger Johansson氏と共に、無料でオープンソースのフレームワークとツールのPuzzle.NETを開発した。Puzzle.NETには、NPersistとObjectMapper  (O/Rマッピング) 、NAspect  (AOP) 、NFactory  (Dependency Injection; 依存性の注入) およびNPath  (メモリ内オブジェクトクエリー) が含まれる。

Mats Helanderのウェブログ - http://www.matshelander.com/wordpress (英語)
Puzzle.NET - http://www.puzzleframework.com (英語)
Avanade Netherlands - http://www.avanade.com/nl/  (英語)

参考文献

[Evans DDD]
Evans, Eric. Domain Driven Design: Tackling Complexity in the Heart of Software. Boston, MA: Addison-Wesley, 2004.

[Fowler AnemicDomainModel]
Fowler, Martin. http://martinfowler.com/bliki/AnemicDomainModel.html

[Fowler PoEAA]
Fowler, Martin. Patterns of Enterprise Application Architecture. Boston, MA: Addison-Wesley, 2003.

[GoF Design Patterns]
Gamma, Erich, Richard Helm, Ralph Johnson, and John M. Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software. Reading, MA: Addison-Wesley, 1995.

[Helander DMM]
Helander, Mats. http://www.matshelander.com/wordpress/?p=30

[Helander Obese Domain Model]
Helander, Mats. http://www.matshelander.com/wordpress/?p=75

[Nilsson ADDDP]
Nilsson, Jimmy. Applying Domain-Driven Design and Patterns. Addison-Wesley, 2006.


ソースコードはこちらから参照可能です(source)

原文はこちらです:http://www.infoq.com/articles/aspects-of-domain-model-mgmt

この記事に星をつける

おすすめ度
スタイル

BT