Object oriented programming (OOP) is a programming paradigm. It allows us to represent the complexities of the real, physical world in a digital way. Languages that support some level of object-orientation are ubiquitous — they include Ruby, Python, Javascript, PHP, C++, C #, Java, Rust, Kotlin, Go, and many others.

This is the first article in a two-part series that will approach the topic of OOP using Ruby to illustrate ideas and is intended for those who have an understanding of the fundamentals of procedural programming and are learning OOP.

At its core, OOP consists of classes and objects made from those classes.

Classes determine attributes and behaviors

A class is a blueprint or a template for objects. It is the concept or idea of a thing, while an object is one physical manifestation of that concept. For example, take the concept of a book. Picture one in your mind. What do you see? In other words, what makes a book a book?

Photo by ASTERISK on Unsplash

A book has attributes

  • Some potentially captivating title
  • Some author/s who wrote it
  • Some number of pages
  • Some type of paperback or hardcover binding
  • A cover that is some color
  • Some measurable weight

These are all characteristics — they describe the book. Instance variables are the attributes that an individual book has (e.g., a title of Harry Potter and the Chamber of Secrets, a page number of 432, a weight of 0.74 lbs). The total amalgamation of all instance variables an individual book has make up that particular book’s state.

Specific actions can be done with a book

  • It can be read
  • It can be picked up
  • It can be put down
  • It can be thrown
  • Pages can be ripped out
  • Notes can be written in the margins

In OOP, the actions that you can do with an object are defined in the class definition. They are called instance methods, and can be called on any object made from the class.

Taking a step back, a class dictates the possible attributes or characteristics of objects that are made from it.

Let’s focus on three key attributes — the title, author, and number of pages to create a Book class below.

class Book
def initialize(title, author, pages)
@title = title
@author = author
@pages = pages
end
end

Within our simple Book class definition is a special instance method initialize. This is Ruby’s constructor method. It is triggered every time the class method ::new is called on the Book class. Note that initialize is a private instance method, which means it cannot be called outside of a class definition. Additionally, if the initialize method is defined to take parameters, the ::new method must be passed arguments (unless the parameters defined within the class definition are given default parameters).

Instance variables compose state

You may have noticed the variables prefixed with @ . Instance variables are denoted with the @ sign (also known as the sigil). These variables are scoped at the object level — they are available to all instance methods defined within a class, and each object made from a class has its own copy of the variable that can hold its own value.

With our class defined, we can call new directly on the class, feeding it the information we want to use to create the initial state of the object.

Book.new('Triumph of the City', 'David Glaeser', 338)
Photo by Almos Bechtold on Unsplash

We have created, or instantiated one Book object — one instance of the Book class. This book has a bundle of instance variables — @title, @author, and @pages. The instance variables of this particular instance of the class are bound to the values we pass to the ::new method invocation. In other words, this Book object has a unique state that is made up of all the objects bound to its instance variables.

We can also assign this object to a local variable for safekeeping.

triumph_of_the_city = Book.new('Triumph of the City', 'David Glaeser', 338)

How can we access data stored within an object? How can we find the number of pages in our Book object? Right now — our options are limited. If you’re interested in gaining a deeper knowledge of Ruby’s meta-programming, look into Object#instance_variables and Object#instance_variable_get.

As a general rule of thumb, we cannot directly access an object’s instance variables outside of the class definition — they are private and only accessible within the scope of the object. However, we can define instance methods within the class definition and reference instance variables there. These methods can then be called on an object of that class to access and interact with that object’s instance variables.

class Book
def initialize(title, author, pages)
@title = title
@author = author
@pages = pages
end
def title
@title
end
end

Here, we’ve added an instance method title, in which we access the instance variable @title. Because this method is available to all Book objects, we can call title like so.

triumph_of_the_city.title

This returns the title of the book.

Instance methods that access instance variables are called getter methods, and those that allow a person to modify instance variables are called setter methods. Let’s write a few more getters and a setter method.

class Book
def initialize(title, author, pages)
@title = title
@author = author
@pages = pages
end
def title
@title
end
def author
@author
end
def pages
@pages
end
def pages=(new_pages)
@pages = new_pages
end
end

As you can see, we now have getter methods for title, author, and pages. We also have a setter method for pages. This one might seem strange. It’s a Ruby convention to use = at the end of the name of a setter method. It’s similar to predicate methods ending with ?.

The setter takes one argument, the new value that the user wants to assign to the instance variable. With Ruby’s syntactic sugar, calling a setter method on an object outside of the class definition is a breeze! Let’s say we rip one of the pages out of our book.

triumph_of_the_city.pages = 337

Ruby allows the act of setting an instance variable of an object to look like assignment. Without the sugar, using the setter method looks like the code below.

triumph_of_the_city.pages=(337)

The syntax may take a little while to get used to, but let’s summarize everything we’ve done so far.

Photo by Kaleidico on Unsplash
  • We defined a class. This class is the template for all objects that are instantiated from it. In the class we dictate the possible attributes and the possible behaviors that objects made from that class will have.
  • We instantiated a Book object and gave it some unique values for instance variables.
  • We defined a handful of getter methods to access the instance variables by calling a method on objects outside of the class definition.
  • We defined a setter method we can use to re-assign the instance variable pages to a new value.

Let’s create one more Book object and bind it to a local variable.

walkable_city = Book.new('Walkable City Rules', 'Jeff Speck', 290)

We now have two different instances of the Book class. They have different titles, authors, and numbers of pages. In other words, they are two different Book objects that each have a distinct state.

*We will forego talking about attr_* methods — Ruby’s short-hand option for creating getters and setters — in this article.

Instance methods give functionality

Right now, we have two Book objects. We can create a new book by passing arguments into the ::new method call on the Book class. Let’s think about the functionalities available for any Book objects.

Photo by Joanna Kosinska on Unsplash

Because we wrote getter instance methods for title, author, and pages we can access the values of each of these instance variables from outside the class. We also wrote a setter method for pages, which allows us to change the value bound to @pages in any Book object to any value we desire. We have read and write access for the pages instance variable, while we have read-only access to title and author.

With our setter method for @pages, we can alter the number of pages. In fact, a user of our program can even set the number of pages to a String object of 'hello'.

walkable_city.pages = 'hello'

Assigning a String to a variable that is intended to represent a number of pages in a book is nonsensical. Let’s put some restrictions on what a user can do with the @pages instance variable. Rather than allowing a user to set the value to anything they please, let’s only allow them to choose an Integer object.

def pages=(new_pages)
if new_pages.instance_of?(Integer)
@pages = new_pages
else
puts 'Sorry, please enter a number'
end
end

We use a simple if-else conditional that calls Object#instance_of? on the object bound to the passed in parameter new_pages. To the instance_of? call, we pass in the argument of the class name Integer. This is a predicate method that will return true or false, depending whether the class of the calling object matches the passed in argument. If new_pages is bound to anInteger object, we point the instance variable @pages to that Integer. If the user tries to pass in any other type of object, like a String or a Hash, we output an error message to the terminal and do not assign any value to @pages.

We have effectively put one protective measure in place to ensure that @pages will always point to an Integer object. This makes for a more robust program. If we have another instance method that makes use of @pages, we can rest a little more assured that we can expect an Integer and not an object from some other class. This will keep our program from breaking.

Now that we have a small, functioning Book class, a couple Book objects with their own unique instance variables and a shared set of methods (just the getters and setter), we can take a look at the main ideas of OOP and its main benefits as a programming paradigm in the second article of this series here.

Research analyst turned aspiring web developer. Learning the fundamentals with Launch School. Lives in Denver, CO.

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