A deeper look into Ruby Modules
I recently completed the Launch School RB129 written assessment. After doing some revisions, and a lot of research online, I still felt like Ruby Modules were not extremely clear. After writing this article, I now feel like I have a good grasp on what a Module is, how to use them, and what their overall role is. I hope you feel the same after reading it!
What is a Ruby Module?
A module is a way for a Ruby programmer to encapsulate logically similar classes, methods, constants, and even other modules. A module is included in other Ruby classes by using the include
method, which can also be referred to as a mixin
. Here is an example of a module:
module Towable
def tow
"I can tow something!"
end
endclass Truck
include Towable
endtruck = Truck.new
truck.tow # => "I can tow something!"
We define a module by using the module
keyword and using a capital letter as the initial character. If a module had multiple words, then camel case syntax is preferred (ex: module TowTruck
). It is also a common naming convention to use an “able” suffix on the verb that the module is modeling.
Inside a module, we can define methods (class and instance methods) similar to how we would define methods inside of a class. In the example above we have defined an instance method tow
within the Towable
module. There is also a Truck
class that has been defined, and we are using the include
method to include the Towable
module and all of the functionality that has been defined within it. Finally, we are initializing a local variable truck
and assigning it to a new Truck
object (or a new instance of the Truck
class). Since we have access to the tow
instance method, we invoke it on truck
, and the program returns "I can tow something!"
.
Why not just use a Class?
Now I know what you’re thinking — why wouldn't I just create a class and inherit the instance methods that way. I’ll provide another example to show you why.
class HockeyPlayer
def slap_shot
"Top shelf! The goalie never had a chance."
end
endclass SoccerPlayer
def penalty_kick
"The ball was faster than the goalie!"
end
endclass Person
end
Here we have an example with three different classes — HockeyPlayer
, SoccerPlayer
, and Person
. As a person who loves sports (not a Person
object, a real person) I don’t want to have to choose between playing hockey or playing soccer. In Ruby, we are only able to inherit from one superclass (either the HockeyPlayer
class or the SoccerPlayer
class) which isn’t great. Let’s fix that by using modules!
module Hockeyable
def slap_shot
"Top shelf! The goalie never had a chance."
end
endmodule Soccerable
def penalty_kick
"The ball was faster than the goalie!"
end
endclass Person
include Hockeyable
include Soccerable
endnathen = Person.new
nathen.slap_shot # => "Top shelf! The goalie never had a chance."
nathen.penalty_kick # => "The ball was faster than the goalie!"
You can see that we are able to include multiple modules in the Person
class, which means the Person
object nathen
has access to both slap_shot
and penelty_kick
instance methods. This is how Ruby addresses the inability to inherit from multiple classes — by way of multiple module mixins
.
What is the role of a Module?
Now that you understand what a module is, and how you can use it, you are ready to understand what the role of a module is. When we define classes in a Ruby file, we are defining them in the global namespace
. This is fine for simple programs, but as soon as you start to add more code and complexity, there is more room for clashing names of classes and methods. Take this example for instance:
# backend.rb
class User
def write_code
"You write awesome Ruby code!"
end
end# more code in between#frontend.rb
class User
def write_code
"Finally, the box is centered."
end
endnathen = User.new
nathen.write_code # => "Finally, the box is centered."
Well, that is confusing. What if we wanted to have a backend developer and a frontend developer? They both write code, but we don’t want one write_code
instance method to overwrite the other. Once again, let’s bring a module to the rescue.
module BackEnd
class User
def write_code
"You write awesome Ruby code!"
end
end
endmodule FrontEnd
class User
def write_code
"Finally, the box is centered."
end
end
endnathen = BackEnd::User.new
nathen.write_code # => "You write awesome Ruby code!"
Here we have defined two modules that describe two different namespaces — BackEnd
and FrontEnd
namespaces if you will. Now we are not only able to be much more clear about whichUser
object we are creating, but we are also able to identify much faster what we expect the program to output. You can see we are using the ::
operator to instantiate the User
class from within the BackEnd
module, which is conveniently named the namespace resolution operator
.
From this, we can tell thatnathen
is assigned to the User
object inside the BackEnd
module, which is why invoking thewrite_code
instance method on nathen
returns "You write awesome Ruby code!"
.
Note: The ::
operator is not limited to only modules. It can be used on classes, modules, and constants.
This is a very simple example, but when we start to work with larger programs that include multiple files and many classes, you can see how class names and even instance methods can easily collide and be overwritten when you put all of them together. This is where the power of modules and creating those ‘safe places’ (namespaces) for you to define classes and methods come into play, making for a much nicer and less conflicted coding experience.
Other considerations
Now I just told you all of these awesome things about modules and how powerful they are, but when designing Object-Oriented programs there are a few things to consider before just putting modules everywhere.
Modules cannot be instantiated. If you are planning on creating instances that have access to all of the functionality within a module, just know that the module itself cannot be instantiated. You are able to instantiate a class within a module (like the developer example above), but the module itself cannot have instances of itself.
Modules modify the method lookup path. If we use the Person
class from above I can demonstrate what I mean.
# modules omitted for brevity.. :(class Person
include Hockeyable
include Soccerable
end
In order to see the method lookup path, we need to use the .ancestors
class method like this: Person.ancestors
, which would return the following Array
of objects:
[Person, Soccerable, Hockeyable, Object, Kernel, BasicObject]
We can see that our Soccerable
and Hockeyable
modules are included in the method lookup path (sometimes referred to as a hierarchy). One thing that is interesting is modules are ordered in the hierarchy from bottom to top, as opposed to top to bottom like they are included in the Person
class. This is important when calling methods and knowing which classes and modules would be ‘looked in’ first. If Hockeyable
and Soccerable
each had an instance method with the same name, the one in Soccerable
would be called first since it appears first in the method lookup path.
Modules can be included in multiple classes. I showed an example above on how multiple modules can be included in a single class, but it’s good to know that a single module can be added to multiple classes. This can be useful when you want functionality in one class, but you don’t want to create another class hierarchy, or if different classes do not inherit from the same superclass.
module Swimmable
def swim
"I can swim!"
end
endclass Pet
endclass Fish < Pet
include Swimmable
endclass Dog < Pet
include Swimmable
endclass Cat < Pet
end
We could define a swim
method within the Pet
class, but because cats can’t swim (or don’t like to), we wouldn’t want to do this. Here I have defined a Swimmable
module and a swim
method within it. Now I only had to define the swim
method once and can include it in both the Fish
and Dog
class. This also makes sure only the Pet
objects (or subclasses of the Pet
class) that want to swim are able to swim.
This isn’t everything you could know about modules, but it is certainly a step in the right direction. I hope you enjoyed the article. Let me know if you had any questions or comments!