How to Break Down a Class-ic Program
The ins and outs of your first class in Ruby
This is the second article in a four-part series on the basics of object-oriented programming in Ruby. Read the previous article here.
Now that we’ve played at the foundation of object-oriented programming, we can sit at the big kids’ table and define our first class step by step. Wait a second… Object-oriented programming… Object-oriented… Object… Obj… What’s an object?
The ABCs of OOP
Classes and Objects
In Ruby, everything from
123 is an object. An object is an encapsulation of state and behaviours. A class is like a blueprint for an object’s state and behaviours, what it’s made of and what it can do. If a class is a blueprint, an object is a house. Let’s define our first class!
We can define a class
Dog with the reserved word
Then, we can call the
new class method on
Dog to create an instance of the class and assign the instance to a local variable like
fluffy. In other words, we can instantiate an object
fluffy from the class
When we instantiate an object, it triggers a constructor method. The constructor method in Ruby is defined by the reserved word
As soon as we instantiate a
initialize method is called and outputs the string
'This object is initialized!'.
Instance Variables and Methods
The purpose of a constructor method, though, is to initialize the state of an object. Let’s change the
initialize method to do just that.
Once again, we instantiate a
fluffy. But this time, we pass a string
'Fluffy' from the
new method to the
initialize method and assign it to a local variable
name. Inside the method, the
@name variable with the
@ symbol in front of it is an instance variable. Instance variables tie data to our objects and track information about their state.
We assign the local variable
name to the instance variable
@name. That means the instance variable
@name points to the string
'Fluffy' and the string
'Fluffy' is a part of our object’s state.
We’ll instantiate another
Dog object called
buddy and name it
buddy are both objects of the
Dog class. Still, they have a separate copy of the
@name instance variable because instance variables are scoped at the object level. An object’s state is unique to the object, and instance variables track information about individual state.
Although objects of the same class don’t share state, they share behaviours. We can imagine both
buddy are able to bark.
This behaviour is defined by an instance method. Instance methods can be called by every object of a class. If we call the
bark instance method on
fluffy, the string
'Woof!' is output.
And if we call it on
buddy, the same string is output!
Since instance variables are scoped at the object level, they’re accessible in any of the object’s instance methods. This means we can use instance methods to expose information about an object’s state, even if the state hasn’t been initialized inside or passed to a particular instance method.
Let’s use the
bark method to expose the
@name instance variable and add a personal touch.
We’ll call it on both
Dog objects again.
Accessor methods are another way to expose information about an object’s state. If we want to retrieve a
Dog object’s name, we can create a method that’ll return its instance variable
@name. This is called a getter method. By convention, we’ll name the method after the instance variable.
name method returns a reference to the instance variable
@name. If we mutate the reference, we mutate the instance variable.
This likely isn’t our intention! But if it is, we can create a setter method to reassign the instance variable
@name rather than mutate it. By convention, we’ll name the method after the instance variable appended by a
On line 21, we should expect to use the
name= method like this:
fluffy.name=(fluffy.name.reverse). But in Ruby, we can use a more natural assignment syntax if we name our setter methods conventionally.
It’s particularly useful to create getter and setter methods when we want to manipulate an instance variable before we return it. Say we'd like
fluffy's name to return in capital letters whenever we retrieve it.
However, the methods take up a lot of space if we only want to read or write over the instance variable
@name. And that’s only one instance variable! Ruby gives us a way to automatically generate them instead.
attr_accessor method takes a symbol as an argument, creates an instance variable of the same name, and gives us read and write access to it. If we only want read access, we can use the
attr_reader method. If we only want write access, we can use the
In this case, we have a
name getter method, a
name= setter method, and a
@name instance variable that’s initialized to a string when our
initialize method runs. (Otherwise, an uninitialized instance variable points to
We can call our methods from inside of the class definition. Instead of referencing the
@name instance variable directly in our
bark method, we’ll call the
name getter method to retrieve it.
By removing the
@ symbol, we refer to the
name getter method rather than the
@name instance variable. Recall when we wanted to reformat
fluffy's name each time we retrieved it. That would be reflected in
bark now. And if we wanted to reformat it again, we’d only have to make changes in one place.
Similarly, we can call the
name= setter method to retrieve and reassign the
@name instance variable. But first, we’ll add an instance variable to track the weight of our
Then, we’ll create a
change_info method to reassign the
@weight instance variables at once.
We’ll call it on
It works! But instead of referencing the instance variables directly in the
change_info method, let’s call our setter methods.
And we’ll call the
change_info method on
Uh-oh! The method doesn’t work anymore!
This is because inside of it, we initialize
weight local variables instead of calling the
weight= setter methods. To disambiguate, we have to be explicit about what we reference. One way is to prepend our setter methods with
Now it’s clear we want to call the setter methods of an object since
self refers to the calling object when it’s inside of an instance method. We can prepend any getter and instance methods with
self inside of an instance method, but it’s not required.
If we call the
change_info method on
self refers to
fluffy inside of
self.name= is the same as
So far, we’ve only handled public methods. Public methods comprise a class’s public interface: how we control access to information, like the state of objects, and interact with the class and its objects outside of the class definition. By default, all methods are public.
On the other hand, we can define private methods in order to limit access to information. We’ll privatize the accessor methods for
@weight by moving them under the access modifier
Earlier, we were able to call the
weight getter methods anywhere in the program. Now that they’re private, if we call them outside of the class they’re defined, a
NoMethodError is raised.
But what happens if we call
bark outside of the class it’s defined? Although it’s a public method, it calls the private
name getter method.
Success! While we call
bark outside of the class it’s defined, it calls
name inside of the class definition. Private methods can be accessed by methods of the same class, so we can use public methods to implicitly call them outside of the class definition. But, even inside, we can’t explicitly call them.
Just like we can’t call
fluffy, we can’t call it on
self explicitly refers to the calling object. This means we can’t call a private method on another object of the same class either, so data is not only encapsulated from outside of a class definition but other objects inside.
But recall if we don’t prepend a setter method with
self, we initialize a local variable instead. What happens if the setter method is private?
Let’s call the public
change_info method, which calls the private
weight= setter methods on
bark method is the only way we can access
@name outside of the class definition, so we’ll call it before and after we call
change_info. And we’ll update it so we can access
Unlike when we explicitly call the
name getter method in
bark, no error is raised. We’re able to change
fluffy's name and weight. So, even when a setter method is private, we have to explicitly call it to disambiguate from initializing a local variable. This is the only exception!
While private methods encapsulate data from outside of a class definition and other objects of the class, sometimes we only want to encapsulate it from the former. To make it available otherwise, we can define protected methods with the access modifier
Protected methods are like private methods outside of the class they’re defined, but they’re like public methods inside.
That means we can only call
a_protected_method from inside of
Test, but we can explicitly call it from
Protected methods allow us to hide sensitive data so it can’t be manipulated by the user, while sharing it between objects of the same class. Say we don’t want a stranger to know our dog’s name unless it befriends their dog. And even if it does, we don’t want the stranger to whistle it over or give it a silly nickname.
If we call the
new_friend method on
fluffy and provide another
Dog object as an argument, we can explicitly call the
name getter method on both since
name is protected and available across objects of the
Dog class now.
And we still can’t call it outside of the
Dog class definition!
A less contrived example may be to compare the dogs’ weights, but in either case, we can start to see how protected methods are useful for handling and comparing sensitive data between objects.
Class Variables and Methods
Outside of an instance method,
self refers to the class itself. When we prepend a method definition name with
self, we define a class method.
Class methods are methods we can call directly on a class without having to instantiate an object. They hold behaviours that don’t concern individual objects of the class or their state. Consequently, they can’t access instance variables or methods.
We’ll create a new
Dog class with a class method.
In our class definition,
self refers to
self.total_number_of_dogs is the same as
@@number_of_dogs variable with two
@ symbols in front of it is a class variable. Class variables track information related to a class as a whole rather than about specific objects.
We initialize the class variable
@@number_of_dogs to 0. When we instantiate a new
Dog object, the
initialize method runs and increments the class variable
@@number_of_dogs by 1. If we want to retrieve
@@number_of_dogs, we can call the class method
self.total_number_of_dogs on the
Let’s try it out.
We see, then, class variables are scoped at the class level. They can be initialized anywhere (but should be defined outside of instance methods), and they’re accessible in both class methods and instance methods.
Since class variables pertain to a class as a whole, all objects of a class share one copy of a class variable. When we increment
@@number_of_dogs, its new value is reflected across the
Dog class and each of its objects.
Now We Know Our ABCs…
Let’s connect what we have to the core concepts we discussed in the previous article. First, we’ll combine our two
Dog classes into one.
We won’t worry about inheritance for now, but consider how a simple class like this demonstrates abstraction, encapsulation, and polymorphism. The following considerations are by no means comprehensive!
The last time we ran our code, we called the
self.total_number_of_dogs method on the
Dog class to find out how many objects of the class there were. We can use it without having to remember a
@@number_of_dogs class variable is incremented each time we instantiate a
Dog object, or even understand what class variables are and how they’re scoped.
It’s the same with the
As long as we remember how to use it, we can easily change
fluffy's name and weight without having to remember it’s defined or implemented.
That said, the method definition looks fairly simple. But do we reassign the
@weight instance variables of our object? If so, is it directly or through setter methods? Shouldn’t the syntax for a method call be more like
self.name=(name)? And what’s
Hopefully we can answer these questions now, even if we hadn’t implemented the
change_info method. The point is there’s always more complexity than meets the eye. We can pick any “simple” part of our code and dig ourselves lower and lower and lower into its levels of abstraction.
Inside of our
change_info method, we use
self to explicitly call
weight=, methods encapsulated by the calling object. The methods reassign
@weight, state that is encapsulated in instance variables by the same object.
We worked with access modifiers, which encapsulate methods and state in varied ways. We can infer state is always private, and we control access to it by providing a public interface to interact with it through methods like
Our public interface is polymorphic. We’ve only passed in strings for our objects’ names and weights, but they can be represented as any data type. If we used a symbol for an object’s name, or say an integer if we had a robo-dog, we don’t have to change how any state or behaviours are implemented. (We’ll tackle why that is in the next article.)
Thank U, Next
In the next article, we’ll consolidate our understanding and write a class-ic program by inheritance: