- Learn about modules––another way to lend functionality across classes.
In the previous lesson, we discussed the concept of inheritance. We learned that through subclasses, a certain class under a super class, the subclass has access to all of the methods of its parent. Inheriting one class from another makes sense. The subclass can be understood as a child or subordinate of the super class. For example, a car is a type of vehicle, so it makes sense for the Car
class to inherit from the Vehicle
class.
Let's think about a slightly different type of example, one that is less hierarchical. We could easily envision writing an app that models the environment of a dance performance. Such an app might have a Ballerina
class. Ballerinas, we know, perform dances. Similarly, we could imagine a little girl going to see the Nutcracker ballet one Christmas, coming home and wanting to practice all of the ballet moves from the show. So, we might write a Kid
class in which an instance of that class, our little girl who has gone to see the ballet, should have access to all those ballet moves (her performance skill not withstanding). This situation is not hierarchical, like our Car
and Vehicle
example. Instead, Kid
and Ballerina
simply need to share some functionality, without being related in any other meaningful way.
This is where modules come in. Modules allow us to collect and bundle a group of methods and make those methods available to any number of classes. In this exercise, we'll be defining a Dance
module and making it available to both the Ballerina
and Kid
class.
This is a code along exercise. Fork and clone this repo by clicking the Github link at the top of the page. Follow along with the walk-through below to get your code working. Get the tests to pass.
We'll code our Dance
module inside the lib/dance_module.rb
file. Open up that file and define your module with the following code:
module Dance
end
Let's give our Dance
module some fabulous moves:
module Dance
def twirl
"I'm twirling!"
end
def jump
"Look how high I'm jumping!"
end
def pirouette
"I'm doing a pirouette"
end
def take_a_bow
"Thank you, thank you. It was a pleasure to dance for you all."
end
end
Okay, now we'll define our Kid
class and tell it to include the capabilities of the Dance
module.
Open up lib/kid.rb
and define your Kid
class:
class Kid
end
Let's do the same for the Ballerina
class in lib/ballerina.rb
:
class Ballerina
end
Now we're ready to include our module in our classes:
To lend our two classes all of the methods of the Dance
module, we use the include
keyword:
class Kid
include Dance
attr_accessor :name
def initialize(name)
@name = name
end
end
class Ballerina
include Dance
attr_accessor :name
def initialize(name)
@name = name
end
end
If we use include
keyword, we allow our classes to use all of the methods of the included module as instance methods. We'll talk about how to lend a module's methods as class methods in a minute.
Now that we've included the module, open up bin/dance_party
and get familiar with following code:
require_relative "../lib/kid.rb"
require_relative "../lib/ballerina.rb"
angelina = Kid.new("Angelina")
mikhail_barishnkov = Ballerina.new("Mikhail")
puts "#{angelina.name} says: #{angelina.twirl}"
puts "#{mikhail_barishnkov.name} says: #{mikhail_barishnkov.take_a_bow}"
Now, run the file by typing ruby bin/dance_party
and you should see the following output in your terminal:
Angelina says: I'm twirling!
Mikhail says: Thank you, thank you. It was a pleasure to dance for you all.
In order to lend a module's methods to a class as class methods, we use the extend
keyword. Let's write yet another module that we can extend into our classes as class methods. For the purposes of this example, let's create a shareable class method, metadata
, which will report on some pertinent (shared) information regarding both classes.
Open up the lib/class_method_module.rb
and define the following module and methods:
module MetaDancing
def metadata
"This class produces objects that love to dance."
end
end
Let's extend
this module into both the Kid
and Ballerina
classes:
class Ballerina
extend MetaDancing
end
class Kid
extend MetaDancing
end
Now, open up the bin/extending file and familiarize yourself with the following code:
require_relative "../lib/kid.rb"
require_relative "../lib/ballerina.rb"
puts Kid.metadata
puts Ballerina.metadata
Run the file with ruby bin/extending
and you should see the following output in your terminal:
This class produces objects that love to dance.
This class produces objects that love to dance.
Run the tests to make sure all of your tests are passing.
In the first code along, we built a module called Dance
, which contained methods that we intended to be used as instances methods in the Ballerina
class.
In the second code along, we built the module MetaDancing
, who's methods were intended to be used as class methods in the Kid
and Ballerina
classes.
There are two drawbacks to this approach. First, if another developer looks at your modules, there is absolutely no way to determine how those methods are intended to be used. Are they class methods? Are they instance methods? Nobody knows!
Secondly, we had to build two separate modules that contained methods that were all related to the same functionality (dancing). But because there was no way to designate class methods versus instance methods, we were forced to define two separate modules, which violates the single responsibility principle. Wouldn't it be great if there was a way to define one module and specify which methods were intended as class methods and which methods as instance methods.
Guess what there is!! We're going to refactor the two modules into one, and use nested module namespacing to clarify our code.
module FancyDance
module InstanceMethods
def twirl
"I'm twirling!"
end
def jump
"Look how high I'm jumping!"
end
def pirouette
"I'm doing a pirouette"
end
def take_a_bow
"Thank you, thank you. It was a pleasure to dance for you all."
end
end
module ClassMethods
def metadata
"This class produces objects that love to dance."
end
end
end
First, we define our FancyDance
module. Then, inside the FancyDance
module, we define a second module, InstanceMethods
. Inside the InstanceMethods
module, we place all our methods that we intend to be used as instance methods (twirl
, jump
, pirouette
, take_a_bow
). Next, we define a second nested module (nested inside of FancyDance
) called ClassMethods
. Inside, we place the metadata
method, which we intend to be used as a class method.
So how do we use these nested modules?
class Ballerina
extend FancyDance::ClassMethods
include FancyDance::InstanceMethods
end
class Kid
extend FancyDance::ClassMethods
include FancyDance::InstanceMethods
end
We refer to the name-spaced modules or classes with ::
. This syntax references the parent and child relationship of the nested modules.
Remember, include
is used to add functionality to our classes designed to be used as instance methods. The InstanceMethods
module inside the FancyDancy
module, contains the methods twirl
, jump
, pirouette
, and take_a_bow
, which any instance of the Ballerina
class and the Kid
class can do.
We can call:
angelina = Ballerina.new
angelina.twirl
// returns "I'm twirling!"
angelina.jump
// returns "Look how high I'm jumping!"
buster = Kid.new
buster.jump
// returns "Look how high I'm jumping!"
buster.take_a_bow
// returns "Thank you, thank you. It was a pleasure to dance for you all."
Because we included the FancyDance::InstanceMethods
nested modules, we can call those instance methods on instances of our classes.
And extend
is used to add additional functionality to our classes by way of class method. We can now call the meta-data
class method on the Ballerina
class and the Kid
class:
Ballerina.meta_data
// returns "This class produces objects that love to dance."
Kid.meta_data
// returns "This class produces objects that love to dance."
Inheritance using the <
syntax, implies that a class is a type of something. A BMW
class should inherit from Car
class, because a BMW is a type of car. class BMW < Car
.
But what about ::
that we're using for our modules? The ::
syntax just denotes a name-space. Doing BMW::Car
just gives the BMW
class access to all constants, instance methods, etc, without stating that a BMW is a type of car. The ::
syntax carries all public items over to the class or module that is "inheriting" from it.
That's it! Now that we are familiar with several methods of sharing code between classes, you're ready to move on to the next few labs.
If you have a module whose methods you would like to be used in another class as instance methods, then you must include the module.
If you want a module's methods to be used in another class as class methods, you must extend the module.
View Modules and Mixins in Ruby on Learn.co and start learning to code for free.
View Intro to Modules on Learn.co and start learning to code for free.