Many Skinny Methods

Posted by Koz Thursday, October 04, 2007 08:12:00 GMT

This refactoring is based on a topic Marcel and I covered at RailsConf Europe.

Before

1
2
3
4
5
6
7
8
9
10
11
12
class Expense < ActiveRecord::Base
  belongs_to :payee
  protected

    # Nice and concise, but what happens as we add more rules
    # and how do we write test cases for the four different possible 
    # validation states?
    def validate
      errors.add("Not enough funds") if payee.balance - amount > 0
      errors.add("Charge is too great") if payee.account.maximum_allowable_charge > amount
    end
end

After

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Expense < ActiveRecord::Base
  belongs_to  :payee

  # Instead of one large validation method, break each individual
  # rule into methods, and declare them here.
  validate      :ensure_balance_is_sufficient_to_cover_amount
  validate      :amount_does_not_exceed_maximum_allowable_charge

  protected

    # These validation callbacks simply add error messages if a particular 
    # condition is met.   Each of them can be tested and understood on their own
    # without having to understand the entire body of the validate method.
    def ensure_balance_is_sufficient_to_cover_amount
      errors.add("Not enough funds") if insufficient_funds?
    end

    def amount_does_not_exceed_maximum_allowable_charge
      errors.add("Charge is too great") if exceeds_maximum_allowable_charge?
    end

    # By defining separate predicate methods we can test each of them individually
    # and new programmers can see the intent of the code, not just the implementation.


    # Instead of subtracting the amount from the balance and checking if the
    # value is greater than 0,  change the implementation to mirror the intent.
    # There was a bug in the before code,  this is more obvious.
    def insufficient_funds?
      amount > payee.balance
    end

    def exceeds_maximum_allowable_charge?
      payee.account.maximum_allowable_charge > amount
    end
end

While the refactored version may have more lines of code, but don’t let that scare you. It’s far more important for code to be human readable than incredibly concise.

Comments

