差额调整

使用反映记录值与应记录值之间差异的条目调整错误事件。

2005年1月2日

这是我在2000年代中期进行的《进一步的企业应用程序架构开发》写作的一部分。不幸的是,此后太多其他事情吸引了我的注意力,因此我没有时间进一步研究它们,而且在可预见的未来我也没有看到太多时间。因此,这些材料非常草稿形式,我不会进行任何更正或更新,直到我能找到时间再次处理它们。

如果你在发现错误时无法编辑条目,你需要创建新的条目。 反向调整 是一种简单的方法,但会导致大量条目。对于每个原始条目,你都会创建两个额外的条目:一个反向条目和一个替换条目。

使用 差额调整,你只需创建一个条目来进行调整,该条目包含原始条目与该条目应有的值之间的差额。

实际上,你通常可以用一个调整条目来修复多个错误条目,如图 0.25 所示。这不仅减少了需要创建的条目数量,而且还可以使事情更清晰。你可以轻松地看到特定调整造成的差异,而不是必须逐一查看反向和替换。

图 1:使用一个条目调整多个事件。

工作原理

这种方法的复杂之处在于,当你试图弄清楚如何计算这些调整条目时。一种方法是编写专门的代码来计算调整。这样做的问题是,如果不复制大量常规计算代码,就很难做到这一点。

我见过一种效果很好的替代方法,就是使用 并行模型。在 并行模型 中,使用 反向调整 来确定正确的条目应该是怎样的。然后,比较 并行模型 中的账户余额与实际账户中的余额,并将差额作为调整进行记账。

这是简短的总结,以下是详细内容。我们从一个客户开始,该客户有一个使用账户,其中包含我们现在知道是错误的条目。这些条目是在过去几个月内创建的,我们正在于 11 月 20 日处理调整。

第一步是创建一组影子账户。本质上,这意味着复制使用账户。

现在,使用 反向调整 对影子账户处理新的事件。

然后,我们比较影子账户和实际账户的余额,并为差额记账。

何时使用

我倾向于看到这种模式在使用 账户 时使用。这是因为你需要确定替换条目中的哪些条目与哪些原始条目匹配,以便你可以计算两者之间的差额。要匹配条目,你需要获取具有相同鉴别符的条目。当你有账户时,这最容易,因为这样只有一个鉴别符。调整条目的值只是每个账户的实际账户和影子账户余额之间的差额。

即使如此,在 反向调整差额调整 之间进行选择并不简单,通常取决于领域专家希望如何思考调整的执行方式。如果他们希望看到明确的抵消,请使用 反向调整,如果他们更喜欢摘要,请使用 差额调整。幸运的是,从 反向调整差额调整 的重构并不太难。反向重构也是可能的,尽管重建反向数据介于复杂和不可能之间。

示例:调整单个用电量(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;
  }

相同的示例代码也适用于调整多个事件,但自然地,设置代码会稍微长一些。

进行差额调整的逻辑在调整事件的 process 方法中。

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;
   }

我让客户负责影子创建(以及稍后记账调整)。这可能是客户不合适的责任,但让调整复制账户会暴露太多关于客户内部的信息。这是一个取决于系统处理其责任的特定方式的决定之一,因此我不能对此进行一般性的说明。你必须根据自己的情况选择影子知识所在的正确位置。

现在我们在 并行模型 中操作,我们可以通过反转原始事件的所有条目并处理替换来使用 反向调整

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;
  }