Decorators Verses the Mix-in
It is a neat time to be involved in the Ruby community, if you ask me. A large portion of us are currently studying the techniques for doing good object oriented development. We are looking at the ideas that have come before and trying to decide the best ways to apply those ideas to our favorite language. This leads to blog posts, forum threads, and conference talks about what we are learning. No matter what, we all gain from explorations like this. Everybody wins as our collective knowledge grows. We all deserve gold stars.
So far, there's one point pretty much everyone agrees on: composition should typically be preferred to inheritance. The trickier part of that discussion though is deciding what composition looks like in Ruby. Generally you see Rubyists comparing the merits of decorators and mix-ins. [Note: the comments correctly pointed out that this was a bad use of the word "composition" on my part, to describe mix-ins.] There's a very representative thread on the excellent Objects on Rails mailing list.
Single Method Classes
[Update: I've changed my mind about some of the following due to this excellent counter argument.]
In the words of Dennis Miller, "I don't want to get off on a rant here, but…"
There's something that drives me crazy and I see it in so much Ruby code. I see it in the documentation for our key projects; I see Rubyists of all skill levels doing it; it's just everywhere.
Let's talk about when the use of a
Classis and is not appropriate.
The Chained new()
Here's an example of one form of code that bugs me:
class Adder def add(n) 40 + n end end p Adder.new.add(2)
The problem here is that a
Classhas been used, probably because as Rubyists that's always our default choice, but it's the wrong fit for this code. A
Classis for state and behavior. The example above is just using behavior. No state is maintained.
A handy tip for sniffing out this problem is watching for a call to
new()in the middle of method chaining as we have here. If you always use a
Classlike that, it's not really a
Class. Put another way, if an instance never gets assigned to a variable, something has likely gone wrong with the design.
Learn to Love Mix-ins
The road to mastering Ruby is paved with understanding some key Ruby concepts. Mix-ins are one of those concepts. I'm sure everyone reading this knows the mechanics of how mix-ins work, but it pays to spend some time really thinking about all that mix-ins imply. Let's do just that.
Adding a Type
One of the primary reasons that Ruby needs mix-ins is that it does not support multiple inheritance. That leaves mix-ins as our only option for modeling hybrid objects. It's the way Ruby programmers can add another type.
That's a good way to think about it too: adding a type.
Take pagination, for example. Pagination methods are usually defined to return an object like this:
class PaginatedCollection < Array # ... paginated helpers defined here ... end
That's never really felt right to me though.
First, inheriting from Ruby's core classes can come back to bite you in some scenarios. The reason is that Ruby makes some performance tradeoffs to keep the core classes fast, but those tradeoffs mean that those classes don't always perfectly follow Ruby's rules.
Class Level Mix-ins
A question that comes up pretty often in Ruby circles was posed again today, by Xavier Noria:
# # Is there standard idiom to add module methods to classes that mix them in? # # That is, I would like class C to croak: # module M def self.croak puts "Croak!" end end class C include M croak # doesn't work end
This brings up a couple of interesting points about mix-ins. Obviously, class methods are not easily added to things, but we generally want class methods so we can call them directly from the module. Here's how I deal with this issue.
First, instance methods are the way to go for anything you will be mixing in, period. With instance methods, you can inject them into a class or object. With anything else you have to start hacking. That gives us our first change:
module M def croak puts "Croak!" end end
Now, we really wanted a module method, so I've made things worse. However, as I've said, we have all the choices with this setup. Let's just mix the module into it's own class!