More on Ruby Classes

Modeling real world things

We can model a person pretty well with an Array:

a = Array.new # This is the longhand for a = []
a.push("Raghu")
a.push("Betina")
a.push("Instructor")
a # => ["Raghu", "Betina", "Instructor"]
a.at(1) # => "Betina"
a.class # => Array

And we can model a person even better with a Hash:

h = Hash.new # This is the longhand for h = {}
h.store(:first_name, "Raghu")
h.store(:last_name, "Betina")
h.store(:role, "Instructor")
h # => { :first_name => "Raghu", :last_name => "Betina", :role => "Instructor" }
h.fetch(:last_name) # => "Betina"
h.class # => Hash

But we can do even better than a Hash. We can define our own class to represent people:

class Person
end

And we can declare what attributes a person can have:

class Person
attr_accessor :first_name
attr_accessor :last_name
attr_accessor :role
end

And now the Person class is a first-class citizen in the language, just like Array and Hash:

c = Person.new
c.first_name = "Raghu"
c.last_name = "Betina"
c.role = "Instructor"
c.last_name # => "Betina"
c.class # => Person

There are a few reasons I like using classes more than Hashes to model things, but here is the big one: in addition to just storing a list of attributes about a thing, we can also add behavior. For example,

class Person
attr_accessor :first_name
attr_accessor :last_name
def full_name
return self.first_name + " " + self.last_name
end
end

Now, in addition to being able to store data (first and last names), I can ask any Person to compute its full name:

hs = Person.new
hs.first_name = "Homer"
hs.last_name = "Simpson"
"Hello, #{hs.full_name}!" # => "Hello, Homer Simpson!"

Two new keywords to note:

  • I used the return keyword to signify what value I wanted to replace hs.full_name in the original expression after it's been evaluated.

  • I used the self keyword to refer to the object who was asked to calculate its full name, since I can't know in advance what (if any) variable name will be used.

Here's a slightly more involved example:

class Person
attr_accessor :birthdate
def age
dob = Date.parse(self.birthdate)
now = Date.today
age_in_days = now - dob
age_in_years = age_in_days / 365
return age_in_years.to_i
end
end

Now every Person that we create will have the ability to compute their age based on their own dob attribute:

hs = Person.new
hs.birthdate = "April 19, 1987"
hs.age # => 29, as of this writing

So, rather than using a Hash to model everything, it's much more powerful to create classes for each kind of thing, and then empower them with behavior (methods) in addition to information.

Inheritance

When you define new classes, you can choose to inherit all the power of a "parent" class, and then add some custom behavior:

class Instructor < Person
attr_accessor :role
end
class Student < Person
attr_accessor :grade
end

Instructors and Students can do everything people can, and a little bit more.

Creating the first individual instance of the Instructor class:

person1 = Instructor.new
person1.first_name = "Raghu"
person1.last_name = "Betina"
person1.role = "Lecturer"

Creating the second individual instance of the Instructor class:

person2 = Instructor.new
person2.first_name = "Arjun"
person2.last_name = "Venkataswamy"
person2.role = "Faculty Coach"

Creating the first individual instance of the Student class:

person3 = Student.new
person3.first_name = "Trenton"
person3.last_name = "Arthur"
person3.grade = "A"

Creating the second individual instance of the Student class:

person4 = Student.new
person4.first_name = "Tom"
person4.last_name = "Besio"
person4.grade = "Incomplete"

Now we can use them:

person1.full_name # => "Raghu Betina"
person1.role # => "Lecturer"
person2.full_name # => "Arjun Venkataswamy"
person2.role # => "Faculty Coach"
person3.full_name # => "Trenton Arthur"
person3.grade # => "A"
person4.full_name # => "Tom Besio"
person4.grade # => "Incomplete"

What would happen if I tried doing person4.role? How about person1.grade? Why? What would the error message be?

Using our own classes

We usually store classes that we write in the app/models folder. For example, if you create a file in that folder called person.rb with the following:

class Person
attr_accessor :first_name
attr_accessor :last_name
attr_accessor :birthdate
def full_name
return self.first_name + " " + self.last_name
end
def age
dob = Date.parse(self.birthdate)
now = Date.today
age_in_days = now - dob # Returns a Rational number
age_in_years = age_in_days / 365
return age_in_years.to_i
end
end

You can now use the Person class from anywhere in the app: from within any controller, any view template, in the rails console -- or even from within another model.

Ruby is called an Objected Oriented (OO) language because we always strive to organize our code into descriptive classes and methods, rather than just using Hashes and Arrays for everything.

For example, Rubyists much prefer to define the Person class above and then

hs = Person.new
hs.first_name = "Homer"
hs.last_name = "Simpson"
"Hello, #{hs.full_name}!" # => "Hello, Homer Simpson!"

Rather than

h = { :first_name => "Homer", :last_name => "Simpson" }
"Hello, #{h.fetch(first_name)} #{h.fetch(:last_name)}!" # => "Hello, Homer Simpson!"

even though the two are functionally equivalent, and the second could be considered more concise (in terms of number of lines of code).

By encapsulating the logic of how to compute full_name in the class definition, I make it much easier to re-use elsewhere and share.

Where have we seen this before?

It might now make a bit more sense what we were doing when we created our controllers while learning RCAV:

# config/routes.rb
get("/rock", { :controller => "games", :action => "play_rock" })
# app/controllers/games_controller.rb
class GamesController < ApplicationController
def play_rock
@computer_move = ["rock", "paper", "scissors"].sample
render("games/play_rock.html.erb")
end
end

In routes.rb, we tell Rails to listen for requests for "/rock", and when it hears one, run the play_rock method on an instance of GamesController. Behind the scenes, Rails would do something like

g = GamesController.new
g.play_rock

when someone visits "/rock". That's why we have to be so careful when we're connecting the RCAV dots; otherwise, Rails can't locate the method to run.

GamesController inherits from ApplicationController, which is a pre-written class we get from Rails.

It has lots of powerful methods that deal with web requests, like render(), which knows how to parse a .html.erb embedded Ruby view template and process it into pure HTML suitable for a browser to display.

Thankfully, we inherit all that functionality for free from Rails, instead of having to write it ourselves! We can instead focus on modeling the problem that we want to solve with our app, rather than worrying about all that plumbing.