Part 2: The big picture of OOP

Photo by Dan DeAlmeida on Unsplash

This is the second in a series of two articles that provide an introduction about what object oriented programming (OOP) is. In the first article, we discussed classes, objects, attributes, behaviors, instance variables, and instance methods.

We also created a Book class and instantiated a few Book objects that will be referenced in this article. You can read the first article here.

The four pillars of OOP

OOP famously has four pillars — four major concepts that justify the existence of this style of programming and act as guidelines for programmers as they code in an OOP fashion.

Abstraction

Abstraction means to simplify complexity. Take our pages= setter method. The user does not need to know that an if-else conditional is used to check their input, or that Object#instance_of? is used to do so. They can call the setter method and use its functionality. Going even deeper, when we call an instance method on an object, like Object#instance_of? on an Integer object — we don’t need to know how that method works internally. We can simply rest assured that Ruby will return true or false based on the calling object and the passed in argument.

Abstraction is the idea that defining classes and grouping behaviors and attributes within them allows users to work with objects made from those classes without having to get bogged down in implementation details that someone else has already taken the time to understand and approach.

Abstraction allows programmers to work at new levels of a problem. For example — with a Book class written and ready to go, another programmer might begin work on a Library class that interacts with Book objects in some way. The programmer working with the Library class might not know every detail of the Book class, because they don’t need to.

One metaphor that comes to mind for abstraction is the idea of a heart.

Photo by Nicola Fioravanti on Unsplash

This is an abstraction of a heart. A heart does not look like this. But it’s a simple, effective way of communicating about this complex organ that we cannot see.

Encapsulation

Encapsulation boils down to two fundamental principles — protecting data and grouping data and methods together.

Encapsulation is a form of data protection. We can expose only the data and functionalities that we want users to have. In our Book class, we allow read and write access for @pages while we only allow read access for @title and author. We determine what a user can see and do with an object. Additionally, we can even use methods ( Module#public, private, and protected ) within our classes to determine what methods we want to be available to be called on objects outside of the class definition.

Photo by Susan Holt Simpson on Unsplash

Encapsulation is also the idea of grouping data and functionalities together. In the context of our Book class, we group a set of attributes and behaviors that will be available for each object we instantiate from the Book class. There are many levels of encapsulation within OOP. Series of instructions are grouped together in methods. Bundles of data are grouped together in objects. Methods and data are grouped together in classes.

Modules, which are similar to classes with the exception that the ::new method cannot be called on them, are a sort of toolbox. They act as containers , or namespaces, for classes, constants, and methods. Modules can be included in a class to give classes access to additional functionalities.

A purse is kind of like a module.

Inheritance

Inheritance is the concept that classes can inherit behaviors and attributes from other classes and modules. It is the idea of categorizing relationships and hierarchies between classes.

It’s something that’s so natural for humans to do. For example, I’m looking at a laptop screen right now, so let’s work with that.

Let’s say that my laptop, austins_laptop, is an instance of a Laptop class, which is a type of Computer. You could say that Laptop is a subclass of the Computer class.

In other words, the Laptop class inherits from the Computer class, which might inherit from an Electronics class, which might inherit from a Machine class.

Computers, phones, headphones, and watches are all types of machines.

In Ruby, all classes inherit from classes and modules until they reach the primordial BasicObject. Inheritance signifies the order in which Ruby will search different containers for the methods that you call. If Ruby finds the method, it will stop searching and use it. If it doesn’t it will keep searching until there is nowhere else to look and return a NoMethodError.

For example, if you have an Array object and call not_a_real_method on it, Ruby will first look in Array, then Enumerable, then Object, then Kernel, then BasicObject and then return a NoMethodError. You can always find the order that Ruby will search for methods (known as the method lookup chain or method lookup path) by calling the class method ::ancestors on the class in question.

Ruby is a single inheritance language, which means that a class can only directly inherit from one superclass (while modules can be mixed into classes to provide additional functionality). Ruby allows for class inheritance and interface inheritance. Both Enumerable and Kernel are examples of modules. Enumerable is mixed in to the Array class, while Kernel is mixed in to the Object class.

Let’s take a step back and re-examine our Book class we defined in the first article.

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)
if new_pages.instance_of?(Integer)
@pages = new_pages
else
puts 'Sorry, please enter a number'
end
end
end

Let’s create a class that inherits from Book.

class Novella < Book; end

We can instantiate a Novella object the same way we instantiate a Book object. Novella inherits all the methods defined in the Book class, including the initialize method.

