1.12 Our own classes
1.12.1 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 # => ArrayAnd 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 # => HashBut we can do even better than a Hash. We can define our own class to represent people with the class keyword:
class Person
endRemember, the only time we use capital letters in Ruby is when we’re referring to classes — and this goes for when we’re naming our own classes, too. So class person will not work. If you have a multi-word name, then CamelCase it — class VeryImportantPerson.
And we can declare what attributes a person can have with the attr_accessor keyword:
class Person
attr_accessor :first_name
attr_accessor :last_name
attr_accessor :role
endAnd now the Person class is a first-class citizen in the language, just like Array and Hash. Compare the code below to the code above for creating Arrays and Hashes to store information:
class Person
attr_accessor :first_name
attr_accessor :last_name
attr_accessor :role
end
c = Person.new
c.first_name = "Raghu"
c.last_name = "Betina"
c.role = "Instructor"
p c.last_name # => "Betina"
p c.role
p c.class # => PersonFor each attribute that we declared, we get methods that we can call to assign and retrieve values.
Defining instance methods
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 define our own methods with the def keyword. For example, try adding the following full_name method to the class we defined in the REPL above:
class Person
attr_accessor :first_name
attr_accessor :last_name
def full_name
return self.first_name + " " + self.last_name
end
endNow, 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
returnkeyword to signify what value I wanted to replacehs.full_namein the original expression after it’s been evaluated. - I used the
selfkeyword 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
require("date") # We need to pull in the Date class, which is not loaded by default
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
endNow 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 # => 32, as of this writingNote that we had to require("date")
The parentheses are almost always dropped after require. in order to load the Date class into the program; Ruby doesn’t load this class into every program by default, like it does with the core classes (String, Integer, etc).
So, rather than using a Hash to model real world things, it’s a good idea to create classes, and then empower them with behavior (methods) in addition to information.
Defining class methods
The methods full_name and age above are known as instance methods, because we call them on individual instances of the Person class (Homer, Mickey, Minnie, etc).
We can also define class-level methods, that we call directly on Person itself. This can be handy if we want to define re-usable utility methods that don’t really belong to any one individual person.
Here’s an example similar to Date.parse — what if we wanted users of the Person class to quickly be able to create new instances of the class like this:
Person.parse("Betina, Raghu")
# => should return a new person with first
# and last name attributes already populatedThen, we can define the class-level method parse, called directly on Person, like this:
class Person
attr_accessor :first_name
attr_accessor :last_name
def Person.parse(last_comma_first)
last_first_array = last_comma_first.split(",")
the_last_name = last_first_array.at(0).strip
the_first_name = last_first_array.at(1).strip
a_new_person = Person.new
a_new_person.first_name = the_first_name
a_new_person.last_name = the_last_name
return a_new_person
end
endThe new things to note in the code above:
- When defining the method, we do
def Person.parserather than justdef parseto make it a class method rather than an instance method. That way, we call the method directly on capital-PPerson. - We give the method the ability to accept an argument by adding parentheses and choosing a name for the argument when defining the method. Then we can use the input within the method definition, sort of like how we use a block variable.
1.12.2 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
endInstructors 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? Try defining all of the above and give it a shot in a REPL:
class Person
end
class Instructor
end
class Student
endOpen the GitPod “our own classes” project for this chapter and start with the exercise person.rb:
LTI{Load assignment}(https://github.com/bpurinton-appdev/our-own-classes-chapter/tree/bp-additions)[MV4dKHMwdAFhfRn752YW3TAY]{KBpPhe42o6wDRi35rWagKY4F}(20)[our_own_classes_project]
For a GitPod refresher, see here.