会计交易

将两个(或更多)条目链接在一起,使交易中所有条目的总和为零

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