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
.
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.
self.method_name
. Class methods work for the class, and not for instances of classes.Class Variables - variables prefixed with @@
. Class variables can be used by the entire class, but aren't specific to the instance of the class.
It differs from instance variables because instance variables are values stored within specific instances of the class.
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
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:
self
is the class, in this case WhatIsSelf
.self
is the instance in context, in this case the instance of WhatIsSelf
at memory location 0x28190
.
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.
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.
Public Interface
The food menu - customers order food off of this menu.
Private Interface
What happens inside the kitchen - the customer is not welcome to stir the soup inside the kitchen
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.
def self.method_name
@@
are class variables
self
refers to the current object in contextLet's build a Quote
class that represents a single quote with a content
and author
attribute.
content
and author
display_quote
that prints out #{quote} by #{author}
random
that will print out the content
of a random quoteThe 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.