Object Oriented Programming in Ruby

Intro

Steve Jobs explains Object Oriented Programming in 1994 (which still holdstrue today):

Jeff Goodell: Would you explain, in simple terms, exactly what object-oriented software is?

Steve Jobs: Objects are like people. They’re living, breathing things that have knowledge inside them about how to do things and have memory inside them so they can remember things. And rather than interacting with them at a very low level, you interact with them at a very high level of abstraction, like we’re doing right here.

Here’s an example: If I’m your laundry object, you can give me your dirty clothes and send me a message that says, “Can you get my clothes laundered, please.” I happen to know where the best laundry place in San Francisco is. And I speak English, and I have dollars in my pockets. So I go out and hail a taxicab and tell the driver to take me to this place in San Francisco. I go get your clothes laundered, I jump back in the cab, I get back here. I give you your clean clothes and say, “Here are your clean clothes.” You have no idea how I did that. You have no knowledge of the laundry place. Maybe you speak French, and you can’t even hail a taxi. You can’t pay for one, you don’t have dollars in your pocket. Yet I knew how to do all of that. And you didn’t have to know any of it. All that complexity was hidden inside of me, and we were able to interact at a very high level of abstraction.

That’s what objects are. They encapsulate complexity, and the interfaces to that complexity are high level.

A car is an object. You interact with it by stepping onto the gas. You don't need to know how the transmission system nor the engine works.

A human is an object. Each person has unique attributes and properties like personalities, traits and physical appearances.

Write pieces of software as if you are defining an object. Later on, when you want to use these pieces of software, you don't have to know exactly what's going on inside. You can use them and interact with them at a very high level.

Objects encapsulate complexity so you can easily interact with them at a high level.

Defining a Class

Let's use Animal as an example. We know that cats, dogs and lions can be categorized as animals. Therefore, cats, dogs and lions is each a subset of animals, and animals is the superset of cats, dogs and lions (and, of course, other animals).

What is a superset? What is a subset?

The relationship of one set being a subset of another is called inclusion or sometimes containment. For example, A is a superset of B means that A contains B and B is contained inside A.

These relationships can go on forever for each level of abstraction. For example, Shiba Inu, a special dog breed, is a subset of dogs. Dogs is a subset of animals.

In Ruby and a lot of programming languages, a set of objects is known as a class. For example, Animal is a class. Cat, Dog and Lion each is also a class.

Remember the relationships between these classes?

Cat, Dog and Lion each is a sub-class of Animal, and Animal is a super-class of Cat, Dog and Lion.

Now, how do we express these relationships in code?

# To define a class, start with the keyword `class`.
# Give the class a name; in this case, we call it `Animal`.
# The naming convention of classes in Ruby is CamelCase.
class Animal

end

# Remember, Cat itself is a class, and Cat is also a sub-class of Animal.
# To establish the relationship between the superclass and the subclass,
# use the keyword `<` followed by the name of superclass.
# In this case, it would be `< Animal`
class Cat < Animal

end

class Dog < Animal

end

class Lion < Animal

end

What's Camel Case?

Can you define a class for Shiba Inu?

class ShibaInu < Dog

end

Note that ShibaInu is a subset of Dog, instead of Animal. We call this kind of relationship inheritance. The subset will inherit from the superset. We are going to discuss in depth here later.

Instance

Great! Now, we can establish relationships between classes. We know that a class represents a category of objects.

For example, the Dog class is a category representing all the dogs, or dog objects. The Dog class represents a category, rather than an actual dog or an object. So, in programming, how do we create actual dog objects?

We can think of a class as a blueprint or mold. We can create an object based on these blueprints and molds. We call this process instantiate or instantiation.

So, when Dog is a class. Dog A, Dog B and Dog C are instances (objects) of the Dog class. The instances (Dog A, Dog B and Dog C) are created based on the blueprint defined by the class (Dog).

class Dog
end

dog_a = Dog.new
dog_b = Dog.new
dog_c = Dog.new

By using the .new method on a class, we can create an object of that class. In programming, we call these objects created from a class an instance.

Important Note: In Ruby, functions are known as methods.

Here, Dog is the class. dog_a, dog_b and dog_c are each an instance of Dog.

Class Inheritance

In each class, you can define methods or functions. For example, when an animal can eat, you can say

# define class
class Animal
  # This is known as an instance method. Why? Because we are saying that every animal (every instance of this class) can "eat".
  def eat
    puts "I am eating"
  end
end

# Instantiate: creating a new instance of Animal
new_animal = Animal.new

# The new instance can access the "eat" method
new_animal.eat
=> "I am eating"

An instance method is a method for every instance of a class. When we say animals can eat, we meant every animal has the ability to eat. In other words, we meant every instance of the Animal class can eat.

