TL; DR
Rather than including modules and hooking into .included
, write a method that adds the behaviour (even if the method just includes and extends).
Introducing the pattern
It's common practice in Ruby these days to use modules for all kinds of crazy.
Here is the most eye-widening example I've seen. You might think you're getting one thing, but you're not, you're getting all the things. Base, anyone?
So what's the problem?
Include has a meaning, and this changes its meaning.
When I include something, I expect to get the module's methods for my class' instances. Honestly, I know include is a method, but I think of it like a keyword. Until I realized this "pattern" was common, I was astounded when I would somehow magically have class methods, the class is a completely different object!
It pollutes the ancestry.
When you include a module, it creates a class with the module's methods and sets it as the superclass of the object. So first of all, it is pollutive (clutters up the ancestry when you are trying to look around) and second of all it is a performance hit. Whenever you invoke a method, it goes up the ancestry, one after the other, searching for the method in each of them. In this case, the module has no methods itself, it was just a way to include InstanceMethods and extend ClassMethods. An empty stop along the call chain. Even the Rails team has realized this and stopped using the InstanceMethods side of the pattern.
It's unreliable and breaks module semantics.
When I want the module's methods on my instances, I include. When I want them on my class, I extend. Thus these two should be equivalent:
And they basically are equivalent, except if you start abusing hooks, consider:
In the first example, the hook was invoked, in the second, it was not. So if you are relying on the hook to add the behaviour, then for it to behave correctly I must be aware of this and adjust my usage.
It's a problem of perception.
In his talk at GoGaRuCo Why does Jeff Casimir1 write this code?
Because he wants to use it like this:
He knows he can extract the code with a module, so he has modules on his brain. And the way to use modules is to include them. I love around 7:19 when he says "I always use code before I write it. So I would write something like this where I would include the module". I consider that a best practice. But here, he isn't using the code before he writes it, it's already written in his brain. Which means the code that is supposed to use it (the code necessitating its existence) is written to the implementation he already has in mind.
This just uses modules to dispatch methods. There is absolutely no reason to include a module here. The module is mixed in because it will invoke the included method... which only needs to exist because that's how you hook into mixins. A regular method would work just fine. He could literally replace include Contact
with Contact.included self
.
So what is the alternative?
We need to stop cargo culting module patterns. We can get behaviour any way we want. In the above example, we could use this alternative:
Now I'm not advocating toplevel method definition, in reality I'd probably put it in a module as a namespace. But this shows that the mixin was just an elaborate ruse to invoke a method that could be invoked directly, or replaced with something more expressive. Some say it's better to be clear over clever.
On Deject I just have a function with the same name as my module 2. In one implementation, it extends the class you pass it. Sure you could get the same thing by just saying extend Deject
but if it ever changes, then the interface will have to change, or I'll have to start abusing hooks. By having a method which is responsible for doing to the class whatever things need to be done, it does not change the meaning of include. In another implementation, it doesn't use modules at all, it just adds the desired methods.
When you need both class and instance methods, give your module a descriptive method which will do the extending and including (or simply defining), there is no need to rely on the hook.
Here are some example alternatives
Define the method explicitly
Include and extend implicitly
And of course, many things which modules are used for could actually be done with composition ;)
Footnotes
1 Jeff is a great guy and his talks always get me thinking and reconsidering, so I feel okay using him for my example. Though this is quite common across the Ruby community. Plus it's hard to resist when the name of the talk is "The Problem is Your Ruby" :P
2 In this example, "deject" is a verb so I felt okay making a method with the same name as the constant itself. I debated this a while, but ultimately was convinced by Execution in the Kingdom of Nouns that it was an acceptable decision.