Add class methods and instance methods to class by including one Module
Hi guys. I'm so glad to announce that article about Specification Pattern was picked for RubyWeekly #341. Also article Decoupling From Rails [Part 1] started just awesome discussion on Reddit. It means that we're moving to the right direction. I'm going to finish my series of articles about Decoupling From Rails soon, but today would like to talk about modules. How to add class methods and instance methods by including one module into class.
As Ruby-developers we know that we use modules for 2 main reasons: namespaces and mixins.
When we want to use module as a mixin we can use include
or extend
. There is also way to prepend
module. I covered that in this article.
Let's figure out the difference between include
and extend
.
If we use include
- all module methods will become instance methods of class. See this example:
module Persistence
def save
'saving'
end
end
class User
include Persistence
end
u = User.new
u.save # => 'saving'
We included Persistence
module into User
class. It means that all objects of class User
will have save
method. All methods of Persistence
module became instance methods of class User
.
Let's check how extend
works:
module Persistence
def find(id)
"looking for entity with id=#{id}"
end
end
class User
extend Persistence
end
User.find(10) # => looking for entity with id=10
Because we used extend
- all methods of Persistence
module became class methods of class User
. So we can call User.find(10)
now.
If you think about something like ActiveRecord model, it allows us to have both: instance methods and class methods. For model we can use class methods to find objects: User.all
, User.first
, User.where(first_name: "John")
, etc.
At the same time we have instance methods to update or save each particular user:
u = User.last
# instance methods
u.save
u.update(first_name: "Sergii")
What if we want to use Persistence
module to achieve the same results? To extend our class by both: instance methods and class methods.
Let's start from the easiest part, from instance methods. To achieve that we can use include
as we've seen before:
module Persistence
def save
'saving'
end
def update
'updating entity'
end
end
class User
include Persistence
end
u = User.new
u.save #=> saving
u.update #=> updating entity
First part is done. We included module and our objects have save
and update
methods now. Next interesting question is how to add class methods having the same code in User
class.
We can not use extend Persistence
because in that case save
and update
methods will be class methods of class User
.
To solve this problem we will use module callbacks. Let me show you how callbacks work:
module Persistence
def self.included(klass)
"Persistence module was included into #{klass}"
end
end
class User
include Persistence
end
It will print "Persistence module was included into User". Every time when any class includes module - Ruby will trigger self.included
method on that module. It will also pass class as a param. In our case User
class will be in klass
argument. There are couple more callbacks available: included
, extended
, prepended
for modules and inherited
for classes.
Now we know that we can get an access to class that included our module. We can take advantage of that and extend that class by class methods from the same module. I'll show final implementation and then we will go line-by-line to understand how it works:
module Persistence
def self.included(klass)
klass.extend(ClassMethods)
end
module ClassMethods
def all
puts 'all'
end
def find(id)
puts "looking for entity with id=#{id}"
end
end
def save
puts 'saving'
end
def update
puts 'updating entity'
end
end
class User
include Persistence
end
u = User.new
# instance methods
u.save #=> saving
u.update #=> updating entity
# class methods
User.all #=> all
User.find(1) #=> looking for entity with id=1
Ok, we achieved our goal. By including one module we extended User
class by instance methods (#save
, #update
) and class methods (.all
, .find
).
Let's go step-by-step:
class User
include Persistence
end
Class User
includes Persistence
module which makes save
and update
methods available for all objects of class User
.
To add class methods we've added ClassMethods
module inside Persistence
module and used callback included
to extend class User
by ClassMethods
module:
module Persistence
def self.included(klass)
klass.extend(ClassMethods)
end
module ClassMethods
def all
puts 'all'
end
def find(id)
puts "looking for entity with id=#{id}"
end
end
#...
end
As we know when we extend class by module - all module methods become class methods. That's why module inside Persistence
module called ClassMethods
. Using callback we extended class User
by ClassMethods
module which added class methods all
and find
to User
class.
Many gems use this technique to extend classes by instance methods and class methods at the same time. Pretty simple idea:
- Add instance methods to module
- Add
ClassMethods
module with methods that going to be class methods - Use
included
hook to extend class byClassMethods
usingklass.extend(ClassMethods)
- Include module into class
Thanks for reading. As usually I'll appreciate any feedback and sharing.
Happy coding!