Array-like Objects in Ruby with Forwardable

in Ruby

A couple of times recently I’ve needed something that behaved like and array, but with some extra methods that are specific to the context of the list I’m dealing with. For example, an array of transactions with a couple of extra methods for common queries I want to perform against those transactions.

Here I’ll show you how I deal with Array-like objects and why I like using Forwardable to create wrappers around arrays.

Transaction

For this example, we’ll use an array of Transaction objects. Let’s assume our base Transaction class looks like this:

1class Transaction
2  def initialize(data)
3    @amount = data[:amount]
4    @status = data[:status]
5  end
6end

Array

If we have a simple array of Transaction objects, we can query it like this:

1# Select transactions that have settled.
2@transactions.select do |transaction|
3  transaction.status == 'settled'
4end
5
6# Select transactions that are still pending.
7@transactions.select do |transaction|
8  transaction.status == 'pending'
9end

It’s not super pretty, but it works. What I’d like is to shorten this up since there’s a fairly finite set of common queries we’ll perform against this collection.

Forwardable

To this end, we’ll create a new ‘TransactionList’ class that uses Ruby’s Forwardable module to delegate to our array. We know that we’ll want to call ’each’ and ‘select’ on our transactions, so let’s start by delegating those using the delegate method.

1class TransactionList
2  extend Forwardable
3  delegate [:each, :select] => :@transactions
4
5  def initialize(transactions)
6    @transactions = transactions
7  end
8end

Cool, this is shaping up! Let’s add some extra methods to simplify our original queries.

 1class TransactionList
 2  extend Forwardable
 3  delegate [:each, :select] => :@transactions
 4
 5  def initialize(transactions)
 6    @transactions = transactions
 7  end
 8
 9  # Select only transactions that have a 'settled' status.
10  def settled
11    @transactions.select do |transaction|
12      transaction.status == 'settled'
13    end
14  end
15
16  # Select only transactions that have a 'pending' status.
17  def pending
18    @transactions.select do |transaction|
19      transaction.status == 'pending'
20    end
21  end
22end

And now, we can use them like this:

 1transactions = TransactionList.new(array)
 2
 3# An array of Transaction objects that has the status 'pending'
 4transactions.pending.each do |transaction|
 5  puts "Transaction for #{ transaction.amount } still pending"
 6end
 7
 8# An array of Transaction objects that has the status 'settled'
 9transactions.settled.each do |transaction|
10  puts "Transaction settled for #{ transaction.amount }"
11end

Pretty nifty, huh? This is a pretty simplified example but I use this pattern of wrapping arrays in custom ‘List type objects all over the place.

Caveats

Because we’ve only instructed TransactionList to delegate the ’each’ and ‘select’ methods to our transactions array, if we try to call other Enumerable or Array methods on our TransactionList we will get a NoMethodError. Remember, you have to explicitly tell Forwardable what methods to delegate.