Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
branch: master
Fetching contributors…

Cannot retrieve contributors at this time

99 lines (69 sloc) 8.063 kb
format title published_on
textile
Demeter's Revenge
Thu Oct 18 14:01:00 UTC 2007

For those who aren’t aware, the Law of Demeter – when applied to object-oriented programs – is a rule that determines which objects another object can send messages to based around the notion of an object knowing as little about the internal structure of the objects it interacts with.

It is commonly summarized as “Only talk to your immediate friends”. Please read the above Wikipedia article for more information and background.

This notion, when applied to Rails applications is equally valid however certain Rails practices seem to encourage demeter violations either through convention or API design and this can cause problems in all layers of your application. I am not the first to voice these concerns.

The issue of Demeter violations in views is a tricky one and not everybody agrees with Jay’s approach to solving violations in the view. Personally, I feel that when it comes to removing Demeter violations in the view, both approaches of adding simple wrapper/delegate methods or larger presenter-based solutions can be useful and choosing which approach to take depends largely on the complexity of the view in question.

Dealing with Demeter violations in Rails

However, my main concern with Rails when it comes to Demeter violations are those found in the controller and model. At Reevoo we try to write as much of our code using TDD or BDD as possible, making use of mocks and stubs to avoid unnecessary database calls (in both model and controller tests). It’s the issue of mocking and stubbing where Demeter violations can be particularly problematic as James and I found on a recent internal greenfield project.

Our new project was written using the Rails 2.0 pre-release and was written in a REST-ful fashion. When dealing with nested resources we would often have code that looked a little bit like this:

class WidgetsController < ApplicationController

  # POST /users/xxx/widgets
  def create
    # where @user was loaded in a before_filter
    @user.widgets.create(params[:widget])
    # and handle the result...
  end

end

We’re using the Rails convention of creating our associated widget object directly off of the User has_many association proxy. No apparent problem here but given that we wrote this test-first, look at the lengths we had to go through to make this work using appropriate mocking/stubbing:

class WidgetsControllerCreateActionTest < Test::Unit::TestCase
  def setup
    # usual rails controller test setup here
    @user = mock('user')
    User.stubs(:find).returns(@user)
  end

  def test_should_create_new_widget_for_parent_user_using_posted_widget_params
    widgets_proxy = mock('association proxy')
    @user.stubs(:widgets).returns(widgets_proxy)
    widgets_proxy.expects(:create).with(:name => 'my funky widget')
    post :create, :widget => {:name => 'my funky widget'}
  end

Because we are violating Demeter by getting a reference to the association proxy and then calling the create method on it all from within our controller, we’ve had to create a mock assocation proxy and stub the association proxy method on user to return it before we can set the expectation that we really care about (the :create call). It might not seem like a big deal, but we also have to make sure we stub @user.widgets to return something in every one of our tests for the create action otherwise we’ll find ourselves having problems with :create calls on a NilObject. Now multiply this issue by every single controller that contains a create action and things start to get very tedious.

The solution

The solution itself is not complicated and simply involves encapsulating the association proxy:

class User
  has_many :widgets

  def create_widget(*args)
    widgets.create(*args)
  end
end

Now our tests become much simpler and the intent clearer:

class WidgetsControllerCreateActionTest < Test::Unit::TestCase
  def setup
    # usual rails controller test setup here
    @user = mock('user')
    User.stubs(:find).returns(@user)
  end

  def test_should_create_new_widget_for_parent_user_using_posted_widget_params
    @user.expects(:create_widget).with(:name => 'my funky widget')
    post :create, :widget => {:name => 'my funky widget'}
  end

For our other tests, we only need to stub one method, the :create_widget method.

Again, whilst this doesn’t seem like a lot of effort, we now find ourselves having to write small delegate methods on all of our ActiveRecord models; and it’s not just create – we also find ourselves writing similar methods for all of our other association proxy methods (delete, update etc…). This too becomes very tedious, which is why my first thought was to try and automate the creation of these methods. This is where Demeter’s Revenge comes in.

“Demeter’s Revenge” is a simple extension to ActiveRecord, written as a Rails plugin that creates a collection of Demeter-friendly methods for your has_many and has_and_belongs_to_many associations. It doesn’t require any special configuration or installation – simply install the plugin as you would any other Rails plugin and your methods will become available to you. Here’s a quick overview of some of the methods you get access to and their standard Rails equivalent:

# given a User that has_many Widgets
user.build_widget(params) # => user.widgets.build(params)
user.create_widget(params) # => user.widgets.create(params)
user.number_of_widgets # => user.widgets.size (or .length)
user.has_widgets? # => user.widgets.any?
user.has_no_widgets? # => user.widgets.empty?
user.find_widgets(params) # => user.widgets.find(params)

For more examples, the plugin comes with a suit of RSpec examples. If you want to take a peek under the hood at the implementation, there is a full suite of specs. You can grab the plugin from my Subversion repository:

svn://lukeredpath.co.uk/var/svn/plugins/demeters_revenge/trunk

If you’re writing Rails apps and mocks and stubs have been causing you pain and/or Demeter violations make you cry, then hopefully this plugin will be of use to you. If you don’t care about Demeter violations and don’t use mock’s and stubs then its probably of less interest to you but I hope you give it a try anyway. If you have any feedback, feel free to drop me an e-mail (see the “correspondence” link in the site header bar) or leave a comment below.

Addendum

One of the things I’m not sure that I made very clear when I first wrote this entry was that this plugin is by no means a silver bullet to end all of your Demeter violation woes, nor are the problems experienced with mocking/stubbing the only reason to avoid violation Demeter violations which I could spend a whole article expounding on.

It just so happens that the pain felt when mocking/stubbing is symptomatic of Demeter violations in your code which should be enough to set alarm bells ringing. In the comments, Neil mentions that this is symptomatic of a problem with the mocking framework; whether or not you believe this to be true, any efforts to allow your mocks to work in such a way that would allow Demeter violating code to be easily mocked/stubbed, I fear that this would simply be a case of sweeping the problem under a rug and hoping nobody notices.

Jump to Line
Something went wrong with that request. Please try again.