差異調整
調整錯誤事件,其條目反映記錄內容與應記錄內容之間的差異。

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