Skip to Content
Course content

243: Method Chaining and Fluent Interfaces

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You've likely seen this pattern in ActiveRecord or other popular Ruby gems: you call a method, then immediately dot-chain another method onto it, and another, until you have a clean, readable pipeline. That's a fluent interface. The magic behind it is deceptively simple, but if you miss one small detail, the whole thing collapses with a NoMethodError.

To show you how this works, let's build a SearchQuery builder. Imagine we're building a system to filter a product catalog. Instead of passing a giant hash of options into a constructor, we want to build the query piece by piece.

The Clunkiness of Standard Setters

If we did this the "standard" way, our code would look something like this:

query = SearchQuery.new
query.where(category: 'electronics')
query.limit(10)
query.order('price_asc')
results = query.execute

It's not terrible, but it's repetitive. We're repeating the query variable over and over. It feels like we're filling out a form rather than describing a search. I prefer my code to read more like a sentence, and that's where method chaining comes in.

Returning Self to Keep the Chain Moving

The secret to chaining is that each method must return an object that responds to the next method in the chain. In most fluent interfaces, the method simply returns self—the very instance of the object we're currently modifying.

Let's start building our SearchQuery class:

class SearchQuery
  def initialize
    @filters = {}
    @limit = nil
    @sort = nil
  end

  def where(params)
    @filters.merge!(params)
    self # This is the magic line
  end

  def limit(number)
    @limit = number
    self # Return self again
  end

  def execute
    puts "Executing search with: #{@filters}, Limit: #{@limit}, Sort: #{@sort}"
    # In a real app, this would hit the database
    []
  end
end

Now we can do this: SearchQuery.new.where(category: 'books').limit(5).execute. It's concise, and the intent is crystal clear.

Wait, Why Am I Getting a NoMethodError?

Here is where I usually trip up when I'm moving too fast. Let's add an order method to our builder. I'll write it quickly, just like I did in a project last month:

def order(direction)
  @sort = direction
end

Now, I try to run this:

SearchQuery.new.where(category: 'books').order('desc').limit(5).execute

Suddenly, Ruby screams at me: NoMethodError: undefined method 'limit' for "desc":String. What happened? I forgot to return self in the order method. Because I didn't specify a return value, Ruby returned the result of the last expression in the method—which was the direction string ("desc").

The chain broke because the order method handed a String back to the caller instead of the SearchQuery object. To fix it, I just need to ensure self is the final expression:

def order(direction)
  @sort = direction
  self
end

The Final Result: Code That Reads Like a Sentence

With all our methods returning self, we've created a fluent interface. The only method that shouldn't return self is the "terminal" method—the one that actually performs the action (in our case, execute). Once you hit the terminal method, the chain ends and you get your final result.

Here is the complete, polished pattern:

class SearchQuery
  def initialize
    @filters = {}
    @limit = nil
    @sort = nil
  end

  def where(params)
    @filters.merge!(params)
    self
  end

  def limit(number)
    @limit = number
    self
  end

  def order(direction)
    @sort = direction
    self
  end

  def execute
    "Searching for #{@filters} sorted by #{@sort} with limit #{@limit}..."
  end
end

# This reads beautifully
query = SearchQuery.new
  .where(status: 'active', color: 'blue')
  .order('price_asc')
  .limit(20)
  .execute

puts query



📋 Practical Task

Build a Fluent PizzaOrder Customizer

You are tasked with building a PizzaOrder class that allows a user to customize their pizza using a fluent interface. The goal is to make the ordering process read like a natural sentence.

Requirements:

  • Create a PizzaOrder class.
  • Implement a size(size_name) method that sets the size (e.g., "Large").
  • Implement an add_topping(topping_name) method that adds a topping to an internal array.
  • Implement a crust(crust_type) method that sets the crust (e.g., "Thin").
  • Implement a terminal method called bake that returns a string summarizing the order (e.g., "Baking a Large Thin pizza with Pepperoni and Mushrooms").
  • Ensure that size, add_topping, and crust can be chained in any order.

Test your code with this chain:

order = PizzaOrder.new
  .size("Medium")
  .add_topping("Pepperoni")
  .add_topping("Olives")
  .crust("Stuffed")
  .bake

puts order
Rating
0 0

There are no comments for now.

to be the first to leave a comment.