18 October 2007
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...
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.
Return to home page | Check out my tumblelog
27 Comments on this article
Return to home page | Check out my tumblelog
Commenting on this article is now closed
1. Comment by Neil Wilson on 18 Oct 2007 at 15:10
Strikes me that the problem is with the mocking framework and not the application.
What’s would be wrong with
@user.widgets.expects(:create).with(:name => ‘funky gibbon’)
?
2. Comment by Luke Redpath on 18 Oct 2007 at 15:10
That would be fine if we were working with a real User object but because we are working with a mock, calling @user.widgets would result in an unexpected message being called the mock which is how I’d expect a mock to react.
Plus, let’s not forget the underlying problem which is the violation of demeter; by exposing the internal structure of our User object we risk making our application code brittle.
In other words, the lengths we have to go to when mocking is more a symptom rather than the problem; perhaps I didn’t make that clear enough.
3. Comment by Luke Redpath on 18 Oct 2007 at 15:10
I’ve updated my article with a few extra thoughts, hopefully this will make my reasoning a bit clearer.
4. Comment by Tim Case on 18 Oct 2007 at 16:10
Luke, law of demeter violations in Rails bothered me before the emergence of mockist specing. I think what mocking does is throw the problem in your face and nag at you to do something about it. I too have done exactly what you did albeit in a more hackish fashion in that I just peppered my entire object model with delegator methods just for attributes. (note: I don’t use :delegate => because of a kink in how it handles a method call on a nil delegated object, I was surprised instead of returning nil it raises an exception). I’m game to check out Demeter’s revenge, thanks for the work!
5. Comment by Luke Redpath on 18 Oct 2007 at 16:10
Tim, I agree that if you are heavily into using mocks and stubs that the demeter problem is more likely to rear its ugly head. Jay Fields makes a similar point on his website. I quote:
“If you are a mockist you will often prefer to follow the Law of Demeter because it makes mocking much easier. However, classicists don’t seem to mind as much since they can use train-wreck syntax for state based tests without any pain. Being a mockist or classicist seems to greatly influence a person’s opinion on whether or not to follow the Law of Demeter.”
Of course, the kind of demeter violations inherent in the Rails associations API are the easiest things to fix which is why I felt a plugin of this nature would be appropriate. Solving demeter violations in a more general sense often requires you to look more carefully at your domain model and see if there might be a more cohesive design struggling to get out.
6. Comment by meekish on 18 Oct 2007 at 16:10
Definitely a step in the right direction.
It makes we wonder: Is it possible to change the visibility of instance methods? Perhaps Demeter’s Revenge could make the association methods private as it sets up the proxies?
7. Comment by Luke Redpath on 18 Oct 2007 at 17:10
meekish: that’s possibly a bit to aggressive an approach for the plugin but if you want to do this yourself, by all means do so. You can do @instance.send(:private, :method_name) if that helps.
In fact, feel free to submit a patch but make it configurable (e.g. ActiveRecord::Base.demeter_strictness = true) if you like.
8. Comment by Cyx on 19 Oct 2007 at 01:10
This is completely off topic, but how do you manage to make rails 2.0 pre release run with rspec? I tried running rails 2.0 pre with rspec trunk and it’s very problematic for me.
9. Comment by Kevin Olbrich on 19 Oct 2007 at 01:10
Very interesting. The icing on the cake would be to automatically create methods for any association extensions.
@user.widgets_with_tag() could be created if you extend has_many :widgets with a method called ‘with_tag’
10. Comment by Yossef on 19 Oct 2007 at 13:10
Tim: On a previous project, problems with delegate annoyed me enough to rewrite it to handle nil targets.
11. Comment by Luke Redpath on 19 Oct 2007 at 15:10
Cyx – we weren’t actually using rspec for our internal Rails 2.0 project, we just used test::unit. My plugin uses rspec and the specs can be run independently of rails.
12. Comment by Eric on 20 Oct 2007 at 02:10
I am not sure exactly how @user.widgets.create(params[:widget]) is a violation of the law of demeter?
The method #widgets (M) of the user object (O) in the case of calling @user.widgets.create will return an association proxy object, such as an HasManyAssociation. A HasManyAssociation object as a @reflection object that holds the reference to the has_many class, in this case it would be Widget. So your actually calling Widget.create, within the context of an AssociationProxy. According to the Law of Demeter:
More formally, the Law of Demeter for functions requires that a method M of an object O may only invoke the methods of the following kinds of objects: O itself M’s parameters any objects created/instantiated within M O’s direct component objects
O in this case is User, which has a direct component of a HasManyAssociation object which holds the reflection to get at class Widget.
I think the confusion for a lot of people about the LoD comes from the fact that if they see method chains:
@user.widgets.create they automatically think it breaks the law of demeter.
Take an example of ruby-sequel’s query building code:
DB[:users].filter(“name = ‘Tom’).or(:created_at < TIme.now).filter(“hits > 0”)
Even though there is a method chain does it mean we have broken the LoD, because each method returns the Query object.
I still use mocks and stubs and partial mocks in my Rspec code, and I have seen the same inefficiencies as you have when it comes to method chains. It is pretty annoying and creating “wrapper” methods (Demeter Transmogrifiers) helps alleviate some of the pain. And yes, a lot of time creating extra mocks and stubs just for “go between” methods will result in finding LoD violations, i just dont think it’s the case in your example.
13. Comment by Dennis Demeter on 20 Oct 2007 at 13:10
For obvious reasons I am curious about where the term “Demeter’s Revenge” came from…. Anyone?
14. Comment by Jonathan Conway on 20 Oct 2007 at 18:10
@Cyx – if you want RSpec to play nice with Rails 2.0 then you’ll need to grab both RSpec and RSpec on Rails from trunk.
15. Comment by Ikai Lan on 20 Oct 2007 at 20:10
Oh, this is spiffy. I gotta try this out.
16. Comment by Luke Redpath on 22 Oct 2007 at 17:10
Eric, I’m afraid I think you’ve got this one all wrong.
You’re missing out a vital component here: the client of our @user object (the controller, usually). Lets take it bit by bit:
“More formally, the Law of Demeter for functions requires that a method M of an object O…”
Where O is an instance of our UsersController and M is an action method on our controller (in this example, #create).
”...may only invoke the methods of the following kinds of objects:
1. O itself”
Any other methods in the controller (could be another action but more commonly some private method).
“2. M’s parameters”
Not that relevant because we don’t normally pass parameters to our controller actions, but it is possible.
“3. any objects created/instantiated within M”
This would be our @user object that has been instantiated with our User.find call.
“4. O’s direct component objects”
There are many but this would typically be our request/response/session objects etc.
So on the above basis, by calling @user.widgets.create we’ve violated Demeter. We are calling the #create method on the association proxy returned by the #widgets call, but the controller shouldn’t need to know about association proxies – this is implementation detail internal to our @user object but now our controller knows about it too – uh oh!
Now let’s look at how using our #create_widget method avoids breaking encapsulation and violating demeter. Firstly, we’ve already established that it’s ok for our controller (object O) to call a method (#create_widget) on one of the objects it instantiated itself (our @user object). So all is good there. Now the #create_widget method itself:
def create_widget(args) widgets.create(args) end
In the context of this piece of code, object O would be a User object and the association proxy returned by widgets would be a component object of User so it’s ok to call #create on the proxy from inside the User class. But not outside – this would be violating demeter.
I hope that clears things up for you.
17. Comment by Luke Redpath on 22 Oct 2007 at 17:10
Sorry about the formatting on the above post – it’s not the best.
Dennis – it’s “Demeters Revenge” because its about the law of demeter and I thought it sounded cool. Nothing more than that. ;)
18. Comment by Luke Redpath on 22 Oct 2007 at 17:10
Eric – you were right about one thing however; method chaining isn’t always a sign of demeter violations, especially in such a dynamic language like Ruby. You’re example of a query builder is one such example because you are actually calling methods on the same object. It could easily be written:
object.method_one object.method_two object.method_three
But because each method returns the original object, we can chain the methods together; however, this is more for convenience/readability than anything.
19. Comment by Don on 24 Oct 2007 at 01:10
I love straightforward plugins that give me new functionality for free!
I learned something new just from your spec helper. It is amazing how much is being setup in so few lines.
20. Comment by szeryf on 24 Oct 2007 at 15:10
I tend to agree with most of the article, but I don’t see how the Demeter’s Revenge plugin is going to make your code any less brittle? IMHO using user.build_widget instead of user.widgets.build is just as problematic.
21. Comment by Luke Redpath on 24 Oct 2007 at 16:10
szeryf – how is it just as problematic? It’s less brittle because the controller is no longer coupled to the internal implementation of User.
22. Comment by Chris on 25 Oct 2007 at 12:10
If the Demeter plugin is coupled to the internal implementation of User (i.e. change the name of an assoication then the demeter method name changes) and the controller is couples to the demeter method names, then isn’t this just as bad??
so, if you change the name of the association, then auto generated demeter method name changes, then your controller is broke.
What am I missing?
23. Comment by Luke Redpath on 25 Oct 2007 at 13:10
That is a valid drawback of dynamically generated methods; that said there is nothing stopping you from persisting your User class public interface by writing the methods you need yourself directly in your class.
As I said, the plugin is not a silver bullet and is designed to solve every problem. But if you still want to create widgets for a User, but the way in which you implement creating widgets changes, then your controllers are still going to want to do @user.create_widget – using that interface over @user.widgets.create meansit is possible to your own create_widget method without having to change client code.
If you are changing your association names themselves this to me suggests an important semantic change in your app that probably should be reflected in your controllers anyway.
24. Comment by Chris on 25 Oct 2007 at 14:10
I’d say violating the demeter principle and writing :
@user.widgets.create(..)..
is actually better than installing a seperate plugin (that may have bugs) that “some guy” on the net created that doesn’t even really solve the problem at all (but will make your tests nicer looking). When the next programmer looks at your code will it be self explanatory whats hapenning?
Do you not think that somehow the Conventions in rails allows you to slightly break the demeter principle? There’s no point in sticking to rules just for rules sake is there? After all, @user.widgets.create will allways be the way that rails works.
25. Comment by Luke Redpath on 25 Oct 2007 at 14:10
That’s pretty shoddy reasoning if you ask me, but hey, nobody is forcing you to use the plugin and I’m not going to waste my time repeatedly explaining its benefits (and the cons). Sure, Rails conventions might not change (but you don’t really know that for sure do you?) but this is less about Rails conventions and more about building a coherent, encapsulated domain model which is far more important to me.
Rails was written by “some guy” too you know. And all code “might” have bugs. What kind of argument is that? Bugs get found, bugs get fixed.
I create things like this for myself first and foremost and choose to release it because I feel it may be of some benefit to others. I’m not interested in writing code that is everything to everyone.
I’m confident in my own code (or I wouldn’t use it would I?) and the source and specs are there for all to see but as with everything of this nature, YMMV. Christ, if we avoided things because they might be buggy, we’d never get anything done.
26. Comment by Kevin Teague on 26 Oct 2007 at 07:10
Interesting stuff to think about. I’d never done any significant reading on the Law of Demeter until tonight, but there is mention of the Demeter Tools which says, “The Demeter literature talks about the introduction of lots of additional small methods, which started getting unwieldy to add manually, and is part of why the Demeter tools exists, so they can be autogenerated as needed.”
The Demeter tools have been written for C++ and Java, but obviously tools such as Demeter’s Revenge is a lot easier to implement in Ruby. Demeter’s Revenge is also a much cooler name :)
@Chris Do you not think that somehow the Law of Demeter rule allows you to slightly break the Rails conventions? There’s no point in sticking to conventions just for conventions sake is there?
27. Comment by Bryan Ray on 26 Oct 2007 at 19:10
Excellent plugin.
I’ve recently struggled with this in my rspec tests as well and this will be an excellent way to clean them up.
Thanks!