Fluent Interfaces in Ruby ecosystem

You already used fluent interfaces multiple times as a Ruby developer. Although you might not have done it consciously. And maybe you haven’t built yourself a class with such API style yet. Let me present you a couple of examples from Ruby and its ecosystem and how I designed such API in my use-case.

What is a fluent interface anyway?

API that aims to provide more readable code

usually implemented with method chaining

often used for configuring / building objects

Examples

Probably most well-known example would be Active Record Query API used for building SQL statements.

User . preload ( :avatar ). where ( name: "John" ). order ( "id DESC" ). limit ( 10 )

Most of the time it will return ActiveRecord::Relation as a result. But there are a couple of methods like first! , to_a , to_sql which break the pattern, evaluate the statement and return a result.

But if you think about it, usually the fluent interface must give you a set of methods that will allow to either return the built object or pass an object to it for interacting.

Rspec Mocks is another well-known example of fluent API.

expect ( invitation ). to receive ( :accept ). with ( "John" ). at_most ( 3 ). times . and_return ( true )

On each own, the methods sound silly. What would with("John") mean? But in the context they are readable and make perfect sense.

I wonder if could say that certain built-in Ruby classes adhere to a fluent interface? For example String or Enumerable have plenty of methods that you can chain and they return the same class.

"Robert Pankowecki" . first ( 7 ). strip . gsub ( "b" , "B" ). reverse # => "treBoR"

But Martin Fowler said:

Certainly chaining is a common technique to use with fluent interfaces, but true fluency is much more than that.

I could not find a more elaborate statement of what he meant exactly. But my guess would be that fluency comes with a combination of Domain Specific Language.

The line between convenient method chaining and fluent interface might be a bit blurry.

My case

Some time ago we built an Insights Panel for a marketplace platform where Merchants could see some stats about their customers. The data is based on the marketplace Google Analytics data and fetched via API. But it limits the data only to the customers of certain merchant; without leaking global stats.

Google Analytics API can be queried in thousands of possible ways. If you have at least one domain with GA, I encourage you to give Query Explorer a try.

You can get a ton of useful knowledge from it. Which days of week people buy most, which hours, where are they from, what devices do they use etc. Google Analytics allows you to do a lot within its interface, but I find the query explorer sometimes to be much easier. Maybe because you can easily map its concepts into SELECT/WHERE/GROUP BY 😊

Going back to the fluent interfaces… Here is the code that I used for building the query. What we usually display in most cases is product sold over time. So that’s the default configuration we set up in the constructor.

class QueryBuilder def initialize ( campaign , merchant_id ) ids ( "ga:99990000" ) start_date ( campaign . created_at . to_date . to_s ( :db ) ) end_date ( campaign . ends_at . to_date . to_s ( :db ) ) dimensions ( "ga:date" ) metrics ( "ga:itemQuantity" ) filters ( "ga:productSku== #{ campaign . id } " ) sort ( "ga:date" ) quota_user ( merchant_id ) end def start_date ( start_date ) @start_date = start_date self end def end_date ( end_date ) @end_date = end_date self end def dimensions ( dimensions ) @dimensions = dimensions self end def metrics ( metrics ) @metrics = metrics self end def add_filter ( filter ) @filters << "; #{ filter } " self end def sort ( sort ) @sort = sort self end def dsc_qty sort ( "-ga:itemQuantity" ) end def max_results ( max_results ) @max_results = max_results self end def quota_user ( quota_user ) @quota_user = quota_user self end def to_hash { 'ids' => @ids , 'start-date' => @start_date , 'end-date' => @end_date , 'dimensions' => @dimensions , 'metrics' => @metrics , 'filters' => @filters , 'sort' => @sort , 'quotaUser' => @quota_user , }. tap do | h | h [ 'max-results' ] = @max_results if @max_results end end private def filters ( filters ) @filters = filters self end def ids ( ids ) @ids = ids self end end

Then we can use the fluent API to change the values easily.

# For displaying cities from which those customers buy builder . dimensions ( "ga:city,ga:countryIsoCode" ). dsc_qty # For their age builder . dimensions ( "ga:userAgeBracket" ). sort ( "ga:userAgeBracket" ) # For referrals builder . dimensions ( "ga:source,ga:medium" ). add_filter ( "ga:medium==referral" ). dsc_qty

The API could be even further refined into:

builder . add_dimension ( "source" ). add_dimension ( "medium" )

or

builder . add_filter ( "medium" ). equals ( "referral" )

But the current form was good enough for our needs and readable enough.

The other most common usage is to group customers and their purchase stats in total, without a timeline. In that case, we often want to display from most to least buying groups. That is a common use-case so we have a dedicated method for it.

def dsc_qty sort ( "-ga:itemQuantity" ) end

I think that’s how fluent interfaces evolve over time. They get better names, better chains, more out of the box, good defaults, and dedicated names.

After all you could write in Rspec receive(:method).exactly(1).times but it is much easier to understand receive(:method).once .

Read more