Lame Code Considered Harmful

December 28th, 2006

In this chapter, our intrepid hero writes slightly-more-tolerable code.

This is a follow-up to my earlier article Making a mockery of ActiveRecord. That seemed to prompt a fair amount of useful discussion. One of the most interesting can be found here, on James Mead's blog. He is more polite about it than this, but to summarize: "Your tests would be easier to write if your code didn't suck."

One way to think about testing (particularly in a message-oriented language like Ruby) is as a weak mathematical proof. Don't take that analogy too far, since we aren't really formally proving anything. Consult a medical professional before using my blog, or any other drug.

Suppose we have a method called 'a', and it invokes the methods 'b', 'c', and 'd' to get its job done. If we have already written tests for 'b', 'c', and 'd' showing that they behave correctly when given a certain set of inputs, all that is left is to show that 'a' gives them that input. This is the concept that lets us use mocks, and still be comfortable that our code is probably correct. We don't need to re-test method 'b' every time it is called. We already know that it works.

What does this have to do with my previous article? James indirectly pointed out to me that I hadn't written a Composed Method, and I was making life hard for myself.
Here's what the code looked like when I wrote the article:

 1 def generate_email_messages
 2   people = self.recipients
 3   Message.transaction do
 4     people.each do |person|
 5       unless person.campaigns.include?(self.campaign)
 6         person.subscriptions.create :campaign => self
 7       end
 8       em = self.email_messages.create :person => person, :message => self, 
 9         :direction => 'mt', :sent_at => Time.now.utc, :body => self.body
10       unless em.valid?
11         raise RuntimeError.new("Unable to ..blah.. for #{person.email_address}.")
12       end
13     end
14   end
15   people.size
16 end

This isn't too bad, but it definitely has some flaws. On line 5, I am reaching out 'through' Person, and interrogating it to decide whether I need to create a Subscription. Clearly, the Person model should be deciding whether or not it needs a subscription.

The same thing happens again on line 10. I reach through the email_messages association, and then raise an error if the creation failed. The EmailMessage model should contain the knowledge of what constitutes an error. This is particularly true since this example is greatly simplified, and the real model uses acts_as_state_machine with conditional validations. As your models grow in complexity, the importance of having the functionality in the right place rises.

Here's what the code looks like now.

 1 def generate_email_messages
 2   people = self.recipients
 3   Message.transaction do
 4     people.each do |person|
 5       person.subscribe_to(self.campaign)
 6       EmailMessage.generate_message_for(self, person)
 7     end
 8   end
 9   people.size
10 end

There are new methods on Person and EmailMessage that encapsulate the necessary steps. At first glance, those methods look trivial. Neither is longer than three lines. However, they can now be tested in isolation, and work the same way with both existing and unsaved objects. ActiveRecord's association proxies (email_messages, campaigns, etc) are powerful tools, but they can also distract you from the actual goal, which is always to write readable programs with as few defects as possible.

7 Responses Follows

  1. court3nay says

    Ooh! Finally someone else who sees it like I do—kill all association proxies!

  2. Steve says

    I like it!

    On an unrelated note, your code samples in my newsreader (FeedDemon) show up as escaped HTML text which makes it virtually unreadable. Just thought you might want to know.

    Cheers, Steve

  3. Wilson Bilkovich says

    Gah, that’s lame. Just when I thought I could trust some blog software. =( I’m not sure how to fix it, since I have to use html to make the highlighting work.

  4. David Chelimsky says

    You’re not only simplifying things here. You’re honoring “Tell, Don’t Ask” and the Law of Demeter. This is a great example of both, and how easy Rails makes it to violate them.

  5. Wilson Bilkovich says

    OK. I think I have the feed HTML problem fixed. Let me know if it’s still odd for you, after refreshing.

  6. Olle Jonsson says

    For linkage’s sake: Law of Demeter resources.

    “Only talk to your friends” is the motto (of that style rule).

  7. David Chelimsky says

    Actually, I think that should be “your code would be easier to write if you tests didn’t suck”.

Sorry, comments are closed for this article.