BLOG

Objective C-like Null Object Pattern in Ruby

At Custora we allow all the numbers on the screen to be lazy-loaded. We precache all of the main page views, but there are too many possible ways that a user can slice the data to pre-compute all of the values.

Writing code to deal with lazy loading can be a bit of a challenge. Basically, it means that whenever we get a number from our internal statistics api, we have to accept the possibility that it is not rendered correctly. And we use the statistic term loosely, it can be a float, an array, or some other ruby class. Anything that is computed from the raw transaction data goes through this API.

So to render the pages we make two passes. First to run through and figure out what statistics need to be calculated. We then send these all to delayed job, display a pending page that perodically polls to see if the jobs are done, and when the jobs are completed we display the rendered page.

We have an interesting technical challenge, how do we handle these uncomputed statistics. When we first started we ran into all kinds of errors. We would have an expression like:

number_with_precision(client.new_customer_value)

 When client.new_customer_value was hit, the job would be enqueued, but then the page would fail to load.

To solve this we tried checking if a statistic was nil, but this made convoluted code

(client.new_customer_value ? number_with_precision(client.new_customer_value) : nil)

So to solve this we made a do nothing class with the goal to fail silently as much as possible. So we have a class that is designed to fail gracefully no matter what you do with it. It should fail silently if you call

stat * 100

or

stat[1]

or

1 * stat

or even

stat[1][0].first.to_s

 We could have done something like:

class NilClass
def method_missing(*)
return nil
end
end

But this would have changed how nil behaves all over our application. Instead we decided to make a new class for unevaluated model statistics.

Another solution would have been to use the .try method on all statistics. But this is cumbersome, and doesn't work when the unevaluated statistic is passed to another function.

So we came up with the unevaluated model statistic class.

Code:

class UnevaluatedModelStatistic
def ==(_other)
false
end

def method_missing(_method,*_args)
self
end

def to_f
0.0
end

def to_str
""
end

#for cohorts statistics
def number_of_display_columns(_arg)
1
end

def *(_arg)
0
end

def +(_arg)
0
end

def -(_arg)
0
end

def coerce(_other)
[self,_other]
end

def /(_arg)
1
end
end

On the second pass we end up with the completely rendered page.

This is the way we are attacking the problem. How would you approach it?

Tagged : Story

Like this? You might also enjoy these.

Story

Be careful how you average - a retail example

Companies often want to track things like the size of first orders and the size...
Read
Story

Setting Up Control Groups - Designing a Marketing ...

Welcome to Part Three of our series on designing and implementing a successful...
Read
Story

Setting a baseline – Designing Marketing ...

In Part Two of our series on designing and implementing a successful marketing...
Read

Meet the Marketer: Renee Halvorsen from Marine Layer

In this ongoing series, we chatted with some of our customers to learn more...
Read

4 Innovative Brands that are Transforming the Customer Experience

We’ve entered a customer experience renaissance in retail, and these four...
Read

How Being Customer-Obsessed Can Increase a CMO’s Tenure

With customer data at their fingertips, the most effective (and longest...
Read