Share

This is a continuation of the post Quick Ruby Tutorials -1
In the last post we created a Dog class with the ability to eat and bark. Lets continue to add more features to our class.

Each dog has a name, weight, height, color etc.
Let us add the property “weight” to the Dog class.

For this we need to
1) Store the weight of the dog. For this purpose we use an instance variable. Instance variables are explained later.
2) Add a method to access the weight
3) Add a method to set the weight.

class Dog
  def get_weight
   @weight
  end

 def set_weight(w)
   @weight = w
 end
end


dog1 = Dog.new
dog1.set_weight(4)
dog1.get_weight #4
dog2 = Dog.new
dog2.get_weight # nil
dog2.set_weight(10)
dog2.get_weight # 10
dog1.get_weight # 4 , weight of dog1 still remains the same

@weight is an instance variable.
Instance variable start with @ sign.
Instance variable can be defined in any instance method. In this case it is defined in set_weight. In get weight it is being accessed, so if get_weight is called before set_weight then it returns a nil.

Note that, changing the weight of dog2 does not effect the weight of dog1.
So there is different copy of instance variable(@weight) for every instance(object). That is why we call it a instance variable.

Let us try accessing our instance variable directly from the object.

dog = Dog.new
dog.@weight #error : syntax error
dog.weight #error: undefined method weight.

So we see that the instance variables are not accessible outside the object. That is the reason we needed to define the the instance methods get_weight and set_weight.

Let us change the names of the methods get_weight and set_weight

class Dog
 def weight
  return @weight
 end

 def weight= new_weight
  puts "in method weight="
  # @weight is an instance variable. Instance variables start with @ sign.
  @weight = weight
 end
end

Now we can do

dog = Dog.new
dog.weight #nil
dog.weight= 10 #in method weight=
dog.weight  # 10
dog.weight = 20 #in method weight=
dog.weight  # 20

Supper!!!

Note the special type of name we have given to method “weight=”.
This method is called when we do weight= or weight =(note the space between “weight” and “=”)
All these are same

dog.weight=(10)
dog.weight= 10
dog.weight = 10

Due to these type of methods and the possibility of eliminating parenthesis,
it does not look like we are accessing a method. It is as if we are accessing a variable “weight” inside the dog object.
When the name of the method is of the format method_name=, then ruby expects the method to accept only a single parameter. If you define a method with this format which accepts no parameters or accepts more than one parameters, then there would be a run time error. Try doing this

Hold to your seats tight, you may fly away after reading the next example

class Dog
 attr_accessor :weight
end

dog = Dog.new
dog.weight #nil
dog.weight = 10
dog.weight  # 10

How did this happen, with just one line of code in the Dog class??

attr_accessor is a method which generates the getter method(to get the value of the instance variable),
and setter method(to set the value of the instance variable)
:weight: this is a data type symbol in ruby. attr_accessor creates instance variable @weight when it is passed the symbol :weight

More about symbols
Symbol. A symbol is an instance of the class Symbol and is defined by prefixing a colon with an identifier eg :name, :weight, :height etc. .
symbols are data types in ruby which are identified by a unique value property ie you cannot create 2 separate objects with same values.

x = :test
y = :test
x.object_id # 79858
y.object_id# 79858
a = "test"
b = "test"
a.object_id # 22528570
b.object_id # 22520730

object id for both x and y are same. Object id is an internal id unique to the variable.(the values may be different for you)
object id for the strings are different. ie 2 different objects are created

Symbols are lighter version of their string counter parts
Symbols are much lighter on memory than strings
Most operations which can be performed on strings cannot be done on symbols.
Operations like read a part of the string, append something, delete some part of string etc
symbols with space are defined like :”ruby on rails”
symbols comparison is faster than string comparison as symbol comparison is just one comparison of hash value, but string comparison involves multiple comparison character by character(till there is no match)
When to use a symbol/ when to use a string
Whenever you want to name some thing: some property, key/value of a hash, but no string operations are required , use a symbol.

Getting back to our example,
Here attr_accessor generates for us the 2 mehtods weight and weight=.
We can pass multiple parameters to the attr_accessor method

class Dog
 attr_accessor :weight, :height, :name, :color
end

This creates the setter and getter methods for instance variables @weight, @height, @name and @color. ie it will create weigh,weight=, height and height= etc methods.
So now we can do

