The Startup
Published in

The Startup

OOP 101

How to Break Down a Class-ic Program

The ins and outs of your first class in Ruby

Photo by Colony Labs

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 'abc' to 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 class.

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

When we instantiate an object, it triggers a constructor method. The constructor method in Ruby is defined by the reserved word initialize.

As soon as we instantiate a Dog object fluffy, the 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 Dog object 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'.

Now fluffy and 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 fluffy and 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.

Paw-fect! (Sorry.)

Accessor Methods

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.

Our 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 = symbol.

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.

The 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 attr_writer method.

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

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

Then, we’ll create a change_info method to reassign the @name and @weight instance variables at once.

We’ll call it on fluffy.

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

Uh-oh! The method doesn’t work anymore!

This is because inside of it, we initialize name and weight local variables instead of calling the name= and weight= setter methods. To disambiguate, we have to be explicit about what we reference. One way is to prepend our setter methods with self.

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 fluffy, self refers to fluffy inside of change_info and self.name= is the same as fluffy.name=.

Access Modifiers

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 @name and @weight by moving them under the access modifier private.

Earlier, we were able to call the name and 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 name on fluffy, we can’t call it on self since 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 name= and weight= setter methods on self. The 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 @weight too.

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.

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

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 Dog and self.total_number_of_dogs is the same as Dog.total_number_of_dogs. The @@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 Dog class.

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!

1) Abstraction

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

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 @name and @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 self anyway?

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.

2) Encapsulation

Inside of our change_info method, we use self to explicitly call name= and weight=, methods encapsulated by the calling object. The methods reassign @name and @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 name= and weight=.

3) Polymorphism

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:

Unlisted

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Rebecca Nguyen

Rebecca Nguyen

Software engineer @ GitHub. Co-creator @ Haven. Passionately curious. I like to read books, make ice cream, and learn things. Chinese-Vietnamese. She/her.