I’m going to have to go from memory here—I borrowed someone’s copy to read a few years ago, realized that it was not a very good book, and didn’t bother buying a copy for myself (and I’m not going to “borrow” an electronic copy, either). If I don’t have some of the specifics correct, please forgive me.
One of the things that stands out in my mind is a few related pieces of advice, most of which are fairly good in isolation:
- prefer composition to inheritance
- prefer more smaller methods (as I said elsewhere, I believe that there’s even a point where it’s suggested that a one-line method is “ideal”)
- prefer smaller objects
In isolation, each of these things is fairly good—but when combined, you change from a codebase of large, unreadable functions, to an unreadable codebase of lots of little, tightly-coupled, smaller objects with smaller methods all composed into doing something that would be simpler to understand as a few medium-size objects and medium-size methods.
In Ruby, all of this is…fungible is the best word I can think of for this. Because of Ruby’s duck-typing, mixins, and most importantly, lambdas and blocks, the best place for your logic to reside is as close as possible to where it’s being used. This doesn’t mean write large methods—it means that all of the advice that’s given for Java written before late 2008 (when lambdas were barely a twinkle in the JSR process and Sun still mattered) doesn’t apply when you have anonymous blocks of code that you can apply immediately and functionally.
The best description I’ve got for this ultimately is related to many of the things said about Gamma, et al.’s Design Patterns: in other languages, design patterns are just features that the language gives you, rather than something you have to implement. Iterators? Built into Ruby, and better because of blocks. In Ruby, you don’t need to learn the lessons given to Java programmers in 2008, because the language gives you more power and expressiveness already. Add a little bit of metaprogramming (in the same way that attr_reader/attr_writer/attr_accessor is implemented, for example) and you’ve got programs that express your intent clearly, but are nearly impossible to apply Clean Code to because you’re doing something smarter (not clever, but smart).
To address the three points that I mentioned a couple of paragraphs ago:
- prefer composition to inheritance: good advice, but composition in Ruby also means considering how blocks and mixins can help you understand your code better.
- prefer more smaller methods: I’m not as sold on this; the more methods you have, even if they’re private, the harder it is to understand just how your code works because you now have a larger API to understand. (And, like someone else in this conversation, I’ve had to deal with 2+kloc functions in other languages; I just haven’t had that happen in Ruby, where the largest functions that I’ve dealt with are in the low hundreds—and those are very rare but focussed functions (e.g., a lot of error handling where you don’t necessarily want to introduce state that lives outside of the function).
- prefer smaller objects: As with smaller methods, I’m not as sold on this. The more objects you have, the more interface you have to remember, the harder it is to remember how to compose everything back into the logic you’re trying to implement.
By all means, extract code—DHH’s advice on what they’re calling “concerns” in Rails is very good (I’ve already done that with more than a small bit of our current Rails application; I’ve also done it with other code over time).