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
endThat'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
# => nilIn our class, we define 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")
# MOOOThe def speak(roar) line of code, which is the opening of our method definition, can read like this:
Define an instance method named
speakthat will accept an argument that we will assign to the local variableroarfor 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 RexThe 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.