Chaining Methods for Cleaner Ruby
July 28, 2023
In Summary (tl;dr)
tap
(docs) will take an object and pass it to a block. The original object is the return value.
then
(docs) will take an object and pass it into a block. The return value from the block is the overall return value.
Once you learn the difference, chaining these methods makes for code that wraps itself up nicely, making it easier to understand.
Introduction
Ruby’s tap
and then
methods are underused. You can chain them to perform additional actions or side effects on objects, leading to code that’s easier to understand.
Depending on how you architect your app, you can also use them to reduce the number of callbacks in your models.
The Power of tap
Let’s say we create an order
and want to send a confirmation email. This is simple enough with a callback:
# callsite, anywhere in the codebase
def process_order(order_data)
Order.create(order_data)
end
# app/models/order.rb
class Order < ApplicationRecord
after_create :send_confirmation_email
private
def send_confirmation_email
OrderMailer.order_confirmation(self).deliver_later
end
end
Beyond the scope of this post, there’s a strong argument against the use of callbacks.
Let’s write this without the callback but without using tap
yet:
# callsite, anywhere in the codebase
def process_order(order_data)
order = Order.create(order_data)
OrderMailer.order_confirmation(order).deliver_later
order
end
This is the “temporary variable sandwich smell” (the name may have just been made up - patent pending).
There’s just a lot of fluff, isn’t there? We’ve had to assign a variable to use it when sending a message to another object, before then leaving it hanging at the end as a return value.
This is a perfect use case for tap
, here’s a rewrite utilising the method:
# callsite, anywhere in the codebase
def process_order(order_data)
Order.create(order_data).tap do |order|
OrderMailer.order_confirmation(order).deliver_later
end
end
Like the previous example, this example still:
- Creates the
order
- Passes the returned value from
Order.create
to theOrderMailer
for processing - Disregards the value returned from the
OrderMailer
- Returns the
order
to the caller
Make no mistake, the code is the same length, but it’s quicker to read (unless you carefully and lovingly read each end
, you freak).
Out in the wild, there are no end of service objects which are basically over-exuberant tap
blocks masquerading as classes with a single public method of call
. Grim.
What about then
, then?
then
is very similar, except it will return whatever the block returns.
Let’s say we were calculating a sales price for an item within an order. We will begin without using then
:
def calculate_final_price(base_price, is_taxable, sales_tax_rate)
price_after_tax = is_taxable ? apply_sales_tax(base_price, sales_tax_rate) : base_price
apply_discount(price_after_tax)
end
private
def apply_sales_tax(price, sales_tax_rate)
price + (price * sales_tax_rate / 100)
end
def apply_discount(price)
price - (price * 0.1) # 10% discount
end
Some variant of this code sits in every Rails code base. Given the amount of times we’ve seen this shape of code, it’s familiar, which makes it readable. Other than the temporary variable, it’s not bad.
However, the “stepped” nature of the calculation is better revealed in the version using then
.
def calculate_final_price(base_price, is_taxable, sales_tax_rate)
base_price
.then { |price| is_taxable ? apply_sales_tax(price, sales_tax_rate) : price }
.then { |price_after_tax| apply_discount(price_after_tax) }
end
# same private methods, snipped for brevity
In this example, a couple of things become clear:
- We are starting with a
base_price
and performing actions on it - There are two “actions” performed on the
base_price
to achieve the final result
We can also see the use case for then
differentiate itself from tap
. We are relying on then
to return the value from the first block to provide the price_after_tax
for the second block. If we were to have used tap
, we would have had base_price
returned.
Embrace Code Elegancy
In a nutshell, tap
and then
can improve your code by wrapping code up within blocks that mean something. With tap
, you can perform actions on objects without affecting their return value, keeping your code neat and readable. then
allows you to ‘step’ your operations, making it easy to follow the logical flow of your code.