差異調整

記録された内容と本来記録されるべき内容の差異を反映したエントリを用いて、誤ったイベントを調整します。

これは、私が2000年代半ばに執筆していた「エンタープライズアプリケーションアーキテクチャ開発の更なる発展」の一部です。残念ながら、それ以降は多くの他の事柄に注意を奪われており、これ以上の作業に取り組む時間がありませんでしたし、近い将来においてもそうなる見込みはありません。そのため、この資料は非常に草案段階のものであり、再び作業に取り組むことができるようになるまで、修正や更新を行う予定はありません。

誤りを見つけたと時にエントリを編集できない場合は、新しいエントリを作成する必要があります。逆転調整はこのための簡単な方法ですが、多くのエントリが生成されます。元の各エントリに対して、逆転エントリと置換エントリの2つを追加します。

差異調整では、元のエントリと本来のエントリとの差異を含む単一のエントリを使用して調整を行います。

実際、図0.25が示唆するように、多くの誤ったエントリを1つの調整エントリで修正できることがよくあります。これはエントリの作成数を削減するだけでなく、状況をより明確にすることができます。逆転と置換を順番に処理する必要がなくなり、特定の調整によって生じた差異を簡単に確認できます。

図1:1つのエントリによる複数のイベントの調整。

動作方法

このアプローチの複雑さは、これらの調整エントリの計算方法を理解しようとする際に発生します。1つのアプローチは、調整を計算する特定のコードを作成することです。これの欠点は、多くの通常の計算コードを複製せずにこれを行うことが困難であることです。

私がうまく機能するのを見た代替案は、並列モデルを使用することです。並列モデル内で逆転調整を使用して、正しいエントリがどうなるべきかを判断します。次に、並列モデル内の勘定科目の残高と実際の勘定科目内の残高を比較し、差異を調整として転記します。

それが簡単な要約です。詳細を以下に示します。まず、誤っていることが判明したエントリを含む利用状況勘定を持つ顧客から始めます。これらは過去数ヶ月間にわたって作成されており、11月20日に調整を処理しています。

最初のステップは、シャドウアカウントのセットを作成することです。基本的に、これは利用状況アカウントのコピーを作成することを意味します。

次に、逆転調整を使用して、シャドウアカウントに新しいイベントを処理します。

次に、シャドウアカウントと実際のアカウントの残高を比較し、差異のエントリを転記します。

いつ使用するのか

アカウントが使用されている場合に、このパターンがよく使用される傾向があります。これは、置換からのどのエントリが元のどのエントリと一致するのかを判断して、両者の差異を計算する必要があるためです。エントリを照合するには、同じ識別子を持つエントリを取得する必要があります。アカウントがあればこれが最も簡単になります。なぜなら、識別子が1つしかないからです。調整エントリの値は、各アカウントについて、実際のアカウントとシャドウアカウントの残高の差異だけです。

それでもなお、逆転調整差異調整の選択は簡単ではなく、通常はドメインエキスパートが調整の実行方法についてどのように考えるかによって異なります。明示的なキャンセルを表示する必要がある場合は逆転調整を使用し、サマリーを好む場合は差異調整を使用します。幸いなことに、逆転調整から差異調整へのリファクタリングはそれほど難しくありません。逆のリファクタリングも可能ですが、逆転データの再構築は、複雑な作業から不可能な作業の間のどこかにあります。

例:単一の電力使用量の調整(Java)

ここでは、電力使用量の例をもう一度取り上げて、差異調整でこれを行う方法を示します。基本的な設定コードを次に示します。

class ExampleTester...

  public void setUp() {
      MfDate.setToday(2004, 4, 1);
      watson = sampleCustomer();
      original = new Usage(Unit.KWH.amount(50),
                           new MfDate(2004, 3, 31),
                           watson);
      eventList.add(original);
      eventList.process();
      MfDate.setToday(2004, 6, 1);
      replacement = new Usage(Unit.KWH.amount(70),
                              new MfDate(2004, 3, 31),
                              watson);
      adjustment = new DifferenceAdjustment(replacement, original);
      eventList.add(adjustment);
      eventList.process();
  }

