CodeNewbie Community

Julianna Tetreault
Julianna Tetreault

Posted on with Ridhwana Khan

Getting Oriented with Objects in Ruby

This post is was originally published on dev.to and is co-authored by @juliannatetreault and @ridhwana_k.

In our last post we briefly touched upon a few topics such as: object orientation, dynamic languages and memory management. However, we barely scratched the surface of these concepts, and so in this post, we’d like to dive deeper into object orientation and how it all comes together in Ruby.

Ruby Objects

As we mentioned in our previous post, Ruby is centered around the concept of “objects”. Everything in Ruby is an object - this includes Booleans, Strings, Integers and more.

A Ruby Object will contain its own set of attributes, which you can think of as data, and its own set of methods, which you can think of as the behavior.

Ruby Objects communicate by sending messages to each other. These messages can be thought of as instructions. When we talk about “sending a message from Object A to Object B”, what we actually mean is that we “call or invoke a method on Object B”. These terms can be used interchangeably.

If you take a second to look up from reading this article and glance around your surroundings, you’ll see that there are objects everywhere in the world.

As I look up at this moment, I see - my 😼, a 🪴, a 📘, a 🖨 and a 🛋 are some of the objects that jump out to me. All of these have the potential to be modeled as Objects each with its own data and behavior, and sometimes these objects send messages to each other, like when my cat chews on a leaf from my plant.

Some of the concepts that we’ll need to discuss first before fully understanding Ruby objects are classes, attributes and methods.

What is a Class?

A Class is a blueprint for the construction of similar objects - you can think of it as a template that we use to create objects. The template provides the set of attributes and methods for an object, which will then help each object to define its own set of data and shared behavior.

For example, we may want to represent my cat. In order to do this, we’d first create a blueprint that we could use to represent all cats.

Below we represent the Class-y Cat, haha get it?

The Cat Class

This blueprint will contain the attributes that a cat may have like name, age, color and weight. It will also contain behavior like the sound that a cat will make or how it will move its tail. In the future, we may want our blueprint to be more general, so we may want to make an Animal blueprint and use it to create both a cat and a dog object - but that’s for later.

In reality, the code for the Cat Class may look something like this:

class Cat 
  def initialize(name, age, color, weight)
    @name = name
    @age = age
    @color = color
    @weight = weight
  end

  def make_sound()
    return "meow"
  end

  def move_tail()
   return "twitching"
  end 
end
Enter fullscreen mode Exit fullscreen mode

In order to describe my cat, we would use this template to instantiate a new object. In Ruby the new method is used for instantiating an object. This now creates my object as an instance of the Cat Class.

zeus = Cat.new("Zeus", 2, "brown", 5)  
Enter fullscreen mode Exit fullscreen mode

Now this may not make sense to you as yet, but let’s break down the code above by describing the attributes and methods.

Attributes (Data)

name, age, color and weight in the parenthesis are referred to as parameters. They describe the characteristics of the object.

When we instantiate an object, like in the code below, we pass through the values for these parameters in the form of arguments. The arguments are what allow us to provide specific values for that object.

Cat Class with initialize method and object

When we call the method new on a class, the class will create a new instance of itself. Internally, it then calls the method initialize on the new object, and it will pass all the arguments to the method initialize.

The initialize method as outlined in the image above is then responsible for preparing the instance data for the object. @name, @age, @color and @weight are referred to as instance variables - they can be identified by the @ prefix. Instance variable data is unique to the object.

Instance variables by default cannot be accessed from outside the object:

zeus = Cat.new(Zeus, 2, brown, 5)  
zeus.age
=> NoMethodError: undefined method `age` 
Enter fullscreen mode Exit fullscreen mode

OMG Cat
But what if we really need to print out Zeus’s age from outside?

Well, there are definitely times when we may want to access (read and write) these instance variables from the outside. In these cases we can access the attributes with attribute accessors.

There are three attribute accessors that we will discuss:

attribute_reader

In order to be able to read an attribute from the outside without the need to change it then you can use an attribute_reader.

class Cat
  attribute_reader :age

  // rest of the code
end
Enter fullscreen mode Exit fullscreen mode

When you add attribute_reader to your code, Ruby then creates a method on your behalf that looks like the code below, and allows you to read the attribute. The symbol for the attribute_reader is used as the name for this getter_method as defined below.

class Cat
  def age
    @age
  end
  // rest of the code
end

zeus = Cat.new(Zeus, 2, brown, 5)  
zeus.age
=> 2
Enter fullscreen mode Exit fullscreen mode

You’ll learn more about methods in the next section.

attribute_writer

Likewise, in order to be able to write to an attribute from the outside then you can use an attribute_writer. Just like with attribute_reader Ruby then creates a method on your behalf that looks like the code below, and allows you to write to the attribute.

class Cat
  def age=(value)
    @age = value
  end
