- Understand what a class constant is
- Use a class constant
- Know when to use
attr_accessor
VS creating the methods yourself
Previously, we saw how to make each individual instance of a class hold on to the information about itself. A book object, an instance of the Book
class, knew its own title and author and genre. Building upon that, you will learn to add functionality that allows a class to hold information from across all the instances of that class that have been created.
- Read through the tutorial, following along as you go.
- Once you've finished, make the tests in
spec/01_book_spec.rb
pass. Run only those tests by typinglearn spec/01_book_spec.rb
on your command line.
In this tutorial, we are given a simple Book
class that has already been implemented with some basic functionality. It has properties like a title, an author, a page count, etc. Our job is to have the class somehow be able to keep track of all of the genres of all of the Books we create.
Let's run the tests for the first time, and see what our first failure is. The output in your terminal should be something similar to:
Failures:
1) Book GENRES keeps track of all genres
Failure/Error: expect(Book::GENRES).to include(genre)
NameError:
uninitialized constant Book::GENRES
# ./spec/01_book_spec.rb:11:in `block (4 levels) in <top (required)>'
# ./spec/01_book_spec.rb:10:in `each'
# ./spec/01_book_spec.rb:10:in `block (3 levels) in <top (required)>'
Ok, so this is new. This syntax, Book::GENRES
tells us a couple of things. First, whenever we see ::
, it indicates "name spacing". In other words, it tells us that the thing on the right side of the colons is a thing defined within the context, or name space, of the thing on the left side of the colons.
In this case, this is telling us that there is a constant, GENRES
(we know it's a constant because it's written in all caps) that is defined within our Book
class.
This is what is known as a Class Constant.
Class Constants are available to all instances of a particular class. Whereas instance variables, i.e. title, author, etc., are individual to each instance of a class, class constants are shared among all instances. They all have access to the same data, and if that data should change for some reason, all instances will know about that change.
Let's go ahead and define this constant. Since it's plural, I'm going to guess that it should be an array, so I'll start by defining it as an empty array:
# book.rb
class Book
attr_accessor :author, :page_count, :genre
attr_reader :title
GENRES = []
def initialize(title)
@title = title
end
def turn_page
puts "Flipping the page...wow, you read fast!"
end
end
This next error, after running the specs again, though is kind of weird:
Failures:
1) Book GENRES keeps track of all genres
Failure/Error: expect(Book::GENRES).to include(genre)
expected [] to include "Thriller"
# ./spec/01_book_spec.rb:11:in `block (4 levels) in <top (required)>'
# ./spec/01_book_spec.rb:10:in `each'
# ./spec/01_book_spec.rb:10:in `block (3 levels) in <top (required)>'
We really have no clue how in the world our array would magically contain "Thriller", so let's look at the specific test to figure out what's going on. Here's what we have in spec/01_book_spec.rb
:
describe 'GENRES' do
it 'keeps track of all genres' do
genres = ["Thriller", "Science Fiction", "Romance"]
genres.each_with_index do |genre, i|
book = Book.new("Book_#{i}")
book.genre = genre
end
genres.each do |genre|
expect(Book::GENRES).to include(genre)
end
end
end
So, it looks like what's going on is this:
- The test is creating a bunch of books.
- The test is assigning each of those books a genre.
- The test is expecting our GENRES class constant to keep track of those genres.
Ok, so let's think about this for a second. Where in that sequence of events can GENRES
be updated? Well, it can't be in step one. None of the books starts out having a genre. Maybe it happens in step two, but I'm not sure.
No, wait...it has to happen in step two. After the books are assigned their genres, nothing else happens before the test checks to see that Book::GENRES
knows about those genres. What does this mean, then?
It means that something else needs to happen in our genre=
method. We need to somehow update our GENRES
constant in that method. Remember, our genre=
method (along with our genre
method) is created by the following line of code: attr_accessor :genre
. Since we want to add some customization to the method, we'll have to do three things:
- Explicitly define the
genre=
method, to integrate our class constant into the method - Remove the
attr_accessor
for:genre
since we no longer need to generate a reader AND a writer. - Add an
attr_reader
for:genre
, since we still want Ruby to generate a reader for us.
So, let's do that!
# book.rb
class Book
attr_accessor :author, :page_count # remove the attr_accessor for genre
attr_reader :title, :genre # add an attr_reader for genre
GENRES = []
def initialize(title)
@title = title
end
def turn_page
puts "Flipping the page...wow, you read fast!"
end
# create the writer for genre and add the logic for the class constant
def genre=(genre)
@genre = genre
GENRES << genre
end
end
Now all the tests will pass!
Try to complete the similar exercise below where the Shoe
class can keep track of all of the brands of the different show objects.
Open spec/02_shoe_spec.rb
and start making the tests pass. Write your code in shoe.rb
.
To run the specs, type learn spec/02_shoe_spec.rb
on your command line.
Happy coding!
View Object Orientation Basics With Class Constants on Learn.co and start learning to code for free.
View OO Basics with Class Constants on Learn.co and start learning to code for free.