---
title: Separate your filters
teaser: |
  Separate the things which are not searching from the things which are
  indeed actually searching.
tags: web,rails,good code
author: Mike Burns
published_on: 2011-10-06
---

Filtering and searching gets out of hand. Split it off into its own
class.

The idea is that we'll use it like this:

```ruby
class ArticlesController < ApplicationController
  def index
    @articles = Article.filtered(params[:article_filters])
  end
end
```

First we'll need a mock filter. This is an object with a `#restrict` method (the
filtering itself), and an equality check (the test magic):

```ruby
class MockFilter
  def initialize
    @filters = []
  end

  def restrict(filters)
    @filters = filters
    self
  end

  def ==(other)
    other.filters == self.filters
  end

  protected

  attr_reader :filters
end
```

Once we have this mock, we can write a quick test. Here we're filtering an
article. The test ensures that we pass the right data:

```ruby
describe Article, 'filtered' do
  let(:filter) { MockFilter.new }
  let(:filtered_articles) { filter.restrict(filters) }
  let(:filters) { {'a' => 'b', 'c' => 'd'} }

  around do |example|
    @old_filter = Article.filter
    Article.filter = filter

    example.run

    Article.filter = @old_filter
  end

  subject { FactoryBot.create(:article) }

  it "produces filtered articles" do
    Article.filtered(filters).should == filtered_articles
  end
end
```

This test sets the class-wide filter (needed because Rails does class-level
programming instead of object-oriented programming), but cleans up afterward.
We pass the `ActiveRecord::Relation` given by `#scoped`
so any existing chains persist.

Given the above test we can write the article class quickly and simply:

```ruby
class Article < ActiveRecord::Base
  cattr_writer :filter

  def self.filtered(params)
    filter.restrict(params)
  end

  protected

  def self.filter
    if defined?(@@filter) && @@filter
      @@filter
    else
      @@filter = ArticleFilter.new(self.scoped)
    end
  end
end
```

Finally we can write the filter itself, encapsulating all filtering logic in
one place. All the tests (not shown) are boring, tedious, and needed, and also
all in one compact place.

```ruby
class ArticleFilter
  def initialize(relation)
    @relation = relation
  end

  def restrict(restrictions)
    published! if restrictions.try(:[], :published) == '1'
    this_week! if restrictions.try(:[], :recent) == '1'

    @relation
  end

  protected

  def published!
    where('published')
  end

  def this_week!
    where('created_at > ?', 1.week.ago)
  end

  def where(*a)
    @relation = @relation.where(*a)
  end
end
```

[Unobtrusive Ruby] helps you [avoid fat models].

[Unobtrusive Ruby]: https://thoughtbot.com/blog/post/10125070413/unobtrusive-ruby
[avoid fat models]: https://thoughtbot.com/blog/post/159807075/skinny-controllers-skinny-models
