A Beginner’s Guide to Ruby Classes (Part 1)

Graham Watson
Sep 9, 2018 · 8 min read

When first starting out with Ruby, we very quickly encounter some basic classes: strings, integers, arrays, etc. We also find that each of these classes has numerous methods that can be called to perform various actions on instances of these classes. For example, we could call the flatten method on a nested array to turn it into a single array.

array = [1, [2, 3], 4]
array.flatten
=> [1, 2, 3, 4]

But what if we had numerous objects with similar or even identical attributes? We could copy and paste a ton of code for each of these objects if we wished. But that sounds like a whole lot of work, and isn’t the whole point of programming to avoid needless, repetitive work? Luckily, we can create our own classes, each with their own methods, that essentially serve as templates for any similar objects we might create.

Defining a New Class

To create our new class, we simply use the keyword class followed by the capitalized name of our class. When finished, we will use the keyword end. In our example, we will create a Car class.

class Carend

Creating New Instances of a Class

Once we have created our class, we need to be able to create instances of the class we have created. To do so, we need only type the name of our class followed by .new. We can also assign a name to the instance by choosing a name and setting it equal to our new instance. If we need to refer to this new instance later on, we can call it by name.

toyota_corolla = Car.new
=> #<Car:0x007f9f0f27b6d0>
toyota_corolla
=> #<Car:0x007f9f0f27b6d0>

This alone does very little for us. We have an instance of the Car class, but as of now, this instance has no useful properties.

The Initialize Method

When we create a new class instance, we might want to assign it some default attributes at the moment of its inception. To do this, we use the initialize method. This works just like creating any other method, but here the arguments passed in will be the attributes we would like every new instance of our class to have. For example, every car should have a make (or manufacturer, if you prefer) and a model. Let’s see how creating an initialize method for our Car class might look.

class Car   def initialize(make, model)
@make = make
@model = model
end
end

OK, so what the heck just happened there? Suddenly we’ve got ampersands and equals signs flying around. We’ll cover this in more detail later, but for now, we just need to know that when we see an @ in front of a variable name in any of our Car class’s methods, we are defining it as an instance variable, or an attribute that will be specific to just one instance of our Car class. Here, we will pass in a make argument and a model argument each time we create a new Car. Let’s use our trusty Toyota Corolla from earlier.

toyota_corolla = Car.new("Toyoda", "Corola")
=> #<Car:0x007f9f0f27b6d0 @make="Toyoda", @model="Corola">

Getter and Setter Methods

Now every time we call toyota_corolla, we’ll see an instance returned that kindly informs us that this particular car has a make called “Toyota” and a model called “Corolla.” Awesome! But what if we wanted to ask our new car here, “Hey, car! What model of car are you again?” It should be simple, right? We should be able to type toyota_corolla.model and have "Corolla" be returned to us. Tragically, Ruby gets upset with us and lets us know by saying something like this:

toyota_corolla.model
NoMethodError: undefined method `model' for #<Car:0x007f9f0f27b6d0 @make="Toyoda", @model="Corola">
from (irb):8
from /Users/graham/.rvm/rubies/ruby-2.3.3/bin/irb:11:in `<main>'

We get a NoMethodError because we haven’t yet created a method that allows us to ask our Car instance what kind of car it is. This will require the creation of a “getter” method, a method that allows to “get” some information about our instance. Let’s create getter methods for make and model so we can ask our car to tell us about itself.

class Car   def initialize(make, model)
@make = make
@model = model
end
def make #this allows us to "get" our make
@make
end
def model #this allows us to "get" our model
@model
end
end

In each of these two methods, we simply call upon the instance variables we created at initialization, @make and @model. Let’s try asking our car again.

toyota_corolla.model
=> "Corola"

Very good! Except for one small problem. Unfortunately, we were sold a cheap knock-off Toyoda Corola earlier. Do you think we could just tell Ruby to set the model equal to “Corolla” like we wanted in the first place? Let’s try.

toyota_corolla.model = "Corolla"NoMethodError: undefined method `model=' for #<Car:0x007f9f0f27b6d0 @make="Toyoda", @model="Corola">
Did you mean? model
from (irb):23
from /Users/graham/.rvm/rubies/ruby-2.3.3/bin/irb:11:in `<main>'

