Tracking Streaks on Your ActiveRecord Models
Progress can be hard to measure with anything you do. That’s why having a process for becoming better at something is so helpful. You don’t always see results right away, but if you practice enough, things will start coming together. Being able to keep yourself accountable for sticking to a process is much easier when you can easily measure it.
When I started building Verba, I tried to think of what would help to motivate me to write every day. I took a cue from Github that tracking a streak of days would work. I wrote an earlier post on how Github’s streak feature helps me commit code (almost) every day.
My goal for Verba was to be able to track the number of days in a row that a given user had written a post. This metric would be displayed to each user.
When I thought about how I could implement a streak feature, I determined that there were two different ways I could build it:
- The
User
class has astreak
integer column that gets checked and possibly changed every time a post is created.
- The
User
class has an instance method#streak
that calculates the streak of posts every time it is called.
There are several tradeoffs here that gave me a bit of trouble. With option one, the Post
class will have to know about the concept of a streak, check whether there was a post before it, and so on. Plus, changing other objects in a callback probably isn’t the best decision, as it introduces tight coupling. It doesn’t cost much in performance, since it only increments a column, but the complexity of the system on my end is high.
With option two, performance will most likely take a hit. Every time #streak
is called on a user, all of it’s associated posts need to be retrieved from the database and we have to calculate a streak based on that returned collection on posts. That being said, this makes it much simpler for me. All I have to worry about is the calculation.
I ended choosing the second option for a couple reasons. It only lives in the User
class, which I think is better because the concept of a streak means nothing to a single post. Only users care about their streak. Also, this way makes it easier makes extending the behavior much easier. I’ll explain why later.
Let’s get into the implementation:
Here, I wrote a simple spec to make sure I’m getting the implementations right, and it will make it easier to refactor later.
First, I’m going to write some pseudo-code in a comment to try to think through the calculation I need to perform.
By having this clear path to getting to the answer I want, writing the code is much easier. Here’s what I came up with:
This method is works, but it’s getting long and hard to understand. We can extract some methods to clean it up:
Building this functionality took a bunch of trial, error. After thinking through how to correctly tackle the problem (and TDDing it), I finally got it to a point to where I am happy with how it works. I thought this might be useful in other projects, so I packaged it up as a gem.