Writing methods for both class and instance levels
When perusing the Rails documentation for various methods I noticed something that seemed odd to me. The method descriptions seemed to show an extra parameter as the first parameter that I was never using. It’s strange when you use the methods all the time and it doesn’t seem to be implemented the way the documentation describes it. So what’s going on here?
Well as it turns out the method is defined twice, but only in name. Once at the class level, and then at the instance level; where they pass the work on forward. How is this accomplished? Well you can access all class level methods on the .class instance variable… why do I call it a variable? Because it’s assigned to the Class from which the instance was instantiated.
"a string" == String # => false "a string".class == String # => true
So any class level methods available in the constant of the class name (eg: String) is also available on the instance variable .class .
class String def self.spell(str) str.split('') end end String.methods - Object.methods # => [:try_convert, :spell] "".class.methods - Object.methods # => [:try_convert, :spell] String.spell "hello" # => ["h", "e", "l", "l", "o"] "hello".spell # NoMethodError: undefined method `spell' for "hello":String "hello".class.spell "cow" # => ["c", "o", "w"]
So if we wanted to call the method spell on the string “hello” we would need to define a class instance method to do this.
class String def self.spell(str) str.split('') end def spell self.class.spell(self) end end "hello".spell # => ["h", "e", "l", "l", "o"] String.spell "hello" # => ["h", "e", "l", "l", "o"] "cow".class.spell "hello" # => ["h", "e", "l", "l", "o"]
Here you can see I’ve defined a class instance method spell without any parameters to be passed. That is because we are passing self, the instance of the string itself, as the parameter to the class level method spell. This is why you will see definition on methods with the object written in as a first parameter. If you’re calling the method on the class itself you will need that first parameter. If it’s an instance of the class then it evaluates self as the first method.
When implementing this with modules to be included into other classes you need to write just a bit more.
module Cat def self.included(base) def base.speak(words) [words, "meow"].join end end def speak self.class.speak(self) end end class String include Cat end
Now you have a cat that will always get the last word when you speak ;-). Defining self.included(base) is the way Ruby lets you perform the same action as if you were writing a def self.my_method right in the class it gets included into. The variable base is the class that called include. So the last part in the example above where include Cat is called inside the String class will make the base variable String.
module Cat def self.included(base) puts base end end class String include Cat end # => String
So defining methods inside the self.include(base) definition on base is the same thing as writing the methods into the class. self is the current Object you’re in wherever you are. So in the example above self.include is the same as Cat.include . And anything within the included defined on base is like self within the class called: def base.speak # => def String.speak
If you want class methods to be available on the .class instance variable then you can’t use the shovel operator (<<) like when you have seen class << self. Why is that? The reason is because you are not actually writing methods into the class, but you’ve opened up a singleton instance and are writing methods directly into there. It’s easier if you see it.
module Crackle puts self puts self.name end # => Crackle # => Crackle module Crackle class << self puts self puts self.name end end # => Crackle # =>
You’ll notice the name came out blank for the second item. That’s because when using the shovel operator << you are opening a singleton instance. These are anonymous objects. When defining a method in here you’re only working on a single instance of the class/module/object in which you define it. So in the second example the module Crackle isn’t only a Module now, but it has an anonymous singleton object which will only be accessed when you call the method on the class Crackle. Nothing that includes Crackle, or extends it, will get these defined methods. For an excellent presentation on this see Dave Thomas talk on “The Ruby Object Model”
Lets look at it not work.
module Crackle class << self def pizza 1 end end end class String include Crackle end String.pizza # NoMethodError: undefined method `pizza' for String:Class "".pizza # NoMethodError: undefined method `pizza' for "":String Crackle.pizza => 1
So even though we included Crackle into String we inherited nothing from the singleton instance that exists on the module Crackle. So don’t use the shovel operator for inheritance.
Rails has implemented a way for mixins to be done with what they call concerns. You need to consider some things when using Rails concerns. They aren’t as they seem. They perform some “management” of your mixins rather than being a pure mixin feature. For more details on that read Corey Haines blog post: Why I Don’t Use ActiveSupport::Concern or, why I actively replace ActiveSupport::Concern with Ruby in codebases I work on
Here’s how you can change your mixin to a ActiveSupport::Concern. You take something like:
module Dog def self.included(base) def base.speak(words) [words, "woof"].join end end def speak self.class.speak(self) end end
And change it to
module Dog extend ActiveSupport::Concern included do def self.speak(words) [words, "woof"].join end end def speak self.class.speak(self) end end
So consider for yourself if it’s what you want to do. Notice we’re using self instead of a parameter like base. In a standard mixin you can’t use self to refer to the caller. But with Rails’ concern self becomes as if you are in the class that is/(will be) inheriting this.
So now you know how to write methods in a DRY way and make them available on both instance and class levels. I like to use this wherever I can. self can be passed where it is the object that will be worked on. This works on objects where they hold any kind of state, query, or action. Even if size only make sense to you on an instance of the Object, it could just as easily be handed self via the class method and get the same job done. It’s up to you whether you like this as a pattern to follow. Yes it may be slightly confusing at first have the same method name take a different number of parameters. But it’s easy to see if it’s an instance or not based on the parameters or the error message. Just think about and try it out.
-Daniel P. Clark