Skip to content

Instantly share code, notes, and snippets.

@rpearce
Last active May 10, 2022 02:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rpearce/18f22a78db1bc665be27 to your computer and use it in GitHub Desktop.
Save rpearce/18f22a78db1bc665be27 to your computer and use it in GitHub Desktop.
Ruby Fundamentals - Classes

Classes

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.

Defining a Class

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!

Creating a New Instance

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.

Instance Methods

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 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.

Passing an Argument

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 variable roar 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

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"

Instance Variables

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.

Getting and Setting Data

Upon Initialization

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.

Getter Methods

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.

Setter Methods

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!

Simplifying With attr_accessor

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"

Class Methods

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.

Inheritance

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment