Intro To Basic OOP In Ruby Class and Objects — Part 1

Abel Gechure
8 min readNov 23, 2020
Ruby Programming Language

Learning to code in Ruby has been fun, frustrating and rewarding, that has been my experience so far. I have learnt the basics of the Ruby language and I have enjoyed its simplicity in both syntax and general understanding of concepts. Lately I have been learning Object Oriented Programming in Ruby and it was quite mind blowing to think in objects and classes as opposed to the more linear procedural programming. The best way for me to learn something new is just a gradual continual consistent interaction with the material until I feel I have a good grasp of it. It doesn’t have to feel 100% perfect but I have to feel confident and comfortable when explaining it to someone or to myself.

Classes and Objects Part 1
States and Behaviors
Classes in OOP are used to create objects. When defining a class, we focus on states and behaviors. States track attributes for individual objects. Behaviors are what objects are capable of doing.

For example, we have a class Dog. We may want to create two dog objects. One named “Fido”, and the other named “Snoopy”. They are both Dog objects but may contain different information, such as name, weight and height. We would use instance variables to track this information. This tells us that Instance variables are scoped at the object (or instance) level and this is how object keep track of their states or properties.

Even though they’re two different objects, both are still objects (or instances) of class Dog and contain Identical behaviors. For example, both Dog objects should be able to bark, run, fetch and perform other common behaviors. These behaviors are defined as instance methods in a class. Instance methods defined in a class are available to objects or instances of that class.

in summary, instance variables keep track of state, and instance methods expose behavior for objects.

Initializing a New Object

class Dog  def initialize    puts "This object was initialized!"  endendsnoopy = Dog.new        # => "This object was initialized!"

The initialize method gets called every time you create a new object. Calling the new class method leads us to the initialize instance method. In the above example, instantiating a new Dog object triggered the initialize method and resulted in the string being outputted. The initialize method can be referred to as a constructor because it gets triggered whenever we create a new object.

Instance variables

Lets create a new object and instantiate it with a property or state like a name

class Dog 
def initialize(name)
@name = name
end
end

The @name is called an instance variable. This is one of the ways we tie data to objects. The above initialize method takes in a parameter name and every new instance or object of the class Dog will be instantiated with a name property.

snoopy = Dog.new("Snoopy")

Here the string “Snoopy” is being passed from the new method through to the initialize method and is assigned to the local variable name. We then set the instance variable @name to name which results in assigning the string “Snoopy” to the @name instance variable . From the above example the name of the snoopy object is the string “Snoopy”. This state for the object is tracked in the instance variable @name . If we created another Dog object then the @name instance variable would contain a unique name. For example,

fido = Dog.new("Fido")

fido is a new object of the Dog class and it has its own unique name property “Fido”. Every Object’s state is unique, and instance variables are how we keep track.

Instance Methods

We can give our Dog class some behaviors through instance methods

class Dog 
def initialize(name)
@name = name
end
def bark
"Woof!"
end
end
snoopy = Dog.new("Snoopy")
snoopy.bark

When you run this program, nothing happens. This is because the bark method returned the string “Woof!” but we now need to print it out.

puts snoopy.bark  #=> Woof!

Now we gave our snoopy the Dog object some behavior, bark . We can share the same behavior with another object of the class Dog.

fido = Dog.new("Fido")
fido.bark #=> Woof!

All objects of the same class have the same behaviors though they contain different states; here the different state is the name.

We What if we wanted to not just say “Woof!”, But say “Snoopy says Woof!”?In our instance methods, we have access to instance variables. So we can use string interpolation like so

def bark
"#{@name} says Woof!"
end

Now we can expose information about the state of the objects using instance methods.

puts snoopy.bark  #=>  "Snoopy says Woof!"
puts fido.bark #=> "Fido says Woof!"

Accessor Methods

What if we wanted to print out only snoopy’s name?

puts snoopy.name

we would get the following error:

