Josh Hepworth

Keeping Classes a Secret

For a long time I fell in the trap of not fully distinguishing Ruby from Rails, so I had a lot of code that relied on ActiveRecord objects. This led to a lot of small classes in the root models folder that were easily visible to each and every other class in the project. This ended up letting classes leak all over the place, until most classes use other classes and simple updates were never simple anymore.

Recently, I've begun trying out a pattern that's new to me, but that I've seen in quite a few big Ruby projects. Defining classes right with the definition of another class. In the same file. This is usually limited to smaller "helper" classes, but can end up with nested classes up to 20 lines long.

Let's go over where I started from, then move to where I got to and what it's given me.

# app/models/post.rb
class Post < ActiveRecord::Base
  def excerpt
    Excerpt.new(excerpt_content)
  end
end

# app/models/excerpt.rb
class Excerpt
  attr_reader :content

  def initialize(content)
    @content = content
  end

  def formatted
    content.upcase
  end
end

That's a pretty simple start. We should pretend that the Excerpt is a little more advanced in this example and wouldn't actually be a good candidate for the presenter pattern.

As the project went forward, I would end up depending on and expending Excerpt in ways that just made the code harder to maintain and understand. More classes in the project would use it and add methods to it in their own way.

# app/models/page.rb
class Page < ActiveRecord::Base
  def summary
    Excerpt.new(excerpt_content).first(100)
  end
end

# app/models/excerpt.rb
class Excerpt
  # ...

  def first(length = 50)
    content[0, length]
  end
end

I've ended up using the Excerpt class for a side effect of something it provided rather than its actual purpose. It doesn't summarize things, but that's how I want to use it because it's there. My solution has been to make these kinds of "helper" classes less available to the rest of the application and keep behavior that really is actually specific to Post, entirely within Post.

# app/models/post.rb
class Post < ActiveRecord::Base
  def excerpt
    Excerpt.new(excerpt_content)
  end

  class Excerpt
    attr_reader :content

    def initialize(content)
      @content = content
    end

    def formatted
      content.upcase
    end
  end
end

This has generally pushed me to consider when I should and shouldn't be reusing code in a more fruitful way by providing a little bit of auto loading hiding from Rails and a more clear purpose for the class when it's first implemented. From here I can then consider if the class is worth pulling out into a concern when I implement the Page#summary or if a separate, more specialized object would be more appropriate.