時間依存プロパティ
時間とともに変化するプロパティ

2004年3月7日
これは、2000年代半ばに執筆していた「Further Enterprise Application Architecture development(エンタープライズアプリケーションアーキテクチャ開発のさらなる発展)」の一部です。残念ながら、それ以来、他の多くのことが私の注意を引いてしまい、それらをさらに進める時間がありませんでしたし、近い将来にも時間が見込めません。そのため、この資料は非常に草稿段階のものであり、再び作業に取り組む時間を見つけるまでは、修正や更新は行いません。
通常、クラスのプロパティを見ると、オブジェクトに対して*現在*尋ねることができる質問を表しています。しかし、オブジェクトのプロパティについて現在だけでなく、変化が生じた可能性のある過去の時点についても質問したい場合があります。
仕組み
このパターンの鍵は、時間とともに変化するオブジェクトのプロパティを扱うための、規則的で予測可能なインターフェースを提供することです。この最も重要な部分は、アクセサ関数にあります。常に時間ポイントを引数にとるアクセサ関数が見られます。これにより、「1998年2月2日にファウラー氏の住所はどこでしたか?」と尋ねることができます。さらに、通常は引数を取らないアクセサ関数も見られます。これは、今日の値など、何らかのデフォルト値に基づいた住所に関する質問です。
情報にアクセスするだけでなく、情報を更新する必要がある場合も多いでしょう。最も単純な形式の変更は、追加的更新です。追加的更新とは、タイムラインの最後に別の情報を追加するものと考えてください。追加的更新は、時間ポイントと新しい値を引数にとる1つのputメソッドで処理できます。つまり、これは「1998年8月23日からファウラー氏の住所を15 Damon Aveに変更する」という意味になります。指定された日付は、遡及的な変更を示す過去の日付、現在の変更を示す現在の日付、または予定された変更を反映する未来の日付にすることができます。ここでも、今日(または同様のデフォルト値)を変更の有効日として使用する、時間ポイントのないputメソッドを用意すると便利です。これにより、現在の変更を簡単に行うことができます。
もう1つの種類の更新は、挿入更新です。これは、「ファウラー氏が1998年1月に963 Franklin Stに引っ越してきたと記録していましたが、それを1997年12月に変更する必要があります」という形式のものです。ここでのポイントは、これは最後に追加するのではなく、現在持っている時間情報を変更しているということです。このためには、現在持っている時点での値にアクセスし、それを新しい範囲に調整する参照が必要です。ここでは値だけを指定するだけでは不十分です。値は異なる範囲で有効な場合があるからです。私は引っ越ししてから同じ住所に戻ることができます。
例として、私が1985年9月から1987年1月まで154 Norwood Rdに住んでいて、6ヶ月間ブライトンに引っ越し、その後しばらく滞在するために戻ってきたとしましょう。最初の滞在の出発を1月から2月に調整する必要がある場合、2回目の滞在ではなく、最初の滞在を取得することが重要です。そのため、住所と、最初の滞在を識別するもの(実際には、最初の滞在の範囲内の任意の日付)の両方が必要です。
保持している値がバリューオブジェクトである場合、実際には追加的更新だけで済みますが、挿入更新の方が使いやすいかもしれません。
時間依存プロパティを実装するには、2つの方法があります。1つは、有効性を使用してオブジェクトのコレクションを作成し、このコレクションを操作する方法です。ただし、これを複数回行うようになると、この動作を提供する特別なコレクションクラス、つまり*時間依存コレクション*を作成するのが最善の方法であることに気付くでしょう。このようなクラスは非常に簡単に記述でき、時間依存プロパティが必要なときはいつでも使用できます。
いつ使うか
時間依存の動作を示すプロパティがいくつかあるクラスがあり、それらの時間依存の値に簡単にアクセスしたい場合は、時間依存プロパティを使用する必要があります。
この最初のポイントは、簡単なアクセスに関する点です。時間的な変更を記録する最も簡単な方法は、監査ログを使用することです。監査ログの欠点は、ログを処理するために追加の作業が必要になることです。そのため、最初に知っておくべきことは、どのような状況で人々がそのプロパティの履歴を必要とするかということです。通常のプロパティを時間依存プロパティにリファクタリングすることは難しくないことを覚えておいてください。(ターゲットフィールドを時間依存コレクションに置き換え、既存のインターフェースを簡単に維持できます。)
2番目のポイントは、いくつのプロパティが時間依存であるかを検討することです。クラスのプロパティのほとんどが時間依存である場合は、時間依存オブジェクトを使用する必要があります。
参考文献
私は時間依存プロパティについて、[fowler-ap]で*履歴マッピング*という名前で初めて説明しました。その後、[PLoPD 4]の時間パターンに関する論文を準備するにあたり、アンディ・カールソンとシャロン・エステップとの共同作業により、私のアイデアは洗練されました。フランシス・アンダーソンのplop論文でも、このパターンを*アソシエーションの履歴*という名前で説明しています。
例:時間依存コレクションの使用(Java)
時間依存コレクションは、時間依存プロパティを実装する簡単な方法です。時間依存コレクションの基本的な表現とインターフェースは、マップに似ています。日付をインデックスとして使用するget操作とput操作を提供します。実際、マップは時間依存コレクションの優れたバッキングコレクションになります。
class TemporalCollection...
private Map contents = new HashMap(); public Object get(MfDate when) { /** returns the value that was effective on the given date */ Iterator it = milestones().iterator(); while (it.hasNext()) { MfDate thisDate = (MfDate) it.next(); if (thisDate.before(when) || thisDate.equals(when)) return contents.get(thisDate); } throw new IllegalArgumentException("no records that early"); } public void put(MfDate at, Object item) { /** the item is valid from the supplied date onwards */ contents.put(at,item); clearMilestoneCache(); }
マップには、有効になった開始日でインデックス付けされた値が含まれています。milestonesメソッドは、これらのキーを逆順に返します。次に、getメソッドはこれらのマイルストーンを処理して、正しいキーを見つけます。このアルゴリズムは、最新の値を要求する可能性が高い場合に最適に機能します。
時間依存コレクションを更新するよりもアクセスする方が多い場合は、マイルストーンコレクションをキャッシュする価値があるかもしれません。
class TemporalCollection...
private List _milestoneCache; private List milestones() { /** a list of all the dates where the value changed, returned in order latest first */ if (_milestoneCache == null) calculateMilestones(); return _milestoneCache; } private void calculateMilestones() { _milestoneCache = new ArrayList(contents.size()); _milestoneCache.addAll(contents.keySet()); Collections.sort(_milestoneCache, Collections.reverseOrder()); } private void clearMilestoneCache() { _milestoneCache = null; }
時間依存コレクションが記述されると、顧客の住所の時間依存コレクションを簡単に作成できます。
class Customer...
private TemporalCollection addresses = new SingleTemporalCollection(); public Address getAddress(MfDate date) { return (Address) addresses.get(date); } public Address getAddress() { return getAddress(MfDate.today()); } public void putAddress(MfDate date, Address value) { addresses.put(date, value); }
時間依存コレクションを使用する際の最大の問題の1つは、コレクションをリレーショナルデータベースに永続化する必要がある場合です。テーブルへのマッピングはそれほど簡単ではありません。基本的に、リレーショナルデータベースは有効性を使用する必要があります。これは多くの場合、日付範囲の場所として交差テーブルを作成する必要があることを意味します。このコードの一部は時間依存コレクションクラスに一般化できますが、いくつかの明示的なマッピングコードが必要になります。
例:時間両依存プロパティの実装(Java)
時間両依存プロパティの仕組みを考える前に、それが何をすべきかを考える価値があります。基本的に、時間両依存プロパティを使用すると、履歴情報を長期にわたって保存し、両方の次元で完全な履歴を保持できます。そのため、次のような履歴を構築します。
class Tester...
private Customer martin; private Address franklin = new Address ("961 Franklin St"); private Address worcester = new Address ("88 Worcester St"); public void setUp () { MfDate.setToday(new MfDate(1996,1,1)); martin = new Customer ("Martin"); martin.putAddress(new MfDate(1994, 3, 1), worcester); MfDate.setToday(new MfDate(1996,8,10)); martin.putAddress(new MfDate(1996, 7, 4), franklin); MfDate.setToday(new MfDate(2000,9,11)); }
更新のリズムに注目してください。時間両依存履歴を保存する場合、記録日は常に今日です。そのため、テストでは、最初に現在の日付を変更してから、履歴に情報を保存します。履歴に情報を格納するときは、実際の日付を指定します。
結果の履歴は次のようになります。
class Tester...
private MfDate jul1 = new MfDate(1996, 7, 1); private MfDate jul15 = new MfDate(1996, 7, 15); private MfDate aug1 = new MfDate(1996, 8, 1); private MfDate aug10 = new MfDate(1996, 8, 10); public void testSimpleBitemporal () { assertEquals("jul1 as at aug 1", worcester, martin.getAddress(jul1, aug1)); assertEquals("jul1 as at aug 10",worcester, martin.getAddress(jul1, aug10)); assertEquals("jul1 as at now",worcester, martin.getAddress(jul1)); assertEquals("jul15 as at aug 1", worcester, martin.getAddress(jul15, aug1)); assertEquals("jul15 as at aug 10",franklin, martin.getAddress(jul15, aug10)); assertEquals("jul15 as at now",franklin, martin.getAddress(jul15)); }
時間依存プロパティの複雑さの多くを時間依存コレクションを使用して実装できるのと同様に、時間両依存プロパティの複雑さの多くを処理するために時間両依存コレクションを定義することもできます。
基本的に、時間両依存コレクションとは、要素が時間依存コレクションである時間依存コレクションです。各時間依存コレクションは、レコード履歴の画像です。
最初に、コレクションから情報を取得する方法を見てみましょう。最新の時間依存コレクションは、記録日が現在である実際の履歴を表しています。そのため、実際の日付のみを持つ取得メソッドはこの現在の実際の履歴を使用します。
class BitemporalCollection...
private SingleTemporalCollection contents = new SingleTemporalCollection(); public BitemporalCollection() { contents.put(MfDate.today(), new SingleTemporalCollection()); } public Object get(MfDate when) { return currentValidHistory().get(when); } private SingleTemporalCollection currentValidHistory() { return (SingleTemporalCollection) contents.get(); }
(クラスSingleTemporalCollection
は、上記で説明したバニラ時間依存コレクションです。2つの関係については後で説明します。)
真の時間両依存値を取得するには、実際の日付と記録日の両方を持つゲッターを使用します。
class BitemporalCollection...
public Object get(MfDate validDate, MfDate transactionDate) { return validHistoryAt(transactionDate).get(validDate); } private TemporalCollection validHistoryAt(MfDate transactionDate) { return (TemporalCollection) contents.get(transactionDate); }
次に、ドメインクラスで時間両依存コレクションを使用できます。
class Customer...
BitemporalCollection addresses = new BitemporalCollection(); public Address getAddress(MfDate actualDate) { return (Address) addresses.get(actualDate); } public Address getAddress(MfDate actualDate, MfDate recordDate) { return (Address) addresses.get(actualDate, recordDate); } public Address getAddress() { return (Address) addresses.get(); }
コレクションを更新するたびに、実際の履歴の古いコピーを保持する必要があります。
class BitemporalCollection...
public void put(MfDate validDate, Object item) { contents.put(MfDate.today(), currentValidHistory().copy()); currentValidHistory().put(validDate,item); } public void put(Object item) { put(MfDate.today(),item); }
時間両依存コレクションは、1次元時間依存コレクションと非常によく似たインターフェースをサポートしています。そのため、時間依存コレクションのインターフェースを作成し、1次元コレクションと2次元コレクションに対して別々の実装を提供できます。これにより、時間両依存コレクションを1次元時間依存コレクションの代わりに使用できるため、1次元時間依存プロパティを時間両依存プロパティに簡単にリファクタリングできます。
この場合、実際の日付と記録日の両方に日付粒度を持つ時間ポイントを使用しています。これにより、例の作成は簡単になりますが、記録時間にはより細かい粒度を使用する方が良い場合があります。ここでの問題は、午後2時にレコードを変更し、午後3時に請求プロセスを実行し、午後4時に再度変更した場合です。現在の実装では、適切な値は完全に失われるわけではありませんが、取得するのは簡単ではありません。ビジネスでよく使用されるもう1つのアプローチは、営業日の終わりにのみ請求などの処理を行い、請求が行われた後はその日にそれ以上の更新を許可しないことです。請求後の更新には、請求が行われた翌日の記録日が付与されます。繰り返しますが、この種の機能は、適切な時間依存コレクションクラスに簡単に追加できます。