Leave a response

  1. MislavOctober 04, 2007 @ 09:28 AM

    This kind of refactoring (making code bigger, but more expressive) is a skill I have yet to master.

    But maybe the two last helper methods should be declared public? They certainly can be re-used.

  2. Phil ThompsonOctober 04, 2007 @ 09:46 AM

    Nice refactoring. It’s very easy, with Ruby, to get too concise as you say. Let’s not forget that one of Ruby’s major advantages is the ability to express things in sentences.

    I like the ‘validate :method_to_validate_with’. I didn’t realise you could do this. It keeps things consistent with the other validations that are declared this way.

    @Mislav – Personally I don’t make things public unless I need to (unless you’re writting an API). It makes people think ‘is there a higher level call I can make instead’ before they go ahead and make it public and it helps keep the interface clean. Remember YAGNI.

  3. Matthijs LangenbergOctober 04, 2007 @ 10:09 AM

    And how exactly would this affect the way the four different possible validation states? I must admit that the code is more readable, but since it doesn’t change any of the business logic, the tests keep the same, right?

  4. DanielOctober 04, 2007 @ 10:33 AM

    Really nice and beautiful example. I totally agree on your statement that code should be as human readable as possible.

  5. Rowan HickOctober 04, 2007 @ 01:36 PM

    For those looking at refactoring, I suggest picking up Martin Fowler’s book on it, it’s got a tonne of detail about how and where to refactor, along with worked examples for many different refactoring techniques.

  6. HadleyOctober 04, 2007 @ 01:52 PM

    I really don’t find this refactoring very appealing – it seems to have introduced a huge amount of duplication – e.g. the intent “make sure there’s enough money” is repeated in four places: the validation method name, the validation statement, the text string and the boolean check function. Is this really necessary?

    Perhaps if there was a tangled mess of validation logic to begin with I could see the benefit, but otherwise it seems like it’s heading towards http://developers.slashdot.org/comments.pl?sid=33602&cid=3636102 ;)

  7. EvgenyOctober 04, 2007 @ 02:39 PM

    1. more code makes reading code more difficult, not more easy 2. more code makes runtime slower.

    It’s especially true when you introduce a huge heap of meta-programming magic by using “validate” which probably uses even more magic like alias_method_chain. Reading, debugging, and actually running this new version has all the minuses, none of the pluses.

    This is not a good enough reason, especially when people easily disagree with it: “It’s far more important for code to be human readable than incredibly concise.”

    just my 2cc

  8. Roman Le NégrateOctober 04, 2007 @ 03:04 PM

    I agree with Hadley. While there are only two validations in the `validate’ method, it’s still possible to test each case or a combination of them in a clean and simple way: instantiate a record, make it erroneous, and check the error messages that must have stacked up. Only decompose the `validate’ method when this process becomes unmaintainable.

    So, to me, this refactoring was useless except for the `amount – payee.balance > 0’ which becomes `amount > payee.balance’. With that change, I find the `validate’ method more human readable than the refactored version (very concise and each line can be read as a sentence).

  9. Justin PeaseOctober 04, 2007 @ 03:15 PM

    This would seem like a happier medium to me:

    class Expense < ActiveRecord::Base belongs_to :payee

    validate      :ensure_balance_is_sufficient_to_cover_amount
    validate      :amount_does_not_exceed_maximum_allowable_charge
    protected
    def ensure_balance_is_sufficient_to_cover_amount
      errors.add("Not enough funds") if amount > payee.balance
    end
    def amount_does_not_exceed_maximum_allowable_charge
      errors.add("Charge is too great") if payee.account.maximum_allowable_charge > amount
    end

    Personally, I find that pretty humanly readable.

    end

  10. Josh PeekOctober 04, 2007 @ 03:19 PM

    Rails is an opinionated framework and I think we should go ahead a deprecate the usage of overriding the validate method (used in the Before).

  11. LukeOctober 04, 2007 @ 03:45 PM
    Nice post. Speaking of changing the implementation to mirror the intent…shouldn’t: def exceeds_maximum_allowable_charge? payee.account.maximum_allowable_charge > amount end be def exceeds_maximum_allowable_charge? amount > payee.account.maximum_allowable_charge end ?
  12. Peter CooperOctober 04, 2007 @ 04:06 PM

    I like this resulting code, but just another thought.. validations can be a bit like “tests” (or specs, if you’re that way inclined), right? I wonder if there’s a way to change the syntax a bit to make validation look more like a “live” test suite (or set of specs) so that the familiarity of syntax is there in both cases..

  13. Carl PorthOctober 04, 2007 @ 06:04 PM

    If I were reading someone else’s code (and in this case I am), I’d much prefer the after. It has all the validations consolidated for easy reading, telling me “what” the validations are doing without me having to figure out the “how”.

    Just like reading an rdoc, most of the time I don’t care “how” each method is written, just “what” it does.

  14. Mark WildenOctober 04, 2007 @ 06:13 PM

    I agree with the comment about duplication. And I would also say that I find

    a < b

    to be more readable than

    is_a_less_than_b

    The former has the benefit of never getting out of sync with itself, unlike a method’s code and its name.

    ///ark

  15. macournoyerOctober 04, 2007 @ 06:47 PM

    I agree, it’s easier to read. Maintainability is up. But what when you end up with 500 LOC in your models ?

    And you should try using http://refactormycode.com/ for pasting your code or just for getting feedbacks on refactorings.

  16. sandofskyOctober 04, 2007 @ 07:53 PM

    I like this refactoring.

    Extracting logic into functions should be about more than cutting down code. It’s breaking logic into discrete facts. With “insufficient_funds?”, it’s easier to support buying items on credit.

    However, I think that method should be public.

  17. KozOctober 04, 2007 @ 09:18 PM

    @Matthijs: There are still four permutations, but you can test them individually and when your test fails it will be something like test_insufficient_funds rather than test_validate. A small difference, but one that matters.

    @Phil: Yeah, validate is not documented, what’s up with that? :P

    @Hadley: As with any of our examples, we’ve had to cut down the context a little. But imagine this was a fully featured application with 10-15 validations, a bunch of associations and some business logic methods. If you were then tasked with tracking down a bug in a 30 line validate method you’d prefer the ‘after’. But with one or two, it’s certainly debatable.

  18. KozOctober 04, 2007 @ 09:22 PM

    @Evgeny: I appreciate your two cents but respectfully disagree. For starters validate doesn’t use alias_method_chain at all, and I certainly wouldn’t opt for an ugly implementation until I had profiling reports showing there was a problem.

    Given that in almost all actions you’re only going to be validating a single instance, you’re talking about four method calls, that will not matter in the scheme of things. Premature optimisation causes far more problems than intention revealing code.

    @Luke: Well caught!

    @Peter Cooper: I don’t quite follow you there, but there’s definitely some similarities going on. Validations are, to an extent, invariants you express about your models. Design by contract etc could be an interesting approach to use.

  19. domOctober 04, 2007 @ 10:19 PM

    Well, I agree completely with “It’s far more important for code to be human readable than incredibly concise”.

    In my experience, code is written for business purposes, paid for by a business, businesses change, and so cost-effective maintainability is very important, even more so than pure speed in many cases.

    Developers hired to maintain code are often very different people to those who created it in the first place and have moved on to the next thing. Different skillsets, experience, approach, ability, confidence. They need all the help you can give them.

    Any part of the code that expresses business logic should be (almost) readable by a business person.

    Framework code, well, that’s a different story. Go for efficiency.

  20. Trevor SquiresOctober 05, 2007 @ 12:12 AM

    Koz,

    I agree with where you guys are going here but I don’t like the flipping back and forth between method-name-intention.

    ensure_balance_is_sufficient_to_cover_amount returns true if insufficient_funds? returns false.

    It’s a bit like: “I need to decide whether to allow this record to be saved. Say ‘yes’ if you want me to say ‘no’.”

    I would have done ensure_sufficient_funds checks sufficient_funds?

    But then you have to ask, do we really need ensure_xxx just to add an error string?

    validate :sufficient_funds?, :message => “Not enough funds” validate :within_maximum_allowable_charge?, :message => “Charge is too great”

    To me it’s much more declarative. Too bad it doesn’t work :-)

    Trevor

  21. Trevor SquiresOctober 05, 2007 @ 12:14 AM

    grumble… formatting messed up. Here is what I meant to say:

    validate :sufficient_funds?, :message => “Not enough funds”

    validate :within_maximum_allowable_charge?, :message => “Charge is too great”

    Trevor

  22. KozOctober 05, 2007 @ 01:40 AM

    @Trevor: Now that’s an interesting idea for a plugin / patch…

  23. Trevor SquiresOctober 05, 2007 @ 02:02 AM

    Okay then…

    How about: http://pastie.caboo.se/private/gyka9yxrfblczytfkekq

    I’m not a fan of: Does not satisfy: “Within maximum allowable charge?” but it’s better than a kick in the pants if you don’t specify a :message.

    Patchworthy?

    Trev

  24. Barry HessOctober 05, 2007 @ 02:17 PM

    It appears we now know where Koz sits in the testing private/protected methods religious argument. I’m not really sure where I’m at with the thing…

  25. JustinOctober 06, 2007 @ 09:48 PM

    Why is there so much emphasis on ‘human readable’? Ruby is already extremely ‘human readable’ and if your decent with Ruby, you can easily read any written code with no problem, IMO.

  26. KozOctober 07, 2007 @ 03:43 AM

    @Barry: Well, actually I’d make those predicates public if I was going to test them. But I don’t tend to take a position on religious arguments :)

  27. Barry HessOctober 08, 2007 @ 02:33 PM

    @Koz: Or would you only test those predicates if they were public. :)

  28. Mário Sérgio Coelho MarroquimOctober 09, 2007 @ 06:12 PM

    I like the first and the second code. The refactoring was OK. This may not be the case, but I think that is somethin worng on the fact that some people spend hours trying to make their code more clean and more pretty. They do not add something useful, just re-code something that was just OK. The first code is OK! That means no need to apply some refactoring on it. I just can´t understand how a programmer looses time to refactor something that is already OK. We should care about FUNCTIONALITY and good code and tested code. Change a good code to just make it prettier and don´t add functionality is a loss of time. The fisrt code is OK!!!

  29. RabbitOctober 10, 2007 @ 09:15 PM

    @Mario

    I’m sure a lot of (clueless) business owners would agree with you. However, another argument for refactoring is that unless you do, it will become more difficult to add new functionality as time progresses and your code base gets larger with code that is simply “OK.”

    It’s not just about being pretty or readable, it’s about being able to expand and add. Chances are good when you were writing line 1 you had no idea what line 5,000 would look like.

  30. Brian BurridgeOctober 11, 2007 @ 03:37 PM

    @Mario

    It’s not just the benefit of being able to add new functionality over time, its being able to come back, in six months to a piece of code and not have to spend a huge amount of time retracing steps to figure out what is happening or what is broken.

    As Koz said, this particular example is not big enough to really display the impact. You can figure out what it is doing very quickly in either example. But if it were much longer, with a lot more code and all you wanted was to double check what validations were occurring, you can simply reference the validation list at the top with the built in descriptions (ie, the method names). If through testing you believe one of them isn’t working, you can focus in on that one specific validation and ignore the functionality of the rest of the class.

    This ability to focus at one task of a greater activity is really important for maintaining large applications over time.

  31. DougOctober 13, 2007 @ 04:27 PM

    Hmm, that “validate” class method is news to me also. Thanks for digging it up!

  32. John BachirOctober 15, 2007 @ 12:54 AM
    It’s far more important for code to be human readable than incredibly concise.

    And even more important, more maintainable, and easier to write tests for. (Which I suppose are both sort of encapsulated by “human readable” :-) )

  33. bad creditcardsDecember 11, 2007 @ 03:47 AM

    I’m sobbing :) Somebody stole my purchase with credit cards! Please advice where to file a fraud! I also need a pair of new credit cards, my credit is good or so. Where to go? My friend Liz told she’s got a pretty nice card from Discover and applied at

    0% balance transfer crdit cards

  34. bad creditcardsDecember 11, 2007 @ 03:48 AM

    I’m sobbing :) Somebody stole my purchase with credit cards! Please advice where to file a fraud! I also need a pair of new credit cards, my credit is good or so. Where to go? My friend Liz told she’s got a pretty nice card from Discover and applied at

    0% balance transfer crdit cards

  35. delcieDecember 19, 2007 @ 12:48 PM

    Hello, pal! Could you give me advice? I am in the process of looking for a credit card. I had some credit problems in the past and now I am not sure that my application will be approved. Will a denial damage my credit history? What to do? I am going to apply online at

    no bt fees discover discovery advantage member credit card

Comment