1.12 Our own classes
1.12.1 Modeling real world things
We can model a person pretty well with an Array
:
Array.new # This is the longhand for a = []
a = "Raghu")
a.push("Betina")
a.push("Instructor")
a.push(
# => ["Raghu", "Betina", "Instructor"]
a
1) # => "Betina"
a.at(# => Array a.class
And we can model a person even better with a Hash
:
Hash.new # This is the longhand for h = {}
h = :first_name, "Raghu")
h.store(:last_name, "Betina")
h.store(:role, "Instructor")
h.store(
# => { :first_name => "Raghu", :last_name => "Betina", :role => "Instructor" }
h
:last_name) # => "Betina"
h.fetch(# => Hash h.class
But we can do even better than a Hash
. We can define our own class to represent people with the class
keyword:
class Person
end
Remember, 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
end
And 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 Array
s and Hash
es to store information:
class Person
attr_accessor :first_name
attr_accessor :last_name
attr_accessor :role
end
Person.new
c = "Raghu"
c.first_name = "Betina"
c.last_name = "Instructor"
c.role =
# => "Betina"
p c.last_name
p c.role# => Person p c.class
For 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 Hash
es 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
end
Now, in addition to being able to store data (first and last names), I can ask any Person
to compute its full name:
Person.new
hs = "Homer"
hs.first_name = "Simpson"
hs.last_name =
"Hello, " + hs.full_name + "!" # => "Hello, Homer Simpson!"
Two new keywords to note:
- I used the
return
keyword to signify what value I wanted to replacehs.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
"date") # We need to pull in the Date class, which is not loaded by default
require(
attr_accessor :birthdate
def age
Date.parse(self.birthdate)
dob = Date.today
now =
age_in_days = now - dob365
age_in_years = age_in_days /
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:
Person.new
hs = "April 19, 1987"
hs.birthdate =
# => 32, as of this writing hs.age
Note 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 populated
Then, 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(0).strip
the_last_name = last_first_array.at(1).strip
the_first_name = last_first_array.at(
Person.new
a_new_person =
a_new_person.first_name = the_first_name
a_new_person.last_name = the_last_name
return a_new_person
end
end
The new things to note in the code above:
- When defining the method, we do
def Person.parse
rather than justdef parse
to make it a class method rather than an instance method. That way, we call the method directly on capital-P
Person
. - 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
end
Instructor
s and Student
s can do everything people can, and a little bit more.
Creating the first individual instance of the Instructor
class:
Instructor.new
person1 = "Raghu"
person1.first_name = "Betina"
person1.last_name = "Lecturer" person1.role =
Creating the second individual instance of the Instructor
class:
Instructor.new
person2 = "Arjun"
person2.first_name = "Venkataswamy"
person2.last_name = "Faculty Coach" person2.role =
Creating the first individual instance of the Student
class:
Student.new
person3 = "Trenton"
person3.first_name = "Arthur"
person3.last_name = "A" person3.grade =
Creating the second individual instance of the Student
class:
Student.new
person4 = "Tom"
person4.first_name = "Besio"
person4.last_name = "Incomplete" person4.grade =
Now we can use them:
# => "Raghu Betina"
person1.full_name # => "Lecturer"
person1.role # => "Arjun Venkataswamy"
person2.full_name # => "Faculty Coach"
person2.role # => "Trenton Arthur"
person3.full_name # => "A"
person3.grade # => "Tom Besio"
person4.full_name # => "Incomplete" person4.grade
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
end
Open 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.