Modules called, they want their integrity back

Modules called, they want their integrity back

Josh Cheek

February 03, 2012

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.

module IWishIWasAClass
		def self.included(klass)
				klass.send :include, InstanceMethods
				klass.extend ClassMethods
		end
		# ...
end

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:

class TheCommonWay
		extend SomeModule
end

class TheEquivalentButUglyWay
		class << self
				include SomeModule
		end
end

And they basically are equivalent, except if you start abusing hooks, consider:

module SomeModule
		def self.included(klass)
				puts "SomeModule was included in #{klass.inspect}"
		end
end
Class.new { include SomeModule } # prints "SomeModule was included in #"
Object.new.extend SomeModule # nothing printed

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?

this

Because he wants to use it like this:

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:

def add_contact_associations_to(ar_base_subclass)
		ar_base_subclass.send :has_many, :phone_numbers
		ar_base_subclass.send :has_many, :email_addresses
end

class Person < ActiveRecord::Base
		add_contact_associations_to self
		def name
				[last_name, first_name].join
		end
end

class Company < ActiveRecord::Base
		add_contact_associations_to self
end

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

module M
		def m_ify(klass)
				add_class_method klass
				add_instance_method klass
		end

		def add_class_method(klass)
				def klass.class_method
						'some class method'
				end
		end

		def add_instance_method(klass)
				klass.class_eval do
						def instance_method
								'some instance method'
						end
				end
		end
		extend self
end
c = Class.new
M.m_ify c
c.class_method # => "some class method"
c.new.instance_method # => "some instance method"

Include and extend implicitly

module M
		module ClassMethods
				def class_method
						'some class method'
				end
		end

		def self.m_ify(klass)
				klass.extend ClassMethods
				klass.send :include, self
		end

		def instance_method
				'some instance method'
		end
end
c = Class.new
M.m_ify c
c.class_method # => "some class method"
c.new.instance_method # => "some instance method"

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.