undefined method `name' for #<Dog:0x0000558e7fde4b90 @name="Sparky"> (NoMethodError)

A NoMethodError means that we are calling a method that doesn’t exist or is unavailable to the object. If we want to access the objects’s name, which is stored in the @name instance variable, we have to create a method that will return the name . We can call it fetch_name, and its only job is to return the value in the @name instance variable

class Dog
def initialize(name)
@name = name
end

def fetch_name
@name
end
def speak
"#{@name} says Woof!"
end
end
snoopy = Dog.new("Snoopy")
puts snoopy.speak
puts snoopy.fetch_name

This is what we get back

Snoop says Woof!
Snoopy

It worked, we now have a getter method.

What if we wanted to change snoop’s name? Thats when we reach for a setter method. It looks like a getter method but with a small difference. lets add that in our code.

class Dog
def initialize(name)
@name = name
end
def fetch_name
@name
end
def set_name=(name)
@name = name
end
def bark
"#{@name} says Woof!"
end
end
snoopy = Dog.new("Snoopy)
puts snoopy.bark
puts snoopy.fetch_name
snoopy.set_name = "Mishka"
puts snoopy.fetch_name

The output

Snoopy says Woof!
Snoopy
Mishka

We have successfully changed snoopy’s name to the string “Mishka”

The first thing to notice about the setter method set_name= this is the entire setter method and string “Mishka” is the argument being passed in to the method. Ruby recognizes that this is a setter method and allows us to use the more natural assignment syntax: snoopy.set_name = “Mishka” . When you see this code know there is a method called set_name= working behind the scenes.

Finally, as a convention, Rubyists want to name those getter and setter methods using the same name as the instance variable they are exposing and setting. Lets change our code to mirror this.

class Dog 
def initialize(name)
@name = name
end
def name
@name
end
def name=(name)
@name = name
end
def speak
"#{@name} says Woof!"
end
end

Getter and setter methods take a lot of space in our code for such a simple feature. You can imagine if we had ten more states or more to track that our code will be too long for performing a simple feature. Good thing Ruby has a built in way to automatically create these getter and setter methods for us, using the attr_accessor method. Below is a refactoring of the code above to using attr_accessor .

class Dog
attr_accessor :name
def initialize(name)
@name = name
end
def bark
"#{@name} says Woof!"
end
endsnoopy = Dog.new("Snoopy")
snoopy.bark
puts snoopy.name #=> "Snoopy"
snoopy.name = "Mishka"
puts snoopy.name #=> "Mishka"

Our Output is the same! The attr_accessor method takes a symbol as an argument, which it uses to create the method name for the getter and setter methods. That one line replaced two method definitions.

But What if we only wanted to the getter method without the setter method? Then we would want to use the attr_reader method.

It works the same way but only allows you to retrieve the instance variable and if you only want the setter method, you can use the attr_writer method. All of the attr_* methods take a symbol as parameters; If there are more states you are tracking, you can use

attr_accessor :name, :age, :height, :weight

Applications of Accessor Methods

With getter and setter methods we have a way of exposing and changing an objects state or properties. We can also use this methods from within the class as well. In the previous examples the bark method referenced the @name instance variable like below

def speak
"#{@name} says Woof!"
end

Instead of referencing the instance variables directly, we want to use the name getter method that we created earlier which is given to us by the attr_accessor method. We’ll change the speaker method to this.

def bark 
"#{name} says Woof!"
end

Now instead of using the instance variable @name we are calling the instance method name. This is best practice. Following this practice will save you some headache down the line.

We can also do the same with the setter method. Wherever we are changing the instance variables directly in our class, we should instead use the setter method.

Suppose we added two more states to track the Dog class called “height” and “weight”:

attr_accessor :name, :height, :weight

This one line of code gives us six getter/setter instance methods: name, name=, height, height=, weight, weight=. It also gives us three instance variables: @name , @height, @weight. Now suppose we want to create a new method that allows us to change several states at once, called change_info(name,height,weight). The three parametersto the method correspond to the new name, height, and weight. We could implement it like this.

def change_info(name,height,weight)
@name = name
@height = height
@weight = weight
end

lets get caught up with our Dog class

class Dog
atrr_accessor :name, :height, :weight
def initialize(name, height, weight)
@name = name
@height = height
@weight = weight
end
def bark
"#{name} says Woof!"
end
def change_info(name, height, weight)
@name = name
@height = height
@weight = weight
end
def info
"#{name} weighs #{weight} and is #{height} tall."
end
end

And we can use the change_info method like this:

snoopy = Dog.new('Snoopy','20 inches', '50 lbs') 
puts snoopy.info #=> Snoopy weighs 50 lbs and is 20 inches tall.
snoopy.change_info("Mishka", "10 inches", "30 lbs")
puts snoopy.info #=> Snoopy weighs 30 lbs and is 10 inches tall.

Just like we replaced accessing the instance variable directly with getter methods, we would also like to do the same with our setter methods. Let’s change the implementation of the change_info method to this.

def change_info(name,height,weight)
name = name
height = height
weight = weight
end

If we go ahead and use this in our code, It will not work just yet, why? The reason is because Ruby thought we were initializing local variables. recall that to initialize or create a local variable, all we do is y = 2 or str = ‘hello’. It turns out that instead of calling the name=, height=, and weight= setter methods, what we did was create three new local variables called name, height, and weight. That’s definitely what we wanted to do.

To to write this in another way, we need to use self.name= to let Ruby know that we’re calling a method. Our change code should be updated to this:

def change_info(name,height,weight)
self.name = name
self.height = height
self.weight = weight
end

This tells Ruby that we’re calling a setter method, not creating a local variable. To be consistent , we could also adopt this syntax for the getter methods as well, though it is not required.

def info
"#{self.name} weighs #{self.weight} and is #{self.height} tall."
end

Finally, If we run our code with the updated change_info method that uses the self. syntax, our code works great!

snoopy.change_info("Mishka","56 inches", "45 lbs")
puts snoopy.info # => Mishka weighs 45 lbs and is 24 inches tall.

Note, You can use the self method with any instance method.

That marks the end to this short intro to objects, classes, instance variables and methods. Next up we will dig deeper and look at class Methods, class variables,constants, the to_s method and more about self.

As a beginner in the world of programming with Ruby, I feel more confident ad the days go by that I am learning and putting into practice what I have learned. This to me is the most important thing. Learn and Keep learning.

--

--