sad_story = Novella.new('Flowers for Algernon', 'Daniel Keyes', 23)

All the behaviors available for Book objects are now available for the object bound to sad_story, despite the emptiness of Novella. Subclasses can either override or extend the functionalities available in superclasses. Let’s extend the functionalities of our Novella class.

class Novella < Book
def binge_read
puts "I read it all in one night!"
end
end

With binge_read, we now have an available action we can take on Novella objects that we cannot use with Book objects. We have defined more specific behaviors for our subclass. Let’s define one more class and give it some unique functionality before we move on.

class Dictionary < Book 
def learn_new_word
puts ['tenable', 'crepuscular', 'petrichor'].sample
end
end

Then we can create a Dictionary object.

websters = Dictionary.new('1978 Edition', 'Webster Team', 14_542)

We now have two classes, Novella and Dictionary, that inherit from the superclass Book. Both novellas and dictionaries are types of books that have authors, page numbers, and titles. However, in this example, we cannot learn a new word from a Novella object, nor can we binge_read a dictionary object. These specific behaviors are only available for the particular subclasses for which they are relevant.

Inheritance helps programmers re-use their code when needed and extract common, repeated code between classes and move it to a superclass. Rather than re-write the Book getter and setter methods for the Novella and Dictionary classes, we can simply have the two classes inherit from the Book class.

Additionally, inheritance allows programmers to extend or override the behaviors of superclasses within subclasses.

Polymorphism

Polymorphism simply refers to the fact that methods defined in one class can be defined differently in different classes. A method binge_read can output 'I read it all in one night!' to the screen for Novella objects, and have a slightly different or even completely different implementation in another class. Let’s define a binge_read method in the Dictionary class.

class Dictionary < Book 
def learn_new_word
puts ['tenable', 'crepuscular', 'petrichor'].sample
end

def binge_read
puts "Zzzzzzzz"
end
end

Our Dictionary#binge_read method has a different implementation than our Novella#binge_read method. We can call binge_read on objects made from both of these classes, and the return values of each call will be different.

[sad_story, websters].each { |book| book.binge_read }

Methods with the same name can be defined in unrelated classes or overridden in a subclass to have a different implementation than a superclass.

The benefits of OOP

We’ve talked through some of the most basic ideas of OOP. We defined a few classes with some limited behaviors and attributes. We instantiated a few objects, each with their own unique instance variables, and we talked through some of the fundamental principles of the OOP paradigm. There is so much more, but we must save it for another time. Before we leave, let’s talk through some of the benefits of OOP.

OOP allows programmers to think in terms of concrete nouns and verbs. Representing seemingly opaque, intangible digital items, like a database or the internet, is much more accessible when you can use clear, concise terms to do so.

OOP allows programmers to abstract away solutions to complex problems. Rather than trying to hold all aspects of a problem in your head at once, you can approach chunks of the problem one step at a time. As you solve more and more problems, you can forget implementation details you used to solve one problem and focus on tackling the next, even more complex problem.

Using classes and modules to modularize code into containers makes for programs that have fewer dependencies and are therefore easier to maintain than a program made of 1,000 lines that could fail if one typo were introduced or if one local variable escaped its intended scope.

Compartmentalized classes and modules can be re-used for different programs. One module of code that was used for a program could easily be tweaked or directly re-used for another program.

Inheritance allows programmers to extract common code between multiple classes, place it in a superclass, and have subclasses inherit from the superclass. This helps achieve one of the fundamental principles of clean code — Don’t Repeat Yourself.

While organizing code into modules and classes does not result in shorter programs, it results in more clearly organized ones. Classes, methods, and constants can all be placed in well-named modules. This helps avoid name clashing errors and help programmers understand relationships between various classes and objects instantiated from those classes.

In conclusion

Thank you for taking the time to read this article. It has been my attempt to summarize the very very basic fundamentals of OOP. Numerous important topics ( attr_* methods, the self keyword, Ruby’s fake operators and syntactic sugar, commonly overridden methods like to_s, working with collaborator objects, and lexical scope, to name a few) were outside the scope of this article.

I hope that you have found this useful in some way. If you have any questions about OOP or programming in general, or any feedback or suggestions, please feel free to leave a comment. If you’re a Launch School student, please feel free to reach out to me directly in Slack.

Additionally — a huge thank you to inspiring fellow Launch School student Leena and esteemed Launch School instructor *superchilled for both reading through these articles and providing immensely valuable constructive feedback.

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