差额调整
使用反映记录值与应记录值之间差异的条目调整错误事件。
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; }