Objects and Methods
We will use the ruby-exercises (objects-and-methods) repository to practice writing objects that have methods and that interact with each other.
Getting Started
Start by cloning the repository:
Terminal
$ $ |
|
Exercise 1
Go to exercise-1:
Terminal
$
|
|
You will have a test directory with several files in it.
Implementing Candy
Start by running the tests for Candy
:
Terminal
$
|
|
What is does the error message say?
The most important part of the error message is this:
Terminal
|
|
The way to fix it is to create a directory named lib
(if it doesn’t exist), and add an empty file called candy.rb
to it:
Terminal
$ $ |
|
Run the tests again, and now the error message has changed:
Terminal
|
|
It’s looking for the constant Candy
. First it looks inside of CandyTest
,
which is what CandyTest::Candy
means. Next it looks outside of that class
(just Candy
). It only reports the first error, though.
We don’t want Candy
to live inside of CandyTest
. Define the Candy
class
inside of lib/candy.rb
.
1 2 |
|
Run the tests again, you will get an ArgumentError
:
Terminal
|
Implement the initialize method in Candy
so that it takes an argument.
Run the tests.
Terminal
|
|
Add an empty method named type
to the Candy class.
Run the tests, and you’ll finally get a failure rather than an error:
Terminal
|
Make the test pass, and then remove the skip
from the next test, and make
that one pass as well.
Implementing Bag
Next run the tests for the Bag
class:
Terminal
|
|
Follow the error messages to get the first test passing. Just do the simplest thing that could possibly work. The next tests will force the code to do whatever it needs to do.
Unskip the next test. It gives you a NoMethodError
that looks like this:
Terminal
|
|
That’s a familiar error message. Create an empty method named count
, and run
the tests again. Fix the failure.
Unskip the next test. The error message complains about another
NoMethodError
:
Terminal
|
|
Again, make the test pass in the simplest way possible.
Unskip the next test. Now you’ll get a NoMethodError
that looks like this:
Terminal
|
|
Really? A method named <<
? Well, that’s what it says. Let’s fix it the way
we fixed the previous NoMethodError
. Inside of the Bag
class, define this
method:
1 2 |
|
Run the tests again, and the error message changes:
Terminal
|
We need an argument for the <<
method, but what should the argument be?
The test says:
1
|
|
So I guess we’re putting a candy in the bag. Let’s call the argument candy
:
1 2 |
|
Run the tests again, and now we get a failure:
Terminal
|
Eew.
OK, let’s pick that apart.
The first line tells us that the failing test is in the BagTest
test suite.
No surprise there.
Then it says the failing test is named test_put_candy_in_the_bag
,
which makes sense, because that’s the test we’re working on at the moment.
It gives us the file name (test/bag_test.rb
). Again, this is no
surprise, since that’s the file we’re running to get this error.
Finally, it tells us which line the failing assertion is on (test/bag_test.rb:24
, so line 24).
Next it gives us some instructions. It says it’s going to show us some output,
and it will prefix the line with +++
if it got something it didn’t expect,
and it will prefix the line with ---
if it is missing something.
In other words, it expected to get an array with an instance of Candy in it, but it actually got an empty array.
What do we have on line 24?
1
|
|
We expected to get an array with an instance of candy in it ([candy]
), but
bag.candies
returns an empty array. At least that’s what I’m getting,
because I hard-coded the response to the candies
method to return an empty
array.
We need to start implementing some actual logic here.
What is the test actually doing?
1 2 3 4 5 6 |
|
It is:
- creating a new bag
- creating a piece of candy
- putting the candy in the bag
- asserting that that particular candy is in the bag’s array of
candies
.
So we need to do something smarter in the <<
method.
We have a method named candies
, but that is a reader
method, which is
intended to just read data, not change data. Since we want to change the
candies
array, let’s change the underlying instance variable named
@candies
.
1 2 3 |
|
Run the tests again.
Terminal
|
OK, it doesn’t like that we’re trying to shovel something into nil on line 15.
Line 15 says:
1
|
|
So @candies
is nil. That makes sense, because we never defined it anywhere.
We can define it in the initialize
method, since we always want to have it
available inside the bag.
Wait. What initialize
method? We don’t have one.
Well, we kind of have one. An empty class gets an empty initialize method for free whether it’s explicitly there or not.
So if we want to have something other than an empty initialize method, we need to create it explicitly:
1 2 3 |
|
That fixes the complaint about shoveling into nil, but our [candy]
array is
still empty.
Take a look at how the candies
method is defined inside Bag
. It’s still
hard-coding the empty array. If you return the instance variable @candies
instead, then this will now pass.
Unskip the next test. Mine fails because it expects the bag to not be empty anymore since we put candy in it. But the bag says that it’s still empty.
That’s because I hard-coded empty?
on the Bag
to return true
. Make the
empty?
method check if the candies
array is empty:
1 2 3 |
|
That makes the test pass. Unskip the next one.
Terminal
|
This test is failing because of a hard-coded value, too. I hard-coded the count as 0
.
candies
is an Array, and Ruby Arrays have a lot of methods defined on them by
default. You can see the full list here. We need a method that will tell us how many elements are in the array.
The documentation says this:
Arrays keep track of their own length at all times. To query an array about the number of elements it contains, use length, count or size.
So it doesn’t seem to matter which one we choose. Let’s use count
.
1 2 3 |
|
This can seem confusing. We’ve got two methods named count, one within the other. What’s going on here?
The first one is a method defined on an instance of Bag:
1 2 3 4 5 |
|
The second one is a message that the bag sends to the array of candies:
1
|
|
Run the tests – they should now be passing. Unskip the next test. This one should pass straight off the bat.
Make sure that you understand what it is doing.
Unskip the last test.
We get a NoMethodError
:
Terminal
|
|
Define the method and run the test again. Fix the ArgumentError
by giving it
a parameter.
The failure we get is a failed assertion, but the error message isn’t very helpful, we need to check what it’s actually trying to prove:
This is the code:
1 2 3 4 |
|
assert
will pass if whatever it is given evaluates to true
. It will fail if it is nil
or false
.
We need bag.contains?("Lindt chocolate")
to return true.
How are we going to know if the bag contains a candy with a type of "Lindt chocolate"?
We need to ask each candy what type it is. We can do that, because we have access to all the candies via the candies
method.
1 2 3 4 5 |
|
Now all the Bag
tests should pass.
Implementing Costume
Run the test/costume_test.rb
file, and implement Costume
. It’s very
similar to Candy
.
Implementing Trick-Or-Treater
This is where it gets interesting.
The first times you run the tests you’ll get the usual errors:
LoadError
– create the missing file.NameError
– create the missing class.ArgumentError
– add an initialize method that takes a parameter
The parameter should be named costume
since that is what we’re passing into
the new TrickOrTreat
instance in the test:
1 2 3 |
|
The TrickOrTreat
class now looks like this:
1 2 3 4 |
|
We’re not doing anything with the costume yet.
The next error message that we get is:
Terminal
|
Define the empty method, and we get a failed expectation:
Terminal
|
Make it pass by hard-coding the expected value, and then unskip the next test.
This test is going to force us to be smarter in the dressed_up_as
method.
The failure says:
Terminal
|
Where do the values "Pony"
and "Cowboy"
come from? They are the style of
the costume.
So dressed_up_as
needs to ask the costume what style it is, and return that value to the test.
To do that we need to save the costume so that we can access it in the initialize method:
1 2 3 |
|
Then we can fix dressed_up_as
:
1 2 3 |
|
That gets the tests passing. Unskip the next one:
Terminal
|
|
Define an empty method for bag
. Next the test gives us a NoMethodError
:
Terminal
|
We need to look at line 24 of the test file to figure out what’s going on:
1
|
|
We’re assuming that the bag
method returns a Bag
object, and then we’re
calling empty?
on it.
So let’s give the Trick-or-Treater object a Bag
object that it can return to us:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
That gets the tests passing.
Notice that we now have references to two objects inside our Trick-or-Treater: a Bag and a Costume. Any messages that we sent to Bag in the bag_test.rb
are fair game from inside the Trick-or-Treater. The same goes for Costume, though admittedly Costume doesn’t do much of anything.
Unskip the next test. Yet another NoMethodError
:
Terminal
|
The test states that when we have an empty bag, we don’t have candy.
Create the method has_candy?
, run the test again. It passes, because the empty method is returning nil, which is good enough when we need a false
.
Moving on: unskip the next test.
Here, we’re putting something in the bag, and now the Trick-or-Treater
should have candy, but the has_candy?
method has no implementation,
which means that it’s returning nil
rather than true
.
From inside has_candy?
the Trick-or-Treater has access to the instance of
Bag
with all of its methods. Since we defined a method on Bag
called empty?
we can call it from inside the has_candy?
method.
1 2 3 |
|
We can’t just say that the Trick-or-Treater has candy if the bag is empty, though, we need the opposite: The Trick-or-Treater has candy if the bag is not empty. Hence the bang (programmers like to call exclamation marks bang. Don’t ask me why).
Unskip the next test, and get a NoMethodError
. Define the method.
Next time we run the test we get a failed expectation: nil
instead of 0.
Go ahead and return 0
from the candy_count
method.
Run the tests, and the same test is failing, because after adding candy to the bag it expects the candy count to be 1.
The bag knows how much candy it contains, so the Trick-or-Treater object can ask the bag for its count:
1 2 3 |
|
Unskip the last method and run the tests. Fix the NoMethodError
by defining
the missing method.
Run the tests again – now we’re getting a failure.
Apparently if the Trick-or-Treater eats a piece of candy, the candy-count decreases.
The candy count is based on how many candies are in the bag, so we’ll need to actually take a candy out of the bag for the count to decreas.
Inside the bag, the candies are stored in an Array. Array has a handy method named pop
which will pop off the last item in the array and give it back to you. We don’t need to do anything with that piece of candy yet, so we can just pop it off and leave it at that:
1 2 3 |
|
And that’s it. This is the code that I ended up with:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
Now do it again
Blow away all your changes and start over, this time without looking at the tutorial so closely.
The easiest way to throw away changes is to run the following commands. The first removes any changes we made to files git knows about. The second will list new files in the working directory, which we can then remove the standard Unix way (remove with rm).
Terminal
$ $ $ |
|
Do this until you can write the production code to pass the tests without referencing the tutorial, then go to the exercise-2 directory and make those tests pass as well:
Terminal
$
|
|