- Pair, stage 2 project
- Due, end of day, Friday October 1st.
Remember the ride share exercise we did with designing and creating a system to track the ride share data from a CSV file? We did a lot of great work on creating arrays and hashes of data, but we've learned a lot since then!
Now, we're going to use our understanding of classes, methods and attributes to create an object-oriented ride share system that is much more robust than our previous implementation.
- Reinforce and practice Ruby programming fundamentals, particularly
- Creating and instantiating classes with attributes
- Using composition to connect different classes together
- Working with files and large amounts of data
- Writing pseudocode and creating tests to drive the creation of our code
- Quickly become familiar with a large and complex code base
- Use inheritance to extend existing classes
We will build a Ruby library that loads lists of passengers, trips and drivers from CSV files, provides methods for exploring this data, and provides a way to request a new trip.
We will not write an interactive command-line program.
We will use the same project structure we used for the previous project. Classes should be in files in the lib
folder, and tests should be in files in the test
folder. You will run tests by executing the rake
command, as configured in a Rakefile.
The support
folder contains CSV files which will drive your system design. Each CSV corresponds to a different type of object as well as creating a relationship between different objects.
- You'll be working with an assigned pair. High-five your pair.
- Choose one person to fork this repository in GitHub
- Add the person who didn't fork the repository as a collaborator.
- Both individuals will clone the forked repo: $ git clone
[YOUR FORKED REPO URL]
- Both partners
cd
into their project directory - Each partner should install Bundler with
gem install bundler
. - Each partner should run
bundle install
, this command will read theGemfile
and install all the required gems (like rake!). If you are curious you can read more about gemfiles. - Run
rake
to run the tests
You should use the following process as much as possible:
- Write pseudocode
- Write test(s)
- Write code
- Refactor
Come up with a "plan of action" for how you want to work as a pair. Discuss your learning style, how you prefer to receive feedback, and one team communication skill you want to improve with this experience. Second, review the requirements for Wave 1 and come up with a plan for your implementation.
This project comes with a large amount of scaffolding code already written. Before you start writing code, you and your partner should spend some time reading through what's here. Building a strong understanding now will make the rest of the project much smoother.
Wave 0 takes time. It is entirely reasonable to spend the first day of the project on Wave 0.
Start by running the tests:
$ rake test
... test output ...
Finished in 0.14685s
28 tests, 64 assertions, 0 failures, 0 errors, 1 skips
All the existing code is thoroughly tested. Some of the code you write for this project will break these tests; when that happens, it is your job to update them. When you add new functionality to this project, you should add new tests as well.
The tests can serve as an example of how the methods and classes should work. If you're ever confused about what one of these methods looks like "from the outside", the tests can serve as your guide.
The existing code contains 4 classes:
Passenger
Trip
CsvRecord
TripDispatcher
Before you go any further, think about how these classes might be related. Draw a diagram, if that will help. Your guess doesn't have to be correct at this stage - making a prediction and then checking it is an important part of the learning process. You can draw the diagram on paper or use an application such as app.diagrams.net which is free although it requires Google Drive.
Now, start reading through the code. There are two equally valid ways to approach this:
- Top-down: start with the big pieces.
- What classes are there, and how do they interact?
- How does it look from the outside?
- How are these classes tested?
- Bottom-up: start with the details. Pick an interesting method (say
Passenger.load_all
) and learn everything about it.- What arguments does it take?
- How does it work? If there's more than one step, what are they? Why are they in that order?
- What other methods does call?
- What does it return?
- What code calls it, and how does it use the returned value?
- How is it tested?
One of the most effective way to learn a new codebase is to use it. Since this code is a library not a program, there's nothing obvious to run from the command line, so we'll have to be a little clever. We can use pry
to load the code, and then play with it in the interpreter:
$ pry -r ./lib/trip_dispatcher.rb
[1] pry(main)> td = RideShare::TripDispatcher.new
=> #<RideShare::TripDispatcher:0x3fe91f52950c>
[2] pry(main)> td.trips.length
=> 600
You could also create your own ruby file, for example driver.rb
and create instances of the provided classes and experiment with the methods.
td = RideShare::TripDispatcher.new
puts "Trips length is #{td.trips.length}"
...
Before you start writing code, you and your partner should use the above techniques and work together to answer the following questions. If you have questions or run into something that you can't figure out, consult with another group.
- What inheritance relations exist between classes?
- What composition relations exist between classes?
- Do these relations match your prediction from earlier?
- Draw a class diagram that contains all of the above relations.
- Why doesn't
Passenger
orTrip
need anattr_reader
forid
? - Why does the version of
from_csv
inCsvRecord
raise aNotImplementedError
? What does this mean? Why don't we hit that when we run the code? - Why is
from_csv
a private method? - How does
CsvRecord.load_all
know what CSV file to open? - When you call
Passenger.load_all
, what happens? What methods are called in what order?
Using the pry session we started above, how would you...
- Print the rating for the first trip
- Print the name of the passenger for trip 7
- Print the ID of every trip taken by passenger 9
- Print the ID of the trip that cost the most money
The purpose of Wave 1 is to help you become familiar with the existing code, and to practice working with enumerables.
Currently our implementation saves the start and end time of each trip as a string. This is our first target for improvement. Instead of storing these values as strings, we will use Ruby's built-in Time
class. You should:
- Spend some time reading the docs for
Time
- you might be particularly interested inTime.parse
- Modify
Trip.from_csv
to turnstart_time
andend_time
intoTime
instances before passing them toTrip#initialize
- Add a check in
Trip#initialize
that raises anArgumentError
if the end time is before the start time, and a corresponding test - Add an instance method to the
Trip
class to calculate the duration of the trip in seconds, and a corresponding test
Hint: If you're hitting a NoMethodError
for Time.parse
, be aware that you need to require 'time'
in order for it to work.
Now that we have data for trip time stored in a more convenient way, we can do some interesting data processing. Each of these should be implemented as an instance method on Passenger
.
- Add an instance method,
net_expenditures
, toPassenger
that will return the total amount of money that passenger has spent on their trips - Add an instance method,
total_time_spent
toPassenger
that will return the total amount of time that passenger has spent on their trips
Each of these methods must have tests. What happens if the passenger has no trips?
Our program needs a data type to represent Drivers in our service.
We will do this by creating a Driver
class which inherits from CsvRecord
, similar to Trip
and Passenger
. The constructor for Driver
should take the following keyword arguments:
Attribute | Description | Rules |
---|---|---|
id |
Unique number for this driver | Pass to the superclass constructor (similar to Passenger ) |
name |
This driver's name | |
vin |
The driver's Vehicle Identification Number (VIN) | String of length 17. Raise an ArgumentError if it's the wrong length. |
status |
Is this Driver available to drive? |
Must be one of :AVAILABLE or :UNAVAILABLE |
trips |
A list of trips this driver has driven | Optional, if not provided, initialize to an empty array (similar to Passenger ) |
Since Driver
inherits from CsvRecord
, you'll need to implement the from_csv
template method. Once you do, Driver.load_all
should work (test this in pry).
Use the provided tests to ensure that a Driver
instance can be created successfully and that an ArgumentError
is raised for an invalid status. Before the driver tests can work, you'll want to uncomment line 15 in test_helper.rb
that looks like require_relative '../lib/driver'
so that the tests can access the new class.
To make use of the new Driver
class we will need to update the Trip
class to include a reference to the trip's driver. Add the following attributes to the Trip
class.
Attribute | Description |
---|---|
driver_id |
The ID of the driver for this trip |
driver |
The Driver instance for the trip |
When a Trip
is constructed, either driver_id
or driver
must be provided.
Note: You have changed the method signature of the constructor for Trip
. Some of your tests may now be failing. Go fix them!
Update the TripDispatcher
class as follows:
- In the constructor, call
Driver.load_all
and save the result in an instance variable - Update the
Trip#connect
method to connect the driver as well as the passenger (you'll want to create add trip on driver first - see below) - Add a
find_driver
method that looks up a driver by ID
After each Trip
has a reference to its Driver
and TripDispatcher
can load a list of Driver
s, add the following functionality to the Driver
class:
Method | Description | Test Cases |
---|---|---|
add_trip | Add a trip to the driver's list of trips | Try adding a trip |
average_rating | What is this driver's average rating? | What if there are no trips? Does it handle floating point division correctly? For example the average of 2 and 3 should be 2.5, not 2. |
total_revenue | This method calculates that driver's total revenue across all their trips. Each driver gets 80% of the trip cost after a fee of $1.65 per trip is subtracted. | What if there are no trips? What if the cost of a trip was less that $1.65? |
All the new methods above should have tests
Expand for musings on object-oriented design
You may notice that `Driver` and `Passenger` share a lot of traits, especially the ability to work with a list of trips. If we were to flesh this library out more, you could imagine having a lot of repeated code. Addressing this problem is an interesting challenge.One way we could DRY this up is through more inheritance (perhaps a TripTaker
class), but inheritance is a little heavy-handed for our purposes. A more appropriate technique might be to use a module to include common behavior. This technique is called a mix-in and is something beyound the scope of what we'll learn at Ruby, but an interesting and useful technique.
For right now, we'll just let Passenger
and Driver
have some repeated code.
Our program needs a way to make new trips and appropriately assign a driver and passenger.
This logic will be handled by our TripDispatcher
in a new instance method: TripDispatcher#request_trip(passenger_id)
. When we create a new trip with this method, the following will be true:
- The passenger ID will be supplied (this is the person requesting a trip)
- Your code should automatically assign a driver to the trip
- For this initial version, choose the first driver whose status is
:AVAILABLE
- For this initial version, choose the first driver whose status is
- Your code should use the current time for the start time
- The end date, cost and rating will all be
nil
- The trip hasn't finished yet!
You should use this information to:
- Create a new instance of
Trip
- Modify this selected driver using a new helper method in
Driver
:- Add the new trip to the collection of trips for that
Driver
- Set the driver's status to
:UNAVAILABLE
- Add the new trip to the collection of trips for that
- Add the
Trip
to thePassenger
's list ofTrip
s - Add the new trip to the collection of all
Trip
s inTripDispatcher
- Return the newly created trip
All of this code must have tests. Things to pay attention to:
- Was the trip created properly?
- Were the trip lists for the driver and passenger updated?
- Was the driver who was selected
AVAILABLE
? - What happens if you try to request a trip when there are no
AVAILABLE
drivers?
One thing you may notice is that this change breaks your code from previous waves, possibly in subtle ways. We've added a new kind of trip, an in-progress trip, that is missing some of the values you need to compute those numbers.
Your code from waves 1 & 2 should ignore any in-progress trips. That is to say, any trip where the end time is nil
should not be included in your totals.
You should also add explicit tests for this new situation. For example, what happens if you attempt to calculate the total money spent for a Passenger
with an in-progress trip, or the average rating of a Driver
with an in-progress trip?
This wave is optional! Don't even look at it until you're sure your code from the previous waves meets every requirement!
We want to evolve TripDispatcher
so it assigns drivers in more intelligent ways. Every time we make a new trip, we want to pick drivers who haven't completed a trip in a long time, or who have never been assigned a trip.
In other words, we should assign the driver to the available driver who has never driven or lacking a new driver one whose most recent trip ending is the oldest compared to today.
Modify TripDispatcher#request_trip
to use the following rules to select a Driver
:
- The
Driver
must have a status ofAVAILABLE
- The
Driver
must not have any in-progress trips (end time ofnil
) - From the
Driver
s that remain, select the one who has never driven or whose most recent trip ended the longest time ago
For example, if we have three drivers, each with two trips:
Driver Name | Status | Trip 1 end time | Trip 2 end time |
---|---|---|---|
Ada | AVAILABLE |
Jan 3, 2018 | Jan 9, 2018 |
Katherine | AVAILABLE |
Jan 1, 2018 | Jan 12, 2018 |
Grace | UNAVAILABLE |
Jan 5, 2018 | nil |
Grace is excluded because they are not AVAILABLE
, and because they have one in-progress trip.
Of Ada and Katherine, we prefer Ada, because their most recent trip is older.
All of this code must have tests.
Check out the feedback template which lists the items instructors will be looking for as they evaluate your project.