DEV Community

Cover image for Introduction to Object-Oriented Programming with Ruby
Lorraine for Next Tech

Posted on • Originally published at next.tech

Introduction to Object-Oriented Programming with Ruby

Object-oriented programming (OOP) is a programming paradigm organized around objects. At a high level, OOP is all about being able to structure code so that its functionality can be shared throughout the application. If done properly, OOP can lead to very elegantly written programs that have minimal code duplication.

This is opposed to procedural programming (PP), in which you build programs in sequential order and call methods when you want shared behavior between pages in the application. Common procedural programming languages include C and Go.

In this tutorial, you’ll learn the fundamental concepts of OOP for Ruby, an object-oriented programming language wherein everything is an object. We will be using Ruby since one of its defining attributes — in addition to its elegant syntax and readability — is how it implements OOP techniques. This makes it a great language to start learning OOP with.

We will cover:

  • Creating classes
  • Instantiating objects
  • Initializing arguments
  • Working with inheritance, and
  • Private and public methods.

In learning these concept, we will build out our own application: an API connector that communicates dynamically with an application that sends a text message. This will include walking through how to leverage concepts such as inheritance and object instantiation to make our code more scalable and reusable.

This brief tutorial is adapted from Next Tech’s Introduction to Ruby course, which includes an in-browser sandboxed environment and auto-checked interactive tasks to complete.


Creating Classes

Before we begin, let’s define what an object is. At its core, an object is a self-contained piece of code that contains data (“attributes”) and behavior (“methods”) and can communicate with other objects. Objects of the same type are created from classes, which act as blueprints that define properties and behavior.

Creating a class in Ruby is fairly easy. To define a class, simply type the class word followed by the name of the class, and end it with the end word. Anything contained between class and end belongs to this class.

Class names in Ruby have a very specific style requirement. They need to start with a letter and if they represent multiple words, each new word needs also to be an uppercase letter — i.e. “CamelCase”.

We’ll start by creating a class called ApiConnector:

class ApiConnector
end

Classes in Ruby can store both data and methods. In many traditional OOP languages such as Java, you need to create two methods for each data element you want to be included in the class. One method, the setter, sets the value in the class. The other method, the getter, allows you to retrieve the value.

The process of creating setter and getter methods for every data attribute can be tiresome and leads to incredibly long class definitions. Thankfully Ruby has a set of tools called attribute accessors.

Let’s implement some setters and getters for some new data elements for our class. Since it’s an API connector, it would make sense to have data elements such as title, description, and url. We can add these elements with the following code:

class ApiConnector
  attr_accessor :title, :description, :url
end

When you merely create a class, it doesn't do anything — it is simply a definition. In order to work with the class, we need to create an instance of it…we’ll cover that next!

Instantiation

To understand what instantiation is, let’s consider a real-world analogy. Let’s imagine that you’re building a house. The first task is to build a blueprint for the house. This blueprint would contain attributes and features of the house, such as the dimensions for each room, how the plumbing will flow, and so on.

Is the blueprint of the house the actual house? Of course not, it simply lists out the attributes and design elements for how the home will be created. So after the blueprint is completed, the actual home can be built — or, “instantiated”.

As explained in the previous section, in OOP, a class is the blueprint for an object. It simply describes what an object will look like and how it will behave. Therefore, instantiation is the process of taking a class definition and creating an object that you can use in a program.

Let’s create a new instance of our ApiConnector class and store it in a variable called api:

api = ApiConnector.new

Now that we have an object created, we can use the api variable to work with the class attributes. For example, we can run the code:

api.url = "https://next.tech/"
p api.url
[Out:]
https://next.tech

In addition to creating attributes, you can also create methods within a class:

def test_method
  p "testing class call"
end

To access this method, we can use the same syntax that we utilized with the attribute accessors:

api.test_method

Putting this altogether, running the full class code below will result in the url and the test_method message to be printed:

class ApiConnector
  attr_accessor :title , :description , :url

  def test_method
    p  "testing class call"
  end
end

api =  ApiConnector.new

api.url = "https://next.tech/"
p api.url

