時間的オブジェクト

時間とともに変化するオブジェクト

これは、2000年代半ばに行っていた「Further Enterprise Application Architecture development」の執筆の一部です。残念ながら、他の多くのことに気を取られてしまい、それ以上取り組む時間がありませんでした。また、近い将来も時間を見つけられる見込みはありません。そのため、この資料は非常にドラフト版であり、再び取り組む時間ができるまで、修正や更新は行いません。

オブジェクトに時間的なプロパティを持たせたいときもあれば、オブジェクト自体を時間的なものと考えたいときもあります。良い例としては、一連の修正を経る契約が挙げられます。それぞれの修正を、新しい条件を持つ新しい契約と考えることもできますし、同じ契約のバージョンと考えることもできます。

どのように機能するか

このパターンは非常に一般的ですが、いくつかの形式で現れます。これらの形式に取り組む上で、ロール分析を使用すると非常に役立つことがわかりました。このパターンには、本質的に2つの役割があります。1つは継続性であり、もう1つはいくつかのバージョンです。

各バージョンは、ある期間のオブジェクトの状態をキャプチャします。オブジェクトのプロパティの値が変更されるたびに、新しいバージョンが作成されます。したがって、バージョンを、日付範囲を処理するための有効性を持つオブジェクトのリストとして想像することができます。

継続性は、バージョン変更を通して継続するオブジェクトを表します。それは、オブジェクトが変化を通して存在していることを考えるときに、人々が参照するオブジェクトです。

バージョンの時間的なプロパティ以外に、継続性はデータを持たないか、継続性の全期間を通じて本当に不変のデータのみを持ちます。ただし、他のオブジェクトがデータを収集するためのポイントとして機能します。現在の値には、現在のバージョンに委譲するgetメソッドで簡単にアクセスできます。履歴値には、いくつかの方法でアクセスできます。継続性がバージョンのプロパティの時間的プロパティを示すこともできますし、継続性がスナップショットを提供することもできます。両方を組み合わせることもでき、一般的な値には時間的プロパティのインターフェースを提供し、残りの値にはスナップショットを提供するなどです。

ユーザーにバージョン履歴を明示的に変更させたい場合は、編集目的でバージョンへの直接アクセスを提供することもできます。ただし多くの場合、継続性を通してすべてのアクセスを制御することになります。

このパターンを扱う上で非常に奇妙なのは、これをモデル化する方法の選択肢があまりにも多いことです。

私が持っているクレジットカードの契約を考えてみましょう。私は1997年2月1日にクレジットカードを取得し、いつものように理解不能な契約書が付属しており、二度と見られないようにファイルキャビネットに保管します。1998年4月15日に、改訂された契約書が郵送で送られてきて、同じように扱われます。したがって、1枚のクレジットカードと2つの(バージョンの)契約があることになります。

1つの方法は、各契約をクレジットカードに結び付けられた事実上別個の契約として扱うことです。多くの場合、これには時間的オブジェクトはまったく含まれていません。代わりに、契約の時間的プロパティを持つクレジットカードを明示的に持つことを考えています。

図1:各クレジットカードを個別の契約として扱う

2つ目の選択肢は、クレジットカードに契約があり、その契約自体に契約バージョンのコレクションがあると言うことです。各契約を別個のものとして扱うこととの違いは、ビジネスプロセスに対するビジネスの見方にある場合がほとんどです。1つの利点は、企業が異なる契約の顧客を持っている場合、契約オブジェクトがその概念を置くのに明確な場所になることです。もちろん、企業は(ゴールド、プラチナ、ベースメタルなどの)クレジットカードタイプなど、この目的のための他の概念を持っている可能性があり、タイプごとに1つの契約しか持っていない可能性があります。その場合、少なくとも表現に関しては、契約オブジェクトの値は減ります。しかし、動作には依然として価値があります。

図2:明示的なバージョンを持つ契約

図2は、パターン内の両方の役割に明示的なクラスがあるため、時間的オブジェクトパターンの最も明白な形式です。ただし、オブジェクトが継続性とバージョンの1つの両方を果たす場合、状況は少し不明確になります。

図3:契約と修正 - クラス図。

この良い例は、図3のようなモデルです。ここでは、契約が修正を持っており、それが契約であるため、独自の修正を持つことができます。この場合、契約クラスは継続性とバージョンの両方の役割を果たします。通常、チェーン内の最初の契約だけが継続性の役割を果たすという概念です。そうすることで、元の契約を参照したものはすべて明確な参照ポイントを持つことになりますが、変更を保持するためにバージョンを使用することもできます。

図3の修正スタイルは、契約がめったに修正されない場合に便利です。その場合、修正がない場合でも常に少なくとも2つのオブジェクトを必要とする図2の明示的なスタイルとは異なり、契約は1つだけです。それにもかかわらず、最近では、責任がより明確に分離されているため、常に明示的な形式を使用する傾向があります。また、明示的な形式は実装に時間的コレクションを使用するのに適しており、リストをトラバースするよりもはるかに簡単であることがわかりました。(ただし、公平を期すために、通常使用されるリスト形式ではなく、修正に時間的コレクションを使用することもできます。)

留意すべきもう1つの側面は、特にOOスタイルのシステムでは、継続性がオブジェクトで表現されていないことが多いということです。たとえば、リレーショナルデータベースでは、バージョンのテーブルのみがあり、継続性のテーブルがない場合があります。このようなリレーショナルモデルでは、継続性は契約番号などのフィールドによって実装されます。契約テーブルの主キーは、この契約番号と有効性の一部(開始日など)を組み合わせたものです。

