Question

I'm currently working on a DSL in relation to accounting. What I'd like to be able to do is:

accountant do
  credit @account_1, -@amount
  debit  @account_2,  @amount
end

Currently, this executes the following method:

class Accountant
  def accountant &block
    AccountantHelper.class_eval(&block)
  end
end

...Which in turn executes the block on the AccountantHelper class, calling the "credit" and "debit" methods respectively:

class AccountantHelper
  def self.credit account, amount
    account.credit amount
  end

  def self.debit account, amount
    account.debit amount
  end
end

(Please hold back any fire about using class_eval() -- this is only a prototype after all!)

The goal is for the block to act as a transaction, ensuring that if the entire block can't be executed successfully, then none of it should. However in addition to this, it should also verify the integrity of the data passed into the block. In this case I need to verify that there is both a "credit" and a "debit" method within the block (in double-entry accounting, for every credit there must also be at least one debit, and vice versa). Currently I could call:

accountant do
  credit @account_1, @amount
end

...And the code will execute without any errors. This would be a bad thing as there is no corresponding "debit" to keep the accounts in balance.

Is it possible to verify what gets passed into the block? Or am I heading down the wrong path here?

Was it helpful?

Solution

I guess you can make your credit and debit actions "lazy", so that they are executed by the wrapper method, after the validation. Here's a proof of concept, similar to yours, but without metaprogramming part, skipped for clarity:

def transaction
  yield
  if @actions.map(&:last).inject(&:+) == 0
    @actions.each do |account, amount|
      @accounts[account] += amount
    end
    @actions = []
    puts 'transaction executed ok'
  else
    puts 'balance not zero, rolling back transaction'
    # rollback (effectively, do nothing)
  end
end

def credit account, amount
  @actions << [account, amount]
end

def debit account, amount
  @actions<< [account, -amount]
end

@actions = []
@accounts = {a: 0, b: 0, c: 0} # start with three blank accounts

transaction do
  credit :a, 5
  debit :b, 2
  debit :c, 3
end
#=>transaction executed ok

p @accounts
#=>{:a=>5, :b=>-2, :c=>-3}

transaction do
  credit :a, 5
  debit :b, 4
end
#=> balance not zero, rolling back transaction

p @accounts
#=> {:a=>5, :b=>-2, :c=>-3}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top