Code Smell: this.object.knows.too.much
One day in your coding journey you might run into a linting error like this:
RSpec/MessageChain: Avoid stubbing using receive_message_chain
Uh, oh. What happened here?
This is the Ruby community's best effort at enforcing The Law of Demeter. While this linting rule doesn't protect us from The Law of Demeter, it does let us know something fishy is happening.
Like everything in software The Law of Demeter has very little to do with the Greek goddess, Demeter.
The Law of Demeter "requires that a method m
of an object a
may only invoke the methods of the following kinds of objects:
a
itself;m
's parameters;- any objects instantiated within
m
; a
's attributes;- global variables accessible by
a
in the scope ofm
."
Thank you, wikipedia.
Another way of thinking about this is, "how much does an object need to know about unrelated objects to get its work done?" If the answer is, "a lot", that's bad.
The Law of Demeter, or message chaining, is an example of coupling in your code that makes dependencies hard to reason about.
Example Time
Here's what this might look like in a Rails project:
class User
def comment(text:)
Comment.create(text: text, user_id: id)
end
end
This might look a bit contrived, but I have seen things like this in the wild. In this example User
knows how to create a Comment
, if new attributes are added to Comment
, User
needs to know these too.
Another funky looking example is message chaining, especially message chaining that creates new objects. I've seen code like this in rails projects before too:
class User
def comment_on_post(post_id:)
posts.find_by(id: post_id).comments.create(text: text, user: self)
end
end
Here we have a long message chain that allows us to call create
on a comment
through a user
. While this might look incorrect, it is technically possible, but not advised, in a Rails project.
What to do instead?
Make sure objects only know about their own functionality.
For both of the examples above, let the Comment
object handle creating its records. If you find yourself needing logic before and after creating a Comment
, you can use validations, or service objects that handle the business logic you need.
Why isn't there a linter rule for this?
The best we have today is the RSpec linter rule. Unfortunately, it is really hard to detect Law of Demeter violations in Ruby. For example some_string.downcase.chomp.gsub('-', '_')
might look like a violation but it's a perfectly reasonable chain of calls since each method returns an instance of String
.
The Law of Demeter isn't so much a style preference like double quotes vs single quotes, as it is a code smell, or an indication that there might be something wrong with the code.
Code smells are a lot harder to lint for, as developers we need to learn to spot them so that we can decouple our code to keep things clean and readable.