5. Instance Methods, Class Methods, and Encapsulation

Let's go back to the Cat class that we wrote.

class Cat
  attr_accessor :name, :age

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

As we discussed before, cats can do these things:

And as we talked about before, the things that objects can do are methods in object oriented programming. What we are going to do is create instance methods for each of these actions.

Instance Methods

Instance methods are methods that can only be called on an instance of a class. In other words, the object has to first be created in order to execute the method:

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

  def meow
    puts "Meow"
  end
end

cat = Cat.new("Cathy")

# This works
cat.meow

# This will give us an error, since we are not calling it on an instance of a class
Cat.meow
=> NoMethodError: undefined method `meow' for Cat:Class

Let's create instance methods for these four actions in our Cat class. Edit your Cat class to look like this:

class Cat
  attr_accessor :name, :age

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

  def walk_forward
    puts "Meow! I'm walking forward!"
  end

  def run
    puts "Meow! I'm running!"
  end

  def jump
    puts "Meow! I'm jumping!"
  end

  def eat
    puts "Meow! This stuff is yummy."
  end
end

cat = Cat.new("Beth", 6)

cat.walk_forward
cat.run
cat.jump
cat.eat

Save the file and run the program:

ruby cat.rb

You can see that the instance methods executed correctly. Let's also add another method called say_introduction that prints out the cat's name and age. Change your Cat class to look like below:

class Cat
  attr_accessor :name, :age

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

  def walk_forward
    puts "Meow! I'm walking forward!"
  end

  def run
    puts "Meow! I'm running!"
  end

  def jump
    puts "Meow! I'm jumping!"
  end

  def eat
    puts "Meow! This stuff is yummy."
  end

  def say_introduction
    puts "Meow! My name is #{@name} and I'm #{@age}!"
  end
end

cat = Cat.new("Beth", 6)

cat.walk_forward
cat.run
cat.jump
cat.eat

cat.say_introduction

If the program ran correctly, you should see the following output:

Meow! I'm walking forward!
Meow! I'm running!
Meow! I'm jumping!
Meow! This stuff is yummy!
Meow! My name is Beth and I'm 6!

As you can see, you can use instance variables inside instance methods. In the example above, we are calling @name and @age inside say_introduction.

Class Methods and Class Variables

Let's say we want to count the number of Cat objects we have. In this case, we will use class methods and class variables.

Let's take a look at an example. In our Cat class, let's add a class variable called @@count. This variable will hold the number of Cat objects that exist. We will also increment @@count by 1 in the initialize method, so that every time a Cat object is created, the @@count is updated.

class Cat
  attr_accessor :name, :age
  @@count = 0

  def initialize(name, age)
    @name = name
    @age = age
    @@count += 1
  end

  def walk_forward
    puts "Meow! I'm walking forward!"
  end

  def run
    puts "Meow! I'm running!"
  end

  def jump
    puts "Meow! I'm jumping!"
  end

  def eat
    puts "Meow! This stuff is yummy."
  end

  def say_introduction
    puts "Meow! My name is #{@name} and I'm #{@age}!"
  end
end

Next, let's add a class method called count. This method will output the number of Cat objects that exist.

class Cat
  attr_accessor :name, :age
  @@count = 0

  def initialize(name, age)
    @name = name
    @age = age
    @@count += 1
  end

  def walk_forward
    puts "Meow! I'm walking forward!"
  end

  def run
    puts "Meow! I'm running!"
  end

  def jump
    puts "Meow! I'm jumping!"
  end

  def eat
    puts "Meow! This stuff is yummy."
  end

  def say_introduction
    puts "Meow! My name is #{@name} and I'm #{@age}!"
  end

  def self.count
    puts "Number of cats: #{@@count}"
  end
end

Let's now call the count method. When we call a class method, we have to call it on the class, and not on an instance of a class.

For example this works:

Cat.count
=> Number of cats: 1

The example below won't work, since class methods don't work on instances:

cathy = Cat.new("Cathy", 6)

cathy.count
=> undefined method `count' for #<Cat:0x0000000138c330 @name="Cathy" @age=6> (NoMethodError)

In our cat.rb file, let's create two cats and see if the count is correctly updated. Update your file to look like this:

class Cat
  attr_accessor :name, :age
  @@count = 0

  def initialize(name, age)
    @name = name
    @age = age
    @@count += 1
  end

  def walk_forward
    puts "Meow! I'm walking forward!"
  end

  def run
    puts "Meow! I'm running!"
  end

  def jump
    puts "Meow! I'm jumping!"
  end

  def eat
    puts "Meow! This stuff is yummy."
  end

  def say_introduction
    puts "Meow! My name is #{@name} and I'm #{@age}!"
  end

  def self.count
    puts "Number of cats: #{@@count}"
  end
end

cathy = Cat.new("Cathy", 6)
beth = Cat.new("Beth", 5)

Cat.count

If you coded it correctly, you see this output:

Number of cats: 2


Let's add a new instance method called say_human_age that will output the cat's age in human years!

This is the algorithm to convert a cat's age to human years (probably not 100% accurate):

Let's put this into code and print out the human age in our say_human_age method:

def say_human_age
  if @age == 1
    human_years = 15
  elsif @age == 2
    # add 15 + 9
    human_years = 24
  else
    # add the first 2 years plus the age subtracted by the first two years, multiplied by 4
    human_years = 24 + (@age - 2) * 4
  end

  puts "I'm #{human_years} in human years!"
end

Let's now add this to our Cat class, if you haven't done so already. Make your Cat class look like this:

