會計交易
連結兩個(或更多)分錄,讓交易中所有分錄的總和為零
2005 年 1 月 22 日
這是 進階企業應用程式架構開發 寫作的一部分,我在 2000 年代中期撰寫。遺憾的是,此後有太多其他事情吸引了我的注意力,所以我沒有時間進一步研究它們,我也看不到未來可預見的未來會有太多時間。因此,這份材料非常草稿,在我有時間再次研究它之前,我不會進行任何更正或更新。
盧卡·帕西奧利對會計師來說可能就像伽利略對物理學家一樣。他是一位方濟會修士,首次描述了會計的一個核心概念:複式簿記。複式簿記背後的想法非常簡單,每筆提款都必須用存款來平衡。因此,您對帳簿所做的每一件事都有兩個要素,從一個帳戶中減去,並添加到另一個帳戶中。這樣,金錢的保存就像物理學家保存能量一樣。對於會計師來說,您不能創造金錢,它只能被挪用。
運作方式
當我們使用交易時,實際上會遇到兩種交易。兩筆交易只會有兩筆分錄,且符號相反。這實際上就是從一個帳戶到另一個帳戶的單一移動。多筆交易允許任意數量的分錄,但仍有總體規則,即所有分錄的總和必須為零,從而保存金錢。

圖 1:兩筆交易的範例

圖 2:多筆交易的類別圖,請注意唯一差異是關聯的多重性:會計交易 -> 分錄

圖 3:多筆交易的範例。這可能表示我用一張存款單將兩張支票存入我的銀行帳戶的情況。
雙腿交易最容易操作,因此,如果業務運作方式如此,即使您可以使用多腿交易支援雙腿交易,也不值得嘗試建立多腿交易。
對於雙腿交易,分錄是可選的 - 您可以選擇將所有資料放在交易中。如果兩個分錄實際上僅在金額符號上不同,則此方法可行。當我住在英國時,從一個帳戶轉帳到另一個帳戶總是需要三天時間。因此,即使我從我的支票(往來)帳戶轉帳到我的儲蓄(存款)帳戶在同一家分行,從我的支票帳戶中提款會在我儲蓄帳戶存款前三天發生。在這種情況下,我們需要分錄,以便我們可以記錄不同的日期。

