Classes are a way of defining a blueprint for how a concept, such as a Dinosaur
, should be described and what actions it should be able to perform. For example, a Dinosaur
might have the descriptors type
, gender
, and health
and might have actions like move
and eat
. We place these attributes and actions into a class called Dinosaur
because they are specific to dinosaurs and not to another concept that exists in our application.
In this lesson, we will discuss how to create and use classes to organize our code in to defined, extendable concepts.
To begin a class definition, use the class
keyword, followed by the CamelCased1 noun that you are describing. Make sure to include an end
to close the class definition.
class Dinosaur
end
That's it!
Classes are essentially blueprints for concepts. This means that we start with an outline/template/blueprint for a concept that comes with default values (given any are provided). Imagine that we have three dinosaurs: a triceratops, a t-rex, and a pterodactyl. Each one of these is a different type of dinosaur with its own gender and health; we could say each of these is a different instance of our Dinosaur
blueprint. In order for us to create a new instance (a copy of the default that we can play with) of our class that can exist separately from any other instances, we use the new
command:
trex = Dinosaur.new
# => #<Dinosaur:0x007faaec8ddd50>
Behind the scenes, this command sets aside a portion of your computer's memory so that we have a temporary place to store and retrieve information from the computer about our individual dinosaur, trex
. You can ignore the 0x007faaec8ddd50
part, as it will be unique to every instance.
Remember our eat
and move
actions from the introduction? Actions and verbs associated with our class can be built in to that class as methods: lines of code that execute and ultimately return a single value. When an action pertains only to an individual instance of a class, such as only move
the trex
or only have the pterodactyl
eat
, then these types of methods are called instance methods, for they only affect the instance they are tied to. Here is an example of our trex
being told to speak
:
class Dinosaur
def speak
puts "RAWRRRR"
end
end
trex = Dinosaur.new
trex.speak
# RAWRRRR
# => nil
In our class, we def
ine a method named speak
. Because this method definition happens inside of our class definition with def method_name_here
, we tie this method to all instances of our class. Then, once we have our trex
instance, we tell trex
to run (call) the speak
method by using a period (.
) to join the two together. If trex
is an instance of Dinosaur
, and speak
is an instance method of Dinosaur
, then that means that all instances of Dinosaur
will be able to call the speak
method.
Calling speak
did indeed print RAWRRRR
to the console, but also notice that nil
was returned. Since the puts
command returns nil
, and it is the last line of our method, that means the entire method will return nil
as its value.
In our previous example, we assume that our dinosaur will always speak "RAWRRRR"
; however, this is not fair to pterodactyls who "SCREEEEECH"
, nor is it fair to triceratops who "MOOO"
. Let's make our speak
method a bit more dynamic:
class Dinosaur
def speak(roar)
puts roar
end
end
trex = Dinosaur.new
trex.speak("RAWRRRR")
# RAWRRRR
pterodactyl = Dinosaur.new
pterodactyl.speak("SCREEEEECH")
# SCREEEEECH
triceratops = Dinosaur.new
triceratops.speak("MOOO")
# MOOO
The def speak(roar)
line of code, which is the opening of our method definition, can read like this:
Define an instance method named
speak
that will accept an argument that we will assign to the local variableroar
for use only within our method. An argument, which is also known as a parameter, is an expression that is passed from where a method is called to the method itself. The method receives the argument, as an athlete would receive the pass of a ball, and assigns the value of the argument to a variable, and then does something with this variable.
The local variable roar
— assigned the values "RAWRRRR"
, "SCREEEEECH"
, or "MOOO"
— does not exist outside of the scope of our speak
method. So if you tried to ask for the value of roar
from anywhere else, you would see the error NameError: undefined local variable or method 'roar' for main:Object
.
Passing multiple arguments involves simply separating the arguments by commas:
class Dinosaur
def speak(roar, type)
puts "The #{type} roared, \"#{roar}\""
end
end
trex = Dinosaur.new
trex.speak("RAWRRRR", "T-Rex")
# The T-Rex roared, "RAWRRRR"
Make sure you receive the arguments in the same order that they are passed! Otherwise, you might get output like
The RAWRRRR roared, "T-Rex"
We know that when a method accepts an argument, it creates a local variable that only exists within the scope of that method. If we would like for a variable to stick around for the duration of our Dinosaur
instance, we can create instance variables to store values and be accessed later. Instance variables are prefixed with an @
sign. Here is an example where we set a Dinosaur
's @type
via a method called set_type
:
class Dinosaur
def set_type(type)
@type = type
end
end
trex = Dinosaur.new
=> #<Dinosaur:0x007fd90387c510>
trex.set_type("T-Rex")
# => "T-Rex"
trex
# => #<Dinosaur:0x007fd90387c510 @type="T-Rex">
In this example, we pass the set_type
method a string, T-Rex
, and the set_type
method receives the value and assigns it to an instance variable named @type
. When we view the trex
variable again, we see that its instance now has @type="T-Rex"
as part of the instance. Congrats! We have saved data to our Dinosaur
instance. Now, let's see other ways of setting and getting back information from our instance.
It would be splendid if we could pass the trex
's type
when we are creating the Dinosaur
, instead of having to call an additional method later. Here is what we want:
trex = Dinosaur.new("T-Rex")
# => #<Dinosaur:0x007fd90387c510 @type="T-Rex">
When a new class instance is created (instantiated), Ruby has a method called initialize
that is executed behind the scenes that we can take over to make this work.
class Dinosaur
def initialize(type)
@type = type
end
end
trex = Dinosaur.new("T-Rex")
# => #<Dinosaur:0x007f8c93821ed8 @type="T-Rex">
Our new
method has more than meets the eye! It can be passed arguments, and these arguments can be received and utilized in the initialize
method. This is lovely, but if you try to access the @type
instance variable, you're going to have a rough time! Instance variables are considered internal aspects of a class and are not easily accessible from our IRB session. Read on to see how we can get and set these instance variables with ease.
We need a public way of asking for a Dinosaur
instance's type
, so an instance method would be great for this! A method that returns the value of an instance variable can be called a getter method.
class Dinosaur
def initialize(type)
@type = type
end
def type
@type
end
end
trex = Dinosaur.new("T-Rex")
trex.type
# => "T-Rex"
All we do here is create a publicly accessible instance method that simply returns @type
.
Imagine our boss has told us that "T-Rex"
is too informal, and we need to update the instance's type
to "Tryannosaurus Rex"
. Given we can get trex.type
, how might we go about setting that value to something else? We could always create a new instance, but that defeats the purpose!
class Dinosaur
def initialize(type)
@type = type
end
def type
@type
end
def type=(new_type)
@type = new_type
end
end
trex = Dinosaur.new("T-Rex")
trex.type = "Tyrannosaurus Rex" # setter method
trex.type # getter method
# => Tyrannosaurus Rex
The line def type=(new_type)
looks strange; I know. But this is how you create a setter method that lets you call trex.type = "Tyrannosaurus Rex"
.
You might be thinking, "All that work for one little attribute?! There must be a better way..." There is!
Imagine if we had getter and setter methods for 5, 10, or even 20 Dinosaur
instance values; our class definition would be massive! Ruby exists to help make developers happy, so here's Ruby to the rescue:
class Dinosaur
attr_accessor :type
end
trex = Dinosaur.new
trex.type = "Tyrannosaurus Rex"
trex.type
# => "Tyrannosaurus Rex"
trex.methods
# => [:type, :type=, ...]
The attr_accessor
method accepts a comma-separated list of Symbols and creates getter and setter methods behind the scenes. If you call trex.methods
, you can see a list of the methods available to trex
, and our type
and type=
methods are at the front!
Be aware that if you want to keep the Dinosaur.new("T-Rex")
pattern, you'll still need to use def initialize
, as we did before. Note that if you want to call an instance method from within the instance, itself, you can use self.method_name
to access it:
class Dinosaur
attr_accessor :type
def initialize(type)
self.type = type
end
end
trex = Dinosaur.new("Tyrannosaurus Rex")
trex.type
# => "Tyrannosaurus Rex"
There may be times when there are actions that are relevant to the concept of dinosaurs but not individual dinosaurs, themselves. Thus, these actions would not make sense existing on every single instance of Dinosaur
, so we can instead use a concept known as a class method.
class Dinosaur
def self.historical_blurb
"Dinosaurs ruled the earth for a very long time but went extinct ~65 million years ago. Archaeologists continue to make new discoveries and learn more about these fantastic beasts."
end
end
Dinosaur.historical_blurb
# => "Dinosaurs ruled the earth..."
Class methods exist on the class, so they don't require a new instance (via new
) and can be called directly. Prepending self.
in front of the method's name tells Ruby that historical_blurb
is a class-level method. If you try to call historical_blurb
from an instance, like trex.historical_blurb
, you will receive an error, so make sure you use the class name in front of the method.
By now, we are tired of writing a T-Rex's type over and over again, and where T-Rex might walk to move, a pterodactyl typically flies. T-Rex and pterodactyls are both types of dinosaurs, so we need a way to inherit aspects of a Dinosaur
for each instance but also have control over each individual. We can do so by using inheritance.
class Dinosaur
attr_accessor :type
def move
"Moving like some generic dinosaur..."
end
end
class TRex < Dinosaur
def initialize
self.type = "Tyrannosaurus Rex"
end
def move
"Walking on two legs..."
end
end
class Pterodactyl < Dinosaur
def initialize
self.type = "Pterodactyl"
end
def move
"Flying high!"
end
end
rexxie = TRex.new
rexxie.type
# => "Tyrannosaurus Rex"
rexxie.move
# => "Walking on two legs..."
birdie = Pterodactyl.new
birdie.type
# => "Pterodactyl"
birdie.move
# => "Flying high!"
There is a bit going on here, so let's break it down.
First, we create a Dinosaur
class with a getter and setter for type
, for we know that every Dinosaur
will have a type
.
Next, we create another class named TRex
and use the less than symbol, <
, to tell Ruby that TRex
should inherit the functionality of a Dinosaur
. We do the same thing for Pterodactyl
. Both of these subclasses each make use of the setter instance method type
within def initialize
. Since type=
is a method that exists on Dinosaur
, that means the subclasses each have access to it, too.
Lastly, we define different move
methods on TRex
and Pterodactyl
in order to have direct access to how each one moves. Note that the move
method on the Dinosaur
class is never seen! This is called method overriding; we override the inherited move
method and replace it with each subclass' own move method. Think of the default one as a fallback for if move
is not defined on a subclass.