has_few :god_objects
Tags: programming, bestpractices, rails, techniques
Published: 2022-12-20 20:40:19

---
title: has_few :god_objects
date: 2022-10-03 00:00:00 UTC
published: true
tags: programming,bestpractices,rails,techniques
canonical_url: http://calebhearth.com/association-pollution
---

In Rails applications, there are often two god objects: the primary business object and `User`. One way of keeping down the complexity and [fan-out](/fan-out-vs-fan-in) is to avoid adding unnecessary bidirectional relationships: those where both a `belongs_to` and a `has_many` or `has_one` relationship are defined. These has\_many and has\_one associations attract behavior, which helps objects that could otherwise have fewer responsibilities trend toward becoming god objects. You can keep your Rails app god object-free and a delight to work with by avoiding bidirectional relationships. I’ll show you how.

We can achieve better object design and smaller interfaces by defining associations in one direction only.

Before working on [the project where I experimented with Sandi Metz’ rules](/sandi-metz-rules-for-developers), I had a habit of adding relationships in both directions whenever a new model was introduced. I’ll use `User` as an example here because most applications have this object, but the concept of association pollution applies to all objects.

## The Code

Let’s look at a bidirectional ActiveRecord relationship. Pretend we’re making a long form blogging app named “long-ium”. Each page keeps track of its view counts already. We are tasked with ensuring they’re unique and want to start tracking the user that viewed the page. We add some code like this to our `PageView` model:

```
class Pageview
  belongs_to :page
  belongs_to :user # <== Just added
end

```

Now while you’re adding that you might be tempted to let `User` know about its newfound child:

```
class User
  has_many :pageviews
end

```

This new `has_many` association allows us to track whether a specific user has read a specific long-ium article with: `current_.pageviews.where(page: page)`. From my experience this helper isn’t much better than writing `PageViews.where(page: page, user: current_user)` and it will be rarely used.

A user can exist without a pageview, but `Pageview` is meaningless without both its user and its page. This is hinted by the name of the association macro: `belongs_to` implies correctly that `Pageview` is dependent on User and Page.

## What’s the big problem?

If User not _needing_ to know about a Pageview association were the extent of the problem, this post wouldn’t be worth writing. The problem stems from my experience that associations attract behavior.

Having `has_many :pageviews` means that there may be instance methods on `User` that deal with pageviews, steps in factory or seed generation to create pageviews along with users, and other behaviors that contribute to `User` _becoming_ a god object.

## A solution: leave out `has_many`!

`PageView` can easily wrap such a concept into a scope such that the interface becomes `Pageview.for_user(user)`.

```
class PageView
  belongs_to :user

  def self.for_user(user)
    where(user: user)
  end
end

```

Should the query become more complex than a single line as business needs change, a query object or [database view](/talks/search) can be used as well. Consider this potential example, where complexity is neatly hidden from both `User` and `Pageview`, and we have not changed the public interface.

```
class PageView
  belongs_to :user

  def self.for_user(user)
    PageviewsForNewActiveUsers.for(user)
  end
end

class PageViewsForNewActiveUsers
  def for(user)
    # long-winded, complex logic that is now isolated
  end
end

```

I have found that while it won’t always be the case, many associations on `User` _where user does not have the foreign key_ are not necessary.

## Performance Concerns

There is one particularly good reason to implement inverse associations (`has_many`, `has_one`): performance. Nate Berkopec is one of the most knowledgeable people about Rails performance. He [cautions](https://www.speedshop.co/2019/01/10/three-activerecord-mistakes.html#where-means-filtering-is-done-by-the-database) against using `where` or scopes (either with the `scope` DSL or class methods as above with `for_user`) when rendering collections. Specifically, when you’re iterating over a collection, it’s not possible to preload anything but a proper association with `includes` so you’ll want to avoid code that calls these methods inside of nested loops.

```
<% Post.where(user: user) do |post| %>
  <%= post.comments.active %>
  <!-- or -->
  <% post.comments.where(deleted_at: nil) do |comment| %>
    <%= comment.body %>
  <% end %>
<% end %>

```

Rather, define an association on Post such as `has_many :active_comments, -> { where(deleted_at: nil }` which can both be passed to `includes` and referenced directly in your nested loop.

```
<% Post.where(user: user).includes(:active_comments) do |post| %>
  <% post.active_comments.each do |comment| %>
    <%= comment.body %>
  <% end %>
<% end %>

```

Note that it’s fine to continue to use `Post.where` here and avoid `User.has_many :posts`; it’s only necessary to define the inverse `has_many` association for the nested loop. Nate’s [article](https://www.speedshop.co/2019/01/10/three-activerecord-mistakes.html#where-means-filtering-is-done-by-the-database) goes into more detail about this.

## Wrapping up

Many associations that are defined in two directions are extraneous. Defining the association only on the model that stores the foreign key can help to reduce “association pollution” in god objects that tend to attract behavior.

By keeping associations off of these models, I am also able to avoid temptation to add further behavior into their classes. This leaves me with a simpler, easier to understand model.