class DifferenceAdjustment…

  public DifferenceAdjustment(MfDate whenOccurred,
                              Subject subject,
                              List<AccountingEvent> oldEvents,
                              List<AccountingEvent> newEvents)
  {
      super(whenOccurred, subject);
      this.oldEvents = oldEvents;
      this.newEvents = newEvents;
  }

この同じサンプルコードは、複数のイベントの調整にも機能しますが、当然ながら設定コードはやや長くなります。

差異調整を行うためのロジックは、調整イベントのプロセスメソッドにあります。

class DifferenceAdjustment…

  public void process() {
      assert !isProcessed;
      adjust();
      markProcessed();
  }
  void adjust() {
      getCustomer().beginAdjustment();
      reverseOldEvents();
      processReplacements();
      getCustomer().commitAdjustment(this);
      recordSecondaryEvents();
  }

調整の基本的なロジックは、シャドウアカウントを構築し、シャドウアカウントで逆転調整を使用して処理を実行し、次に差異調整を転記することです。

最初のステップは、シャドウアカウントを作成することです。

class Customer...

  public void beginAdjustment() {
      assert ! isAdjusting();
      savedRealAccounts = accounts;
      accounts = copyAccounts(savedRealAccounts);
  }
  private boolean isAdjusting() {
      return null != savedRealAccounts;
  }
  public Map<AccountType, Account> copyAccounts(Map<AccountType, Account> from) {
       Map<AccountType, Account> result = new HashMap<AccountType, Account>();
       for (AccountType t : from.keySet()) result.put(t, from.get(t).copy());
       return result;
   }

顧客にシャドウの作成(および後の調整の転記)の責任を負わせました。それは顧客にとって不適切な責任かもしれませんが、調整によってアカウントをコピーすると、顧客の内部構造があまりにも多く公開されてしまいます。これは、システムが責任を処理する方法に依存する決定の1つであり、これについて一般的に言えることはほとんどありません。独自の状況に応じて、シャドウイングの知識がどこに存在するべきかを適切に選択する必要があります。

これで並列モデルで動作するようになったため、元のイベントのすべてのエントリを反転させて置換を処理することにより、逆転調整を使用できます。

class uses...

  void reverseOldEvents() {
      for (AccountingEvent e : oldEvents)
          e.reverse();
  }
  void processReplacements() {
      for (AccountingEvent e : newEvents) e.process();
  }

class AccountingEvent...

  public void reverse() {
      assert isProcessed();
      for (Entry e : getResultingEntries()) reverseEntry(e);
      for(AccountingEvent ev : getSecondaryEvents()) ev.reverse();
  }

  public void reverseEntry(Entry arg) {
      Entry reversingEntry = new Entry(arg.getAmount().negate(), arg.getDate());
      Account targetAccount = subject.accountFor(arg.getAccount().type());
      targetAccount.post(reversingEntry);
   }

ここでの逆転の対象アカウントは、エントリが存在するアカウントと同じアカウントではありません。なぜなら、シャドウエントリではなく、元のイベントを介して到達した実際のエントリを参照しているからです。ただし、逆転エントリはシャドウアカウントに作成する必要があります。

逆転の転記が完了したら、顧客は差異調整を実際のアカウントに転記できます。

class Customer...

  public void commitAdjustment(Adjustment adjustment) {
      assert isAdjusting();
      for (AccountType t : AccountType.values())
           adjustAccount(t, adjustment);
      endShadowAccounts();
  }
  public void adjustAccount(AccountType type, Adjustment adjustment) {
       Account correctedAccount = accounts.get(type);
       Account originalAccount = savedRealAccounts.get(type);
       Money difference = correctedAccount.balance().subtract(originalAccount.balance());
       Entry result = new Entry(difference, MfDate.today());
       originalAccount.post(result);
       adjustment.addResultingEntry(result);
   }
 public void endShadowAccounts() {
      assert isAdjusting();
      accounts = savedRealAccounts;
      savedRealAccounts = null;
  }