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.