會計交易

連結兩個(或更多)分錄,讓交易中所有分錄的總和為零

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