もう1つの簡単な例は、ソースコード制御システムです。ここで、継続性はバージョン管理されたファイルのファイル名であり、各バージョンは、通常はデルタとして、ソースコード制御システム内の個別のエントリとして保存されます。

いつ使用するか

このパターンをいつ使用するかという最大の疑問は、時間的プロパティを使用する場合と比較することです。この2つには多くの重複があり、実際、継続性のインターフェースは時間的プロパティのセットであることがよくあります。実際、クライアントに関する限り、すべてのプロパティを時間的プロパティにすることと、時間的オブジェクトを使用することのインターフェースはほぼ同じです。

1つの明らかな要因は、時間的プロパティの割合です。ほんのわずかである場合は時間的プロパティを使用し、ほとんどである場合は時間的オブジェクトを使用します。もちろん、それは、わずかであることとほとんどであることの違いの判断をあなたに任せることを意味するだけです。これはイライラしませんか?

もう1つの問題は、ビジネス担当者が情報をどのように見たいかということです。連絡先が明示的な修正を持っていると考えてほしい場合は、プロパティが1つだけ時間的であっても、時間的オブジェクトを使用する価値があります。ビジネス担当者が明示的にバージョンを参照する必要がある場合は、時間的オブジェクトを使用して、参照するバージョンを提供する必要があります。

参考資料

アンダーソンのPlop paperでは、このパターンをHistory on Selfという名前で説明しています。[Arnoldi et al]では、このパターンをVersion Historyという名前で説明しています。

例:明示的な継続性とバージョン(Java)

サンプルコードでは、図2の明示的な形式に従います。顧客バージョンクラスには、顧客に関する期待されるデータが含まれています。

class CustomerVersion...

  private String address;
  private Money creditLimit;
  private String phone;

  String address() {return address;}
  Money creditLimit() {return creditLimit;}
  String phone() {return phone;}

  void setName(String arg) {_name = arg;}
  void setAddress(String arg) {address = arg;}
  void setCreditLimit(Money arg) {creditLimit = arg;}

顧客クラスには、顧客バージョンの時間的コレクション(その動作については時間的プロパティを参照)が含まれており、その単純なgetメソッドは最新バージョンに委譲します。

class Customer...

  private TemporalCollection history = new SingleTemporalCollection();

  public String name() {return current().name();}
  public String address() {return current().address();}
  public Money creditLimit() {return current().creditLimit();}
  public String phone() {return current().phone();}
  
  private CustomerVersion current() {
    return (CustomerVersion)history.get();
  }

顧客の更新に関して、バージョンをどれだけ明示的にしたいかを検討する必要があります。必要なのが単純な現在の加算更新だけの場合、通常のルックアンドフィールな設定メソッドを提供できます。この設定メソッドは、バージョンのコピーを取得し、コピーを更新して、そのコピーを履歴に追加します。

class Customer...

  public void setAddress(String arg) {
    CustomerVersion workingCopy = getWorkingCopy();
    workingCopy.setAddress(arg);
    history.put(workingCopy);
  }
  public CustomerVersion getWorkingCopy() {
    return current().copy();
  }

class CustomerVersion...

  CustomerVersion copy() {
    return new CustomerVersion(_name, address, phone, creditLimit);
  }
  
  public CustomerVersion (String name, String address,
            String phone, Money creditLimit)
  {
    super(name);
    this.address = address;
    this.phone = phone;
    this.creditLimit = creditLimit;
  }

これにより、簡単な設定メソッドで時間的レコードを変更できます。

class Tester...

  public void testSimple () {
    MfDate.setToday(new MfDate (1998, 8, 23));
    martin.setAddress(Damon15);
    martin.setCreditLimit(Money.dollars(100));
    MfDate.setToday(new MfDate (2000, 9,30));
    assertAddresses();
    assertCreditLimits();
  }
  private void assertCreditLimits() {
    assertEquals(Money.dollars(50), martin.creditLimit(new MfDate(1997, 12, 25)));
    assertEquals(Money.dollars(50), martin.creditLimit(new MfDate(1998, 8, 22)));
    assertEquals(Money.dollars(100), martin.creditLimit(new MfDate(1998, 8, 23)));
    assertEquals(Money.dollars(100), martin.creditLimit());
  }
  private void assertAddresses() {
    assertEquals(Franklin963, martin.address(new MfDate(1997, 12, 25)));
    assertEquals(Franklin963, martin.address(new MfDate(1998, 8, 22)));
    assertEquals(Damon15, martin.address(new MfDate(1998, 8, 23)));
    assertEquals(Damon15, martin.address());
  }

ただし、遡及更新の場合、顧客のクライアントはバージョンを認識する必要があります。したがって、遡及更新は、次のようなクライアントによって実行されます。

class Tester...

  public void testWorkingCopy() {
    MfDate.setToday(new MfDate (2000, 9,30));
    CustomerVersion workingCopy = martin.getWorkingCopy();
    workingCopy.setAddress(Damon15);
    workingCopy.setCreditLimit(Money.dollars(100));
    martin.addVersion(new MfDate (1998, 8, 23), workingCopy);
    MfDate.setToday(new MfDate (2000, 9,30));
    assertAddresses();
    assertCreditLimits();
  }