18 October 2007
updated 15 February 2009
Demeter's Revenge
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... endend
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) enddef 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 :widgetsdef 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) enddef 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.