d = Dog.new
d.weight = 10
d.weight #10
d.height = 1
d.height  #1
d.name = "Tim"
d.name #"Tim"
d.color = "brown"
d.name # "brown"

Similar to attr_accessor there attr_reader and attr_writer methods.
attr_reader only generate the reader(weight/height).
attr_writer only generates the writer(weight=/height=).

The properties of dog such as height/weight/color etc cannot me modified externally. ie they can only be read, not written. They may get modified due some internal processes. Like the weight and height of the dog grows due to metabolic activities internally.
The instance methods we define should only expose things which are possible.
So modification of weight, height, color should not be possible directly .
Of course we should be able to feed the dog more to increase its weight :)

class Dog
 attr_reader :weight, :height, :color
 attr_accessor :name
end

d = Dog.new
d.weight = 1 # error undefined method weight= for the instance
d.height = 2 # error undefined method height= for the instance
d.color = "brown" # error undefined method color= for the instance
puts d.weight #nil
puts d.height #nil
puts d.color #nil

Not that you would need to comment out the line with errors for the program execution to complete

So it is clear from the above example that only setter/writer methods are not defined, only getter/reader methods are defined
Now our dog is born with no values set for weight, height, color. This is not correct.
Whenever a dog is born(a new object is created), the dog should have some weight, height, color etc.
Lets say when a dog is born it has a weight of 1 kg, height 0.5 feet and color “brown”.

We can use constructors for this purpose, similar to those in C++.

A constructor is a method which is called immediately after a new object is created.
In ruby the constructor method is named “initialize”.
So if you define a method with name “initialize” in your class, it will be called after a object of that class is created.

class Dog
 attr_reader :weight, :height, :color
 attr_accessor: name
 
 def initialize
  puts "in initialize" #This is used to demonstrate the flow of control
  @weight =1
  @height = 0.5
  @color = "brown"
 end
end

dog = Dog.new #in initialize
puts dog.weight # 1
puts dog.height #0.5
dog.color # brown

Let us give our dog a name

dog.name = "Tim"
puts dog.name # Tim

Note that a dog when born does not have a name. It is given to it later.
That is the reason we did not assign the name in the initialize method.

As per the above code, all the dogs born will have a weight of 1kg, height of 0.5 feet and white color.
If god would have written such a program, then there would be no diversity on this planet. That would be very boring, right?
We should have a option of selecting the weight, height and color. If we can pass these parameters to the new method that would be perfect.
That can be done making our initialize method accept some parameters.

class Dog
 attr_reader :weight, :height, :color
 attr_accessor: name

 def initialize(weight, height, color)
  puts "in initialize"
  @weight, @height, @color = weight, height, color #note the parallel assignment of variables
 end
end

dog1 = Dog.new(2,1,"white") # in initialize
puts dog1.weight # 2
puts dog1.height # 1
puts dog1.color # white

dog2 = Dog.new # error: wrong number of arguments 0 for 3

It would be great if there are some defaults for the weight, color and heigth.
ie, if the value is not passed then use a default value

class Dog
 attr_reader :weight, :height, :color
 attr_accessor: name
 
 def initialize(weight = 1,height = 0.5, color = "brown")
  puts "in initialize"
  @weight, @height, @color = weight, height, color
 end
end

d = Dog.new #in initialize
puts d.weight # 1
puts d.height # 0.5
puts d.color # white

d2 = Dog.new(4,2) #in initialize
puts d2.weight # 4
puts d.height # 2
puts d.color # brown

d3 = Dog.new
puts d2.weight # 1
puts d.height # .5
puts d.color # brown

So now God has lot more flexibility in creating new dogs :)

Add other abilities to our Dog :sleep, eat, run

class Dog
 attr_reader :weight, :height, :color
 attr_accessor: name
 
 def initialize(weight = 1,height = 0.5, color = "white")
  puts "in initialize"
   @weight, @height, @color = weight, height, color
  end

 def bark
  puts "bhow bhow!!!"
 end

 def eat(food="bone")
  puts "I am eating #{food}"
 end

 def sleep
  puts "bbye, I am going to sleep"
 end

 def shit
  puts "aah! now I am hungry again"
 end
end

dog = Dog.new
dog.bark # bhow bhow!!!
dog.eat # I am eating bone
dog.sleep # bbye, I am going to sleep
dog.shit #aah! now I am hungry again

Now our dog can eat, bark, sleep and shit.
Good enough, we will enhance our dog in the next post.

Share