Ruby’s “mixins” provide a simple, middle-ground option to developers that wish to include reusable functionality in several classes. When using C++-style multiple inheritence, class hierarchies quickly become muddy, and it becomes necessary to look in several places to find the “core” behavior of a class. Mixins allow you to move reusable methods into modules. These methods can provide an extended interface based on the base functionality of your class (such as the Enumerable module), or they can be used to include “helper” methods (such as ActiveSupport’s Memoizable). Using mixins to include reusable behavior, such as helper methods or interface extensions, can greatly reduce repetition and bloat in your code.
Ruby stew
However, mixins can easily be abused. One common use of Ruby modules is to “break up” a large class that is becoming unmanageable. Let’s say you have a simple Article model:
class ArticleTest < ActiveSupport::TestCase
should_validate_presence_of :title, :abstract, :body, :author
end
class Article < ActiveRecord::Base
validates_presence_of :title, :abstract, :body, :author
end
So far so good. Now let’s say you decide you want to submit articles to an external service upon publication, such as the crossref service for DOIs:
class ArticleTest < ActiveSupport::TestCase
should_validate_presence_of :title, :abstract, :body, :author
should "submit to crossref after being saved" do
title = 'a title'
# other constants
# expectations for net/http
Factory(:article, :title => title, ...)
end
end
class Article < ActiveRecord::Base
after_create :submit_to_crossref
private
def submit_to_crossref
# convert article to xml
# format a crossref request
# connect to crossref using net/http
end
end
Very quickly, this submit_to_crossref
method will get unwieldy, and a good
programmer will refactor it into several small methods. Now you’ve added maybe a
hundred lines to your simple article model, and the tests are probably more
complicated. Once you start handling possible failure cases (such as unexpected
server responses or connection errors), there’s probably more crossref code than
article code.
At this point, the most obvious symptom of your problem is the fact that you need to look through all of this crossref code when dealing with an article, and you probably rerun all the crossref tests when changing your article model. One quick way to deal with this symptom is to quickly move all the methods from your model into a module and include it:
class Article < ActiveRecord::Base
after_create :submit_to_crossref
include Crossref
end
module Crossref
private
def submit_to_crossref
# call helpers to format and submit the request
end
# helper methods related to crossref
end
At that point, you can still test this module through article, but your article tests are still testing too much functionality, and you’d be in worse trouble if you decided to add Crossref support to another model. For now, though, let’s just assume that you only want to submit Article records to Crossref. You’ve still only treated a symptom of the core problem: dealing with crossref is really none of your article model’s business. It shouldn’t be tested with your article code, and it shouldn’t be part of the model’s interface. Moving the code into a module spreads the behavior out, so that you need to look in several places to find out what’s going on in Article. However, the concerns are still completely mixed, and testing the Crossref behavior without having an instance of Article is impossible, which leads to obscure and fragile tests.
Adding a class
There’s a basic appeal to the “one domain concept, one model, one class” approach you find in a lot of Rails applications, but that approach breaks down in all but the most basic applications. Your models will likely have to deal with concepts that don’t directly concern them (and aren’t part of the core domain concept), and you’ve seen that mixins don’t seem to help with these bloated classes. So, where else can you put this separate behavior? A new class:
class Article < ActiveRecord::Base
after_create :submit_to_crossref
private
def submit_to_crossref
CrossrefRequest.new(:title => title,
# other info for the request
:author => author).submit
end
end
class CrossrefRequest
def initialize(attributes)
# set attributes
end
def submit
# call helpers to format and submit the request
end
private
# helper methods related to formatting/submission
end
Now you have a discrete class that deals only with crossref requests. None of
its helpers end up in your Article model, and you can create focused tests just
for crossref. You no longer need to guess where the submit_to_crossref
method
comes from – we replaced the call to a mixed in method by instantiating another
class, and it’s much easier to locate a constant. If you’re a fan of mocking,
you can also easily test that Article uses CrossrefRequest appropriately, so
you’ll get immediate feedback if your callback chain gets out of whack:
class ArticleTest < ActiveSupport::TestCase
should_validate_presence_of :title, :abstract, :body, :author
should "submit to crossref after being saved" do
title = 'a title'
request = mock('crossref-request', :submit => true)
CrossrefRequest.
expects(:new).
with(:title => title, ...).
returns(request)
Factory(:article, :title => title, ...)
end
end
Whether you decide to mock out CrossrefRequest or take a state-based testing approach, I recommend also having acceptance tests in place to ensure the components collectively result in the feature you’re trying to provide. This class will also be much easier to reuse, and the temptation to couple the two models will be lower, as methods in your model are not directly available in CrossrefRequest – everything necessary from the request must be passed in to the constructor.
Ruby smoothies
It’s much easier to test and maintain small, discrete objects, so don’t be afraid to pull functionality out of your models and into classes specific to their concerns. Next time you end up with a model that hurts your editor, take a look at the method list – I bet a lot of that behavior is none of your model’s business!