class Cat
  attr_accessor :name, :age
  @@count = 0

  def initialize(name, age)
    @name = name
    @age = age
    @@count += 1
  end

  def walk_forward
    puts "Meow! I'm walking forward!"
  end

  def run
    puts "Meow! I'm running!"
  end

  def jump
    puts "Meow! I'm jumping!"
  end

  def eat
    puts "Meow! This stuff is yummy."
  end

  def say_introduction
    puts "Meow! My name is #{@name} and I'm #{@age}!"
  end

  def self.count
    puts "Number of cats: #{@@count}"
  end

  def say_human_age
    if @age == 1
      human_years = 15
    elsif @age == 2
      # add 15 + 9
      human_years = 24
    else
      # add the first 2 years plus the age subtracted by the first two years, multiplied by 4
      human_years = 24 + (@age - 2) * 4
    end

    puts "I'm #{human_years} in human years!"
  end
end

Let's now create a couple of Cat objects with different ages and test if the say_human_age is working. Add the following lines of code to the end of the cat.rb file:

cat_1 = Cat.new("Beth", 1)
cat_2 = Cat.new("Beth", 2)
cat_3 = Cat.new("Beth", 6)

cat_1.say_human_age
cat_2.say_human_age
cat_3.say_human_age

Save the file and run the program. The results should be this:

This is pretty awesome, but we can write our code in better ways. Let's refactor our code. Refactoring means to rewrite our code to improve the quality of the code, to make it more cleaner, more understandable, maintainable, etc.

Right now inside the say_human_age method, we are doing two things:

In general, methods should be responsible for doing only one thing. This is often called the Single Responsibility Principle. When you introduce too many responsibilities for a method or a class, it often leads to unmaintainable code. You don't need to worry too much about this just yet, but just keep in mind that in general, a method should only do one single thing.

Since our say_human_age method should only be responsible for outputting the cat's age in human years, let's move the calculation part to another method.

Let's add a new method called calculate_human_age, and move the calculation in that method. Change your code to look like this:

def say_human_age
  puts "I'm #{calculate_human_age} in human years!"
end

def calculate_human_age
  if @age == 1
    return 15
  elsif @age == 2
    return 24
  else
    return 24 + (@age - 2) * 4
  end
end

As you can see, now we have two methods that are each responsible for only one thing.


self

self is a concept in Ruby that is quite hard to grasp at first.

Simply put, self refers to the current object:

class WhatIsSelf
  def test
    puts "At the instance level, self is #{self}"
  end

  def self.test
    puts "At the class level, self is #{self}"
  end
end

WhatIsSelf.test
 #=> At the class level, self is WhatIsSelf

WhatIsSelf.new.test
 #=> At the instance level, self is #<WhatIsSelf:0x28190>

The example above indicates two things:

Private Methods and Encapsulation

Private methods are methods that cannot be called from outside of the class. Everything below private will be a private method:

class Test
  def initialize
  end

  def test_public
    # Private methods can be called within the class
    test_private
  end

  private
  # everything below here is private

  def test_private
    puts "This is private"
  end
end

test = Test.new

# This will work
test.test_public

# This will not work
test.test_private

Private methods are useful because they can hide some methods that should be hidden from the outside world. For example, in our Cat class, the outside world simply only needs to know that there is a method to output the cat's age in human years. It shouldn't need to know about how we are actually calculating it.

This is broadly a computer science concept called encapsulation. The basic idea is that by hiding how we are doing things, or in other words by hiding the implementation of the code, we allow whoever is using the method to just implement it without caring about how it's actually implemented.

It's similar to a restaurant kitchens. Customers order food off of a menu. The orders then come to the customer without the customer interfering with how the food is made in the kitchen. The menu is exposed to the customer, but what happens inside the kitchen is completely private.

Public Interfaces and Private Interfaces

Continuing with the example of a restaurant kitchen, there are things the customer should know about, and there are things that should be hidden from the customer.

In object oriented programming, the things that the outside world should know about are called public interfaces and the things that should be hidden from the outside world are called private interfaces.

By seperating the kitchen (or the code) in public and private interfaces, it lets the user ask for what they want without knowing anything about how the kitchen makes it.

In programming, public methods inside a class make up the public interface, and the private methods inside a class make up the private interface.

Let's take a look at a simple program that calculates a "Lucky Number" based on the name:

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

  def display_lucky_number
    number = calculate_lucky_number
    puts "My lucky number is #{number}!"
  end

  private

  def calculate_lucky_number
    (@name.length * 15 / 0.3 + 5).round
  end
end

In this case, the public interface consists of the display_lucky_number method, and then private interface consists of the calculate_lucky_number method.

In this case, the calculate_lucky_number method is made private, since the implementation details shouldn't be exposed to the user. Instead, it should be kept hidden, just like a kitchen doesn't expose what exactly is happening in the kitchen.

Another thing to note is that how the lucky number is calculated might change in the future. the public interface should only consist of methods that are unlikely to change. The private interface can consist of methods are allowed to change, like the calculate_lucky_number method.

Overview

Assignment

Let's build a Quote class that represents a single quote with a content and author attribute.

The program should work like this:

Quote.new("Stay hungry, stay foolish", "Steve Jobs")
Quote.new("Your most unhappy customers are your greatest source of learning.", "Bill Gates")
Quote.new("By giving people the power to share, we're making the world more transparent.", "Mark Zuckerberg")

Quote.random
=> #<Quote:0x007fa420835d30 @content="Your most unhappy customers are your greatest source of learning.", @author="Bill Gates">

linus_quote = Quote.new("Talk is cheap. Show me the code.", "Linus Torvalds")
linus_quote.display_quote
=> Talk is cheap. Show me the code. by Linus Torvalds

Hint: Create a class variable that will hold all of the quotes. Every time a Quote object is instantiated, it should be added to this class variable.