end

zeus = Cat.new(Zeus, 2, brown, 5)  
Zeus.age = 4
Enter fullscreen mode Exit fullscreen mode

attribute_accessor

In order to be able to both read and write to an attribute, we would use attr_accesor. Ruby then creates both methods on your behalf.

Now that we’ve discussed attributes, let’s talk about methods.

Behavior (Methods)

Methods allow you to bundle code that can perform tasks and return results in a named, DRY (don’t repeat yourself!) way. Methods can be called from other code, allowing programmers to use the bundled code without having to rewrite it in every place it’s needed.

When it comes to crafting a method, there are a few things you should know about them:

First, methods are defined using the def and end keywords. def, followed by a name, represents the start of a method. The end keyword does just that – it ends the method. What comes in between the def and end keywords is the bundled code or logic.

def method_name
   // insert some behavior here 
end
Enter fullscreen mode Exit fullscreen mode

Next, methods have their own set of naming conventions. When creating a method, it is important to keep these naming conventions in mind, as it’ll make it easier to understand the intent of the method for the next programmer (or you, 6 months down the road!). The naming conventions we’d like to draw your attention to are: naming your method, the capitalization of your method, and what to do when your method name is longer than a single word.

Let’s begin with naming your method. When brainstorming method name ideas, it is important to ensure that the method is aptly named – the method’s name should be explanatory, giving insight into the method’s purpose. If you’re creating a method that will give your Cat Object the ability to meow, then naming the method meow is a good place to start. Now let’s touch upon method capitalization, or the lack thereof. Method names should consist of all lowercase letters. Finally, let’s discuss what to do when your method name is longer than a single word. Multi-word method names in Ruby are written in snake case. For example, if you had a method that both made your Cat Object meow and move its tail, you might write it as meow_and_move_tail, where the underscores between each word represent snake casing.

def meow_and_move_tail
   // insert some behavior here 
end
Enter fullscreen mode Exit fullscreen mode

Finally, the logic within a method is always indented. The rule of thumb when it comes to indentation is four spaces or a tab to the right. While there is an ongoing feud within the programming community about which approach is right (hint: neither are!), it all boils down to preference.

Tabs or spaces - which side are you on?

On the topic of crafting methods, one thing that is important to mention is that methods can accept default arguments and values, which can level up your method and make it more reusable. A default argument allows a method to be called with or without the provided argument, allowing for flexibility. It also gives you the option to reassign the value passed to the default argument, making the method more extensible. To pass a method a default argument with an assigned value, open a set of parentheses after the method name and define the argument and its value like so:

def make_sound(sound = Meow)
  // insert some behavior here 
end
Enter fullscreen mode Exit fullscreen mode

In the above make_sound method, we pass a default argument sound, with the value meow. By passing this default argument, if we were to lean on the default argument in the method’s logic, we could expect that the string “Meow” would be returned whenever the method is called.

def make_sound(sound = Meow)
  puts #{sound}!”
end
Enter fullscreen mode Exit fullscreen mode

Now you may be wondering what those squiggly brackets and hashtag are doing within the make_sound method–the use of this is called string interpolation, which can briefly be explained as embedding the value within the string that it is a part of. This is something we may touch upon in a later post, so stay tuned, folks!

Similar to default arguments, methods can also accept required parameters. While default arguments and values are optional, required parameters are just that - required. Just like default arguments, required parameters are defined within a set of parentheses that come right after a method’s name. Unlike default arguments though, a required parameter is not given a default value, rather its value is defined when the method is invoked.

def make_sound(sound)
  puts #{sound}!”
end

make_sound(Purr)
Enter fullscreen mode Exit fullscreen mode

So, method naming conventions, default arguments, and string interpolation aside, let’s briefly touch upon CRUD actions, or the widely used methods standard to Ruby. CRUD, which stands for create, read, update, and delete, are a set of methods provided in Ruby that perform basic actions such as creating, reading, updating, or deleting an Object. These methods are important to recognize due to their wide use.

Now, with all of this new-found knowledge of methods, you may be wondering how to actually make use of this behavior. If so, then this paragraph is for you! We refer to making use of a method’s behavior as “invoking” or “calling” a method in Ruby. This can be accomplished using dot notation, which when used, invokes a method, passing along a message to an Object, which in our case, is our Cat, Zeus.

Describing Objects and Methods

Is that really all there is to methods?! Seems pretty straightforward to me…

The above is just the beginning and while there is so much more to learn about methods, knowing the above should get you to a good baseline understanding of how Objects are given behavior and receive messages through methods. If you’re keen to dive a bit deeper though, we’ll be releasing a brief follow up post that will touch upon some of the topics that didn’t make it into this post.

And that’s it folks…

Meow

Credits to @ridhwana_k for all of the help with the drawings.

Discussion (0)