圖 4:沒有分錄的會計交易類別圖
對於多腿交易,通常困難在於如何建立交易。雙腿交易可以在一個操作中輕鬆建立。然而,多腿交易需要多一點功夫。因此,在適當的帳戶中正確過帳之前,使用建議物件建立交易是值得的。
何時使用
為了回答這個問題,值得思考為什麼複式簿記一開始被視為一個好主意。基本上,一切都建立在發現和防止洩漏,或換句話說,打擊詐欺。沒有複式簿記,讓金錢神秘地出現和消失實在太容易了。當然,複式簿記並不能消除所有詐欺,但它讓發現詐欺變得容易一點,這就足以讓人們使用它。事實上,它已深植於會計架構中,以至於人們在使用時不會想到它。
這並不表示您應該總是使用會計交易。在許多方面,您使用此模式取決於您網域中的人是否使用此模式。首先,只有在您使用帳戶時,使用會計交易才有意義。因此,如果您發現您不使用帳戶,您也不會使用會計交易。
不使用會計交易的另一個原因是,當所有分錄都是由電腦製作時。記錄和追蹤這件事可能滿足所有追查漏洞的渴望。由於你可以檢查原始碼和資料庫記錄,這會給你很多槓桿作用 - 使用會計交易不會提供更多。
因此,你應該由你的領域專家來決定何時使用模式。特別是,如果你領域專家不覺得有必要,你不應該將其用作額外功能。像許多模式一樣,會計交易會增加系統的複雜性,而複雜性會增加其本身的價格。
兩條腿還是多條腿?
如果你決定使用會計交易,那麼你必須決定使用兩條腿還是多條腿版本。多條腿交易給你更大的靈活性,以支援單筆存款可以總和許多提款或反之亦然的分錄。然而,許多應用程式不想要這樣,因為他們的領域只有兩條腿交易。多條腿交易也複雜得多。因此,只有在絕對需要其功能時才使用多條腿交易。
讓多條腿交易支援兩條腿交易很容易,因此從一個重構到另一個通常很容易。因此,從兩條腿開始,稍後再改為多條腿很容易。相反的也很簡單,但如果你不確定,最好從較簡單的開始。
範例:兩條腿範例(Java)
我將為兩條腿和多條腿案例提供範例程式碼,從較簡單的兩條腿案例開始。
的確,兩條腿案例真的只需要一個簡單的會計交易物件。
public class AccountingTransaction { private Collection entries = new HashSet(); public AccountingTransaction(Money amount, Account from, Account to, MfDate date) { Entry fromEntry = new Entry (amount.negate(), date); from.addEntry(fromEntry); entries.add(fromEntry); Entry toEntry = new Entry (amount, date); to.addEntry(toEntry); entries.add(toEntry); }
有了這個,你只需要將分錄的建構函式限制為受限,這樣你才能在交易過程中建立分錄,而不是其他。在 Java 中,這是建構函式的套件存取和編碼慣例的組合。
與其直接使用會計交易建構函式,不如在帳戶物件上提供一個合適的方法更有意義。
void withdraw(Money amount, Account target, MfDate date) { new AccountingTransaction (amount, this, target, date); }
這使得操作的程式碼更容易使用。
public void testBalanceUsingTransactions() { revenue = new Account(Currency.USD); deferred = new Account(Currency.USD); receivables = new Account(Currency.USD); revenue.withdraw(Money.dollars(500), receivables, new MfDate(1,4,99)); revenue.withdraw(Money.dollars(200), deferred, new MfDate(1,4,99)); assertEquals(Money.dollars(500), receivables.balance()); assertEquals(Money.dollars(200), deferred.balance()); assertEquals(Money.dollars(-700), revenue.balance()); }
範例:多條腿範例(Java)
多條腿案例更為尷尬,因為建立多條腿交易需要更多工作,並且需要驗證。在這種情況下,我使用建議物件,以便我可以逐步組合交易,然後在我將所有部分組合在一起後將其過帳到帳戶。
使用這種方法,我需要能夠透過單獨的方法呼叫將分錄新增到交易物件。一旦我擁有所有交易,我就可以將交易過帳到帳戶。在過帳之前,我需要檢查所有分錄是否結餘為零,並且一旦過帳,我就不能再向交易新增任何分錄。
我將從欄位和建構函式開始揭示程式碼。
public class AccountingTransaction { private MfDate date; private Collection entries = new HashSet(); private boolean wasPosted = false; public AccountingTransaction(MfDate date) { this.date = date; }
因此,在此範例中,我有一個整個交易的日期。這無法處理我以前的英國銀行,但這讓事情更容易解釋。
add 方法會將分錄新增到交易,前提是交易尚未過帳。
class Transaction... public void add (Money amount, Account account) { if (wasPosted) throw new ImmutableTransactionException ("cannot add entry to a transaction that's already posted"); entries.add(new Entry (amount, date, account, this)); }
在這種情況下,我使用不同的輸入類別來保留輸入與交易和帳戶之間的雙向關聯。(輸入是不可變的,這使得處理雙向連結變得更容易。)
class Entry... private Money amount; private MfDate date; private Account account; private AccountingTransaction transaction; Entry(Money amount, MfDate date, Account account, AccountingTransaction transaction) { // only used by AccountingTransaction this.amount = amount; this.date = date; this.account = account; this.transaction = transaction; }
一旦我將輸入新增到交易中,我就可以張貼交易。
class AccountingTransaction... public void post() { if (!canPost()) throw new UnableToPostException(); Iterator it = entries.iterator(); while (it.hasNext()) { Entry each = (Entry) it.next(); each.post(); } wasPosted = true; } public boolean canPost(){ return balance().isZero(); } private Money balance() { if (entries.isEmpty()) return Money.dollars(0); Iterator it = entries.iterator(); Entry firstEntry = (Entry) it.next(); Money result = firstEntry.amount(); while (it.hasNext()) { Entry each = (Entry) it.next(); result = result.add(each.amount()); } return result; } class Entry... void post() { // only used by AccountingTransaction account.addEntry(this); }
然後,我可以使用類似這樣的程式碼進行交易。
AccountingTransaction multi = new AccountingTransaction(new MfDate(2000,1,4)); multi.add(Money.dollars(-700), revenue); multi.add(Money.dollars(500), receivables); multi.add(Money.dollars(200), deferred); multi.post(); assertEquals(Money.dollars(500), receivables.balance()); assertEquals(Money.dollars(200), deferred.balance()); assertEquals(Money.dollars(-700), revenue.balance());
所有這些設定和張貼業務都說明了為什麼使用多階段交易如此尷尬。好消息是,如果您只需要有時進行兩階段交易,則可以使用多階段交易來實作兩階段介面。
class Account... void withdraw(Money amount, Account target, MfDate date) { AccountingTransaction trans = new AccountingTransaction(date); trans.add(amount.negate(), this); trans.add(amount, target); trans.post(); }