Great! Now, like an animal, a dog can also eat... So, let's turn it into code too

class Dog
  def eat
    puts "I am eating"
  end
end

Great! But, like a dog, a cat can also eat... So, do we need constantly define the eat method for each class? Does it seem very redundant?

Indeed, it is very redundant. Since an animal can eat and a dog is just a subclass of animal, we don't need to redefine the eat method every single time. Instead, we can inherit it.

class Animal
  def eat
    puts "I am eating"
  end
end

# Dog inherits Animal
class Dog < Animal
end

new_dog = Dog.new
new_dog.eat # will this work? try it

Note that eat was NOT defined inside the Dog class; and yet, new_dog can use eat method because it inherited such method from the Animal class. This is class inheritance.

Similarly, you can say

class Animal
  def eat
    puts "I am eating"
  end
end

class Dog < Animal
end

# ShibaInu inherits Dog
class ShibaInu < Dog
end

new_shiba = ShibaInu.new
new_shiba.eat # will this work? try it

Shiba Inu

This is a Shiba Inu. And, yes, new_shiba.eat should also work.

The following generalizes what happens when one class inherits another:

class ParentClass
  def a_method
    puts 'b'
  end
end

class SomeClass < ParentClass
  def another_method
    puts 'a'
  end
end

father = ParentClass.new
father.a_method
# will print "b"

child = SomeClass.new
child.another_method
# will print "a"
child.a_method
# will print "b"

Instance Attributes (@)

We have seen that we can create and inherit methods / functions of a class, like a dog can eat. On the other hand, a dog also has an age, weight and other attributes.

Attributes are different from methods because their primary purpose is to store values. So, we need be able to both read and write attributes to an instance. In Ruby, we need both the read and write method for each attribute. For example,

class Dog
  def age
    # return the value of the instance variable @age
    @age
  end

  def age=(val)
    # assign the value of `val` to the instance variable @age
    @age = val
  end
end

new_dog = Dog.new # instantiate
new_dog.age       # check new_dog's age; we expect nil because we haven't set the age yet
# => nil
new_dog.age = 7   # set dog's age
new_dog.age       # check dog's age again
# => 7

Remember, an @ in front of a variable means that the variable is an instance variable. An instance variable is accessible within an instance.

Doesn't it look tedious to write 2 methods (read and write) for every attribute you want to add? If I want attributes like age, breed_type, height and weight, I would need to make 8 methods just to read and write these attributes.

Inventors of Ruby saw this problem and came up with a simple solution – introducing attribute reader attr_reader and attribute writer attr_writer.

So now, you can do the following instead.

class Dog
  attr_reader :age
  attr_writer :age

  # This has been replaced by attr_reader
  # def age
  #   @age
  # end

  # This has been replaced by attr_writer
  # def age=(val)
  #   @age = val
  # end
end

new_dog = Dog.new
new_dog.age = 6
new_dog.age
# => 6

:age is a symbol. You can also use a string, but strings will be converted to symbols automatically here.

We can even go further to combine attr_reader and attr_writer and replace them with attr_accessor to achieve read and write attributes.

class Dog
  attr_accessor :age

  # This has been replaced by attr_accessor
  # attr_reader :age
  # attr_writer :age
end

new_dog = Dog.new
new_dog.age = 8
new_dog.age
# => 8

So, in the future, when you want to enable attributes with read and write abilities, you can use attr_accessor to define those attributes.

Now, let's look at the following code.

class Dog
  attr_accessor :age, :weight
end

new_dog = Dog.new
new_dog.age = 8
new_dog.weight = 30

Here, we instantiate a new dog, and set the age as 8 and weight as 30.

Can we combine them, so that I can set the age and weight when I am instantiating? Indeed, yes! We can do something like Dog.new(8, 30)

Ruby classes run a default function called initialize during every instantiation.

To enable it, do the following:

class Dog
  attr_accessor :age, :weight

  def initialize(age, weight)
    @age = age
    @weight = weight
  end
end

new_dog = Dog.new(8, 30)
new_dog.age
# => 8
new_dog.weight
# => 30

The parameters of initialize define the same parameters accepted by Dog.new. Inside the initialize, we have set the instance variables @age and @weight to the value given by the parameter age and weight.

Default Attribute value

You can also set a default attribute value in case no input is given. Here we will default age to be 0 if nothing is given at initialization.

class Dog
  attr_accessor :age, :weight

  def initialize(age=0, weight=0)
    @age = age
    @weight = weight
  end
end

new_dog = Dog.new
new_dog.age
# => 0
new_dog.weight
# => 0

results matching ""

    No results matching ""