api.test_method`
[Out:]
"https://next.tech"
"testing class call"

Initializer Method

One thing you may find handy in Ruby development is the ability to create an initializer method. This is simply a method called initialize that will run every time when you create an instance of your class. In this method, you can give values to your variables, call other methods, and do just about anything that you think should happen when a new instance of that class is created.

Let’s update our ApiConnector to utilize an initializer method:

class ApiConnector
  def initialize(title, description, url)
    @title = title
    @description = description
    @url = url
  end
end

Within the initialize method, we created an instance variable for each of the parameters so that we can use these variables in other parts of the application as well.

We also removed the attr_accessor method since the new initialize method will take care of this for us. If you need the ability to call the data elements outside of the class, then you would still need to have the attr_accessor call in place.

To test if the initialize method is working, let’s create another method within the class that prints these values out:

def testing_initializer
  p @title
  p @description
  p @url
end

Finally, we’ll instantiate the class and test the initialize method:

api = ApiConnector.new("My title", "My cool description", "https://next.tech")
api.testing_initializer
[Out:]
"My title"
"My cool description"
"https://next.tech"

Working with optional values

Now, what happens when we want to make one of these values optional? For example, what if we want to give a default value to the URL? To do that, we can update our initialize method with the following syntax:

def initialize(title, description, url = "https://next.tech")

Now our program will have the same output even if we don’t pass the url value while creating a new instance of the class:

api = ApiConnector.new("My title", "My cool description")

Using named arguments

Though this looks simple, passing arguments can get complex in real-world Ruby applications because some methods may take a large number of arguments. In such cases, it becomes difficult to know the order of arguments and what values to assign to them.

To avoid this confusion, you can utilize named arguments, like this:

class ApiConnector
  def initialize(title:, description:, url: "https://next.tech")
    ...
  end
  ...
end

api = ApiConnector.new(title: "My title", description: "My cool description")
api.testing_initializer 

You can enter the arguments without having to look at the order in the initialize method, and even change the order of the arguments without causing an error:

api = ApiConnector.new(description: "My cool description", title: "My title")

Overriding default values

What happens if we want to override a default value? We simply update our instantiation call like this:

api = ApiConnector.new(title: "My title", description: "My cool description", url: "https://next.xyz")

This update will override our default value of https://next.tech, and calling api.testing_initializer will now print https://next.xyz as the URL.

Inheritance

Now, we are going to learn about an important object-oriented principle called inheritance. Before going into how it is executed in Ruby, let’s see why it’s important for building applications.

To start with, inheritance means your classes can have a hierarchy. It is best used when different classes have some shared responsibilities, since it would be a poor practice to duplicate code in each class for identical or even similar behavior.

Take our ApiConnector class. Let's say we have different API classes for various platforms, but each class shares a number of common data or processes. Instead of duplicating code in each of the API connector classes, we can have one parent class with the shared data and methods. From there, we can create child classes from this parent class. With the way that inheritance works, each of the child classes will have access to the components provided from the parent class.

For example, say we have three APIs: SmsConnector, PhoneConnector, and MailerConnector. If we wrote code individually for each of these classes, it would look like this:

class SmsConnector
  def initialize(title:, description:, url: "https://next.tech")
    @title = title
    @description = description
    @url = url
  end

  def send_sms
    p "Sending SMS message with the title '#{@title}' and description '#{@description}'"
  end
end

class MailerConnector
  def initialize(title:, description:, url: "https://next.tech")
    @title = title
    @description = description
    @url = url
  end

  def send_mail
    p "Sending mail message with the title '#{@title}' and description '#{@description}'"
  end
end

class PhoneConnector
  def initialize(title:, description:, url: "https://next.tech")
    @title = title
    @description = description
    @url = url
  end

  def place_call
    p "Sending phone call with the title '#{@title}' and description '#{@description}'"
  end
end

As you can see, we are simply repeating the same code across different classes. This is considered a poor programming practice that violates the DRY (Don’t Repeat Yourself) principle of development. Instead, we can make an ApiConnector parent class, and each of the other classes can inherit the common functionality from this class:

class ApiConnector
  def initialize(title:, description:, url: "https://next.tech")
    @title = title
    @description = description
    @url = url
  end
end

class SmsConnector < ApiConnector
  def send_sms
    p "Sending SMS message with the title '#{@title}' and description '#{@description}'"
  end
end

class MailerConnector < ApiConnector
  def send_mail
    p "Sending mail message with the title '#{@title}' and description '#{@description}'"
  end
end

class PhoneConnector < ApiConnector
  def place_call
    p "Sending phone call with the title '#{@title}' and description '#{@description}'"
  end
end

By leveraging inheritance, we were able to cut all of the duplicate code throughout our classes.

The syntax for using inheritance is to define the child class name, followed by the < symbol, then the parent class name — i.e. our SmsConnector, MailerConnector, and PhoneConnector classes inherit from the ApiConnector class .

Each of these child classes now has access to the full set of elements provided in the parent ApiConnector class. For example, if we create a new instance of SmsConnector with the following parameters, we can call the send_smsmethod:

sms = SmsConnector.new(title: "Hi there!", description: "I'm an SMS message")
sms.send_sms
[Out:]
Sending SMS message with the title 'Hi there!' and description 'I'm an SMS message'.

A rule of thumb in OOP is to ensure that a class performs a single responsibility. For example, the ApiConnectorclass should not send SMS messages, make phone calls, or send emails since that would be three core responsibilities.

Private and Public Methods

Before we dive into private and public methods, let’s first go back to our original ApiConnector class and create a SmsConnector class that inherits from ApiConnector. In this class, we will create a method called send_sms that will run a script that contacts an API:

class ApiConnector
  def initialize(title:, url: 'https://next.tech')
    @title = title
    @url = url
  end
end

class SmsConnector < ApiConnector
  def send_sms
    `curl -X POST \
    -d "notification[title]=#{@title}" \
    -d "notification[url]=#{@url}" \
    "http://edutechional-smsy.herokuapp.com/notifications"`
  end
end

This SMS API was created by J. Hudgens (2017).

This method will send a title and url to an API, which will in turn send an SMS message. Now we can instantiate the SmsConnector class and call the send_sms message:

sms = SmsConnector.new(
  title: "Hey there!",
  url: "https://next.tech/xyz/introduction-to-ruby"
  )
sms.send_sms

Running this code will contact the SMS API and send the message. You can go to the bottom of this page to see your message!

Now, using this example, let’s discuss the types of methods provided by classes.

The send_sms method is a public method. This means that anyone working on our class can communicate with this method. This may not seem like a big deal if you are working on an application that no one else is working on. However, if you build an API or code library that is open sourced for others to use, it's vital that your public methods represent elements of functionality that you actually want other developers to use.

Public methods should rarely, if ever, be altered. This is because other developers may be relying on your public methods to be consistent, and a change to a public method may break components of their programs.

So, if you can’t change public methods, how can you work on a production application? That’s where private methods come in. A private method is a method that is only accessed by the class that it is contained in. It should never be called by outside services. This means that you can alter their behavior, assuming that these changes don’t have a domino effect and alter the public methods that they may be called from.

Usually private methods are placed at the end of the file after all the public methods. To designate private methods, we use the private word above the list of methods. Let’s add a private method to our ApiConnector class:

class ApiConnector
  def initialize(title:, url:)
    @title = title
    @url = url
    secret_method
  end

 private

   def secret_method
     p "A secret message from the parent class"
   end
end

api = ApiConnector.new(title: "My Title", url: "https://next.tech")

Notice how we're calling this method from the inside of the initialize method of the ApiConnector class? If we run this code, it will give the following output:

[Out:]
A secret message from the parent class

Now child classes have access to methods in the parent class, right? Well, not always. Let’s remove the secret_method method from the initialize method in ApiConnector and try to call it from our SmsConnector child class, as shown here:

class ApiConnector
  def initialize(title:, url:)
    @title = title
    @url = url
  end

 private

   def secret_method
     p "A secret message from the parent class"
   end
end

class SmsConnector < ApiConnector
  def send_sms
    `curl -X POST \
    -d "notification[title]=#{@title}" \
    -d "notification[url]=#{@url}" \
    "http://edutechional-smsy.herokuapp.com/notifications"`
  end
end

sms = SmsConnector.new(
  title: "Hey there!",
  url: "https://next.tech/xyz/introduction-to-ruby"
  )
sms.secret_method
[Out:]
Traceback (most recent call last):
main.rb:29:in `<main>': private method `secret_method' called for #SmsConnector:0x000056188cfe19b0> (NoMethodError)

This is because the SmsConnector class only has access to the public methods from the parent class. The private methods are, by their nature, private. This means that they can only be accessed by the class that they are defined in.

So a good rule of thumb is to create private methods when they should not be used outside the class and public methods when they have to be available throughout the application or used by outside services.


I hope you enjoyed this quick tutorial on the fundamental concepts of object-oriented programming in Ruby! We covered creating classes, attribute accessors, instantiation, initialization, inheritance, and private and public methods.

Ruby is a powerful object-oriented language used by popular applications, including our own here at Next Tech. With this foundational knowledge of OOP, you’re well on your way to developing your own Ruby apps!


If you’re interested in learning more about programming with Ruby, check out our Introduction to Ruby course here! In this course we cover core programming skills, such as variables, strings, loops, and conditionals, more advanced OOP topics, and error handling.

Top comments (0)