Intro To Basic OOP In Ruby Class and Objects — Part 1
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
endsnoopy = 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
endsnoopy = 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
endsnoopy = 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!"
endend
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!"
endendsnoopy = 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.