And Ruby’s mad again. We see the same NoMethodError, which seems weird, because we definitely made a model method earlier. But now, we see Ruby says undefined method `model=’. In short, the getter method we made a moment ago only allows us to get information about the car, but it doesn’t allow us to change that information. If we want to “set” our make and model to something different, we’ll need “setter” methods as well.

class Car   def initialize(make, model)
@make = make
@model = model
end
def make #this allows us to "get" our make
@make
end
def model #this allows us to "get" our model
@model
end
def make=(new_make) #this allows us to "set" our make
@make = new_make
end
def model=(new_model) #this allows us to "set" our model
@model = new_model
end
end

Each of these new methods allows to change the values of the instance variables we created earlier. Let’s try fixing that typo again.

toyota_corolla.make = "Toyota"
=> "Toyota"
toyota_corolla.model = "Corolla"
=> "Corolla"
toyota_corolla
=> #<Car:0x007f9f0f27b6d0 @make="Toyota", @model="Corolla">

Success! We can now change the attributes of our instances as we see fit.

Attribute Accessors: The Efficient (lazy?) Programmer’s Getters and Setters

So it’s great and all that we can ask our car what kind of car it is and magically transform it into a Lamborghini Huracán if we so choose, but it took way more code to accomplish than we realistically would like to write. Enter attribute accessors. These are convenient shortcuts we can use to replace our getter and setter methods. There are three types: attr_reader, which replaces our getter method, attr_writer, which replaces our setter method, and attr_accessor, which replaces both in one fell swoop. In our Car class, we are currently able to get and set our makes and models, so we can replace our methods with attr_accessor.

class Car   attr_accessor :make, :model   def initialize(make, model)
@make = make
@model = model
end
end

That sure looks a lot better. We can see here that we pass our make and model in as symbols, indicated by the colons at the beginning of each. However, as much fun as it sounds to transform our Japanese economy car into an Italian supercar, it’s not very realistic. We would like our code to model the real world as much as possible, so let’s go ahead and just use attr_reader for our make and model so we can’t change them.

class Car   attr_reader :make, :model   def initialize(make, model)
@make = make
@model = model
end
end

Instance Variables vs. Class Variables

We mentioned instance variables earlier, specifically as variables that were tied directly to a single instance of our class. For example, toyota_corolla has two instance variables, @make and @model. However, these variables could be different in another instance. If we made new instances of the car class, we could give each its own values for@make and @model.

ford_explorer = Car.new("Ford", "Explorer")
=> #<Car:0x007fa34701dd70 @make="Ford", @model="Explorer">
bmw_x3 = Car.new("BMW", "X3")
=> #<Car:0x007fa3468dcae8 @make="BMW", @model="X3">

We can also create class variables. As we might expect, these variables are specific to the whole class, not just an instance. These are denoted with two ampersands and are frequently used to keep track of all the instances we’ve created. For example, @@all = [] could be used to create an array that we then use store all of our instances. Let’s change our class to include this class variable and change our initialize method so that every new Car is placed in the @@all array.

class Car   attr_reader :make, :model
@@all = []
def initialize(make, model)
@make = make
@model = model
@@all << self #this shovels our new instance into the array
end
def hit_the_gas
"Vroom!"
end
def hit_the_brakes
"Screech!"
end
end

We will further discuss the use of self shortly.

Creating New Instance Methods

We’ve seen how to create a new class, how to create new instances of that class, how to create attributes for those instances when we create them, and how to see and change those attributes. We can also create instance methods that allow our instances to do things. Take our Car class, for example. What might a car do? It should go when we hit the gas pedal. It should stop when we hit the brakes. Let’s make a couple of methods to do this.

class Car   attr_reader :make, :model
@@all = []
def initialize(make, model)
@make = make
@model = model
@@all << self
end
def hit_the_gas
"Vroom!"
end
def hit_the_brakes
"Screech!"
end
end

Now we can tell any car we make to hit_the_gas or hit_the_brakes.

toyota_corolla.hit_the_gas
=> "Vroom!"
toyota_corolla.hit_the_brakes
=> "Screech!"

We can start getting more creative as we get more advanced. We could initialize new cars with a gas tank that depletes when we call the hit_the_gas method. We could create a refuel method that will fill our hypothetical gas tank back up. We’re only limited by our imagination!

Creating New Class Methods

All of the methods we’ve created thus far are instance methods that are called only by an instance of a class. In fact, any method we create inside of a class will be an instance method unless we specify otherwise. One way to do this is to name the class as part of our method name. Let’s see an example of this that allows us to call our @@all array from earlier.

class Car   attr_reader :make, :model
@@all = []
def initialize(make, model)
@make = make
@model = model
@@all << self
end
def hit_the_gas
"Vroom!"
end
def hit_the_brakes
"Screech!"
end
def Car.all #putting "Car" here indicates a class method
@@all
end
end

Now when we call Car.all, we’ll have our @@all array returned to us, showing us all of our car instances.

Car.all=> [#<Car:0x007f9f0f27b6d0 @make="Toyota", @model="Corolla">, #<Car:0x007fa34701dd70 @make="Ford", @model="Explorer">, #<Car:0x007fa3468dcae8 @make="BMW", @model="X3">]

However, our preferred way of indicating a class method is the use of self rather than naming the class explicitly. Let’s make that change.

def self.all
@@all
end

We saw that self earlier. But what is it and how do we use it?

Using Self

When we use self in a method, it is essentially standing as a placeholder for the instance or class that has called the method. For example, in our initialize method earlier, we sawself being shoveled into our @@all array. Since the initialize method is creating an instance, self is referring specifically to the new instance being created. When we made our class method earlier, the self being used represents the Car class. In short, any time we want to refer to the instance or class that is calling a method, we should use self rather than naming the instance or class explicitly.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade