Gear-up: Sunday, March 13 at 3pm ET at this link!
Design Check: March 14-16, 2022
Due Date: Thursday, March 24, 2022 at 11:59pm ET
Note on stencil code: You'll receive invitations to join your repository with the stencil code shortly after project pairings are out.
In this project, you will be designing a system that will allow a user (e.g. Pitbull aka tall Mr. Worldwide) to plan travel routes that meet their needs (though it's likely he's already been there, done that). Specifically, users can load a travel graph, then request routes according to one of 3 priorities:
Users will load graphs and request routes through text prompts in the IntelliJ console (we've written that part). Your code will compute routes, which our code will print back to users.
For this and all future assignments, we expect your code to conform to the CS200 Java Style Guide.
In lecture, we have talked about different algorithms for finding paths in graphs. Different algorithms are good for optimizing different aspects of paths. Sometimes, the same algorithm needs to be tailored to work on different pieces of information. This project has you combine what we've been learning about algorithms, software organization, and testing to develop a well-designed program for choosing and customizing algorithms. Concretely, you will:
Note: The end of each sub-section in the Implementation Details section gives you a list of tasks you need to do to complete for each part of the project. The design check section walks you through some ways you might think about designing the code for these tasks.
Let's say you have the following route data:
A few things to note:
If a user requests the fewest connections to get from New York City to Providence, they'll be given a direct route that costs $40 and takes 225 minutes. If instead they ask for the fastest such route, however, they'll be given a route that goes through Boston and takes a total of 130 minutes.
Here's an example of what the interface looks like for a user to load a graph and request a (fast) route. The valid commands at the prompt are load
, fast
, cheap
, and direct
. Note that we have to put "New York City" in quotes since its name is more than one word.
If an error occurs, the interface should print an informative message such as
The cities and transit options between them are provided in CSV files. We have given you the CSV parser (TravelCSVParser.java
) and the command-line interface code (REPL.java
). The latter is associated with an interface ITravelController
that requires the load
, fast
, cheap
, and direct
methods. You will flesh out a class that implements this interface, and you will write the classes for the underlying graph data structure and the route-finding algorithms.
The classes for this project should be organized around a model-view-controller architecture.
You will find the following classes in the src
directory:
City
class that you will use to represent the vertices of your graphIBFS
interface with the header for the method that performs BFSIDijkstra
interface with the header for the method that performs Dijkstra's lowest cost algorithmIGraph
interface with methods for creating and getting information from graphsITravelController
interface with methods to load graphs from files and find routes with different prioritiesMain
class that you will use to run your project.REPL
class containing code to allow the user to interact with the programTransport
class that you will use to represent the edges of your graphTransportType
enum that defines an enumerated type (fixed set of values) for transit options, specifically plane
, train
, and bus
TravelCSVParser
class to parse CSV files into Map
objects that you can use to create graphsYou will find the following (initial) classes in the sol
directory:
TravelController
class that implements the ITravelController
interface`TravelGraph
class that implements the IGraph
interfaceDijkstra
class that implements the IDijkstra
interfaceBFS
class that implements the IBFS
interfaceYou will find the following classes in the test
directory:
simple
package, containing:
SimpleVertex
class that represents a simple vertexSimpleEdge
class that represents a simple edgeSimpleGraph
class that represents simple graphGraphTest
class, which you will use to test your graph methodsBFSTest
class, which you will use to test your BFSDijkstraTest
class, which you will use to test your Dijkstra'sThe data
directory provides the CSV files for the sample we showed (cities1.csv
and transport1.csv
), as well as a second graph on 5 cities. You will create CSV files for your own graphs for testing in this directory.
The test/simple
directory provides three basic classes for a vertex, edge, and graph. These don't have the city and transit details. We provide these so you can do basic tests of your BFS and Dijkstra's algorithms, but using these classes is not required and you will not be graded on tests using these classes.
Running the program (via the Main.java
we provide) will call the run
method in REPL.java
.
The run
method presents a prompt to the user (in your IntelliJ terminal window). At this point, the user can enter commands (load
, fast
, cheap
, direct
) as shown in the above interface demo. The REPL
code will call the methods declared in the ITravelController
interface to execute those commands (you will implement the underlying methods in one of your classes).
Your implementation of load
(from ITravelController
) will call methods in the TravelCSVParser
class. Your implementations of fast
, cheap
, and direct
in your TravelController
will call the IBFS
and IDijkstra
methods that you've implemented.
There are different ways you could proceed with the design of this project. We outline two options near the bottom of the handout.
Your program needs to support finding routes based on combinations of time or cost. Fast routes look at time and ignore costs. Cheap routes look at cost and ignore time. One could also imagine more sophisticated weights based on factors like the cost per minute of travel time. This means that edge weights may vary depending on travel priorities. How do you implement Dijkstra's algorithm to account for differing edge weights from use to use?
You might imagine having your Dijkstra's implementation take a String
as an argument that names which field to use. This is a weak approach (you'll think about why for your design check). Instead, you'll want to provide Dijsktra's with a function that indicates how to weigh an edge for that particular run of the algorithm. This is the kind of task that we used lambdas for in Racket and Pyret. How do we do this in Java?
Java provides an interface called Function
that can be used to create a function with one parameter. Here's a brief example:
The Function
interface takes two arguments: the input type and the output type. The notation for the function definition itself is
You don't include return
in this notation (the result of the expression gets returned automatically). An important exception to this rule is when you are writing a Function
that returns Void
. In this case, you must explicitly return null
at the end of your function, even if it is a single-line function (i.e. making it a two-line function). Again, this is similar to lambdas in Pyret and Racket.
Sometimes, the body of a Function
may need to perform multiple steps. Wrapping the body expression in curly braces allows this (note: in this case you need to include an explicit return
):
To use the Function
object, we call its (built-in) apply
method, giving it the input for the call. The second println
in the below code thus calls testWithEight
on the Integer
5
.
Here's an example of a method that takes a Function
as an input:
You should use Function
objects to customize two parts of your code: tailoring edge weights in Dijkstra's algorithm and building your graph using the CSV parser. Details on both are below.
There are three major programming steps, each detailed in its own labeled section below.
Graphs can be represented and stored with a variety of built-in data structures (LinkedLists
, ArrayLists
, Maps
, etc.) and classes that you define. You will use the City
class to represent your vertices (as the parameterized V
type) and the Transport
class to represent your edges (as the parametrized E
type). You will use these in a class called TravelGraph
that implements the IGraph<V,E>
interface.
Something you will need to think about inside your TravelGraph
class is how you will get from a city name to the object in memory that represents the corresponding City
. How can you store each City
such that it is paired with the name of said city? The design check walks you through some scenarios where you might need to think about this.
Project tasks:
City
) and/or edges (Transport
) of a graph.TravelGraph
class that implements IGraph<V,E>
. You will use City
as your V
and Transport
as your E
. You may have to add additional methods for your TravelGraph
beyond what is in IGraph
! The design check walks you through thinking about this.The contents of the graphs you will work with are given in pairs of CSV files. One file defines the cities, and another defines the transit options between them. Here are what those look like for the sample graph at the top of the handout:
data/cities1.csv
data/transport1.csv
Note: The first line of your cities file MUST be "name" and the first line of your transport file MUST be "origin,destination,type,price,duration". Our parser will not work if this is not the case
Creating a graph from the CSV files requires extracting the rows one at a time and using them to create vertices and edges in your data structures. The TravelCSVParser
class that we provide does part of this work: it reads rows one at a time, turns each into a Map
from column name (ex: "destination") to cell value (ex: "Providence"), then passes that row to a function that creates the vertex or edge data accordingly. This class uses a Java CSVParser library to transform the CSV information into Maps.
For instance, a Map
for the 3rd row of data in the above transport1.csv
file would look like this, with the first row representing the keys and the 2nd row representing the corresponding values:
"origin" | "destination" | "type" | "price" | "duration" |
---|---|---|---|---|
Providence |
Boston |
train |
13 |
80 |
Specifically, TravelCSVParser
provides two methods, one for reading each kind of file. Further details are provided in the header comments above the methods.
We have also provided a TransportType
class that creates an enumerated type of known transportation types (BUS
, TRAIN
, PLANE
). The following code converts a transit type string into an enumerated value:
Project Tasks:
TravelGraph
TravelController
To find the best route for the respective priorities, you will implement breadth-first search and Dijkstra's algorithm (both of which were covered in lecture). It is your job to figure out which algorithm is best fit for each of price, time, and directness. You will call the methods for these algorithms when appropriate in your ITravelController
class to define the route-finding methods.
We have given you classes BFS
and Dijkstra
which implement the IBFS
and IDijkstra
interfaces, respectively. You can read about the methods for these classes in the Javadocs comments for IBFS
and IDijkstra
in the src
directory.
Note that your implementation of Dijkstra's algorithm will require a PriorityQueue
(or another data structure that stores things by priority). As a result, you will need to make a Comparator
(refer to Lab 6 for how to do this). As this interface needs to be parameterized, you will need to consider what is being stored in your PriorityQueue
and how it should be parametrized.
Note: Java's PriorityQueue
does not automatically update when the priority of one of the vertices changes. As a result, if a vertex's priority changes, you must remove it and re-add it to the PriorityQueue
.
Project Tasks:
getPath
method of your BFS<V,E>
class. We encourage you to use helper methods to make your code more readable!
getShortestPath
method of you Dijkstra<V,E>
class. Again, we encourage you to use helper methods!
TravelPlanner
.For testing, we want you to:
You can (and should!) use the same graph for multiple individual tests (e.g., a fast route between two cities, a direct route between two different cites, etc).
A testing plan shows drawings of the graph to use for testing and a bulleted list of the cases to test in the graph and why. For example (again using our sample graph):
Testing Plan:
- Check for the fastest route from New York City to Providence because it requires taking a longer route
- …
You do not need to test every last method you wrote, but you should test the ones you deem critical. Even a small bit of testing can be a useful checkpoint on your work (and it can save you a lot of debugging time later).
Project tasks:
GraphTest.java
BFSTest.java
DijkstraTest.java
There are four main goals for the design check: understanding the BFS and Dijkstra algorithms, understanding the starter code, considering the impacts of algorithm and design decisions, and planning the project.
Design Check Task: Work through an example of using Dijkstra's algorithm in the conceptual check Gradescope assignment. As you submit each question, Gradescope will tell you whether you are right or wrong, so that you can check your conception as you go. You can and should resubmit as many times as you need to. You may talk to your partner as you go through this task, but we expect both partners to submit (and get full points on) the assignment.
Design Check Task: Look at the example graph at the top of this handout and, by hand, compute BFS (assume neighbors are considered in alphabetical order) and Dijkstra (select an appropriate edge weight) for each of these routes (the first one is done for you, using cost as the edge weight):
Route | BFS route | Dijkstra route |
---|---|---|
NYC -> Boston | Plane(NYC -> Boston) | Bus(NYC -> Providence); Bus(Providence -> Boston) |
NYC -> Providence | ||
Boston -> Providence | ||
Boston -> NYC | ||
Providence -> Boston | ||
Providence -> NYC |
Design Check Task: Based on these results and your understanding of BFS/Dijkstra, identify which algorithm (BFS or Dijkstra) you should use for computing the:
Design Check Task: Because there are two algorithms and three priorities, you had to select the same algorithm for at least two of the goals (for example, at least two priorities need to be computed with Dijkstra, or at least two priorities need to be computed with BFS). Identify how the application of the algorithm changes for the two priorities. (e.g. if you chose Dijkstra for two of the priorities, what would cause the algorithm to come up with one answer for one priority and a potentially different answer for a different priority?)
Design Check Task: Look through the starter code and draw a containment/association diagram of how the classes fit together. You can ignore Main
and REPL
. We have started this for you:
See the notes from Lecture 4 of the 111/17/19 track for an example of a containment/association diagram.
Design Check Task: Practice connecting concepts in this handout to the code by answering the following questions:
Function
object defined in the code and also find where that object is passed in as an argument to another method.Design Check Task: Draw out the way the following method calls will affect fields in the TravelGraph
class and related classes. This means that you'll have to decide on the field(s)/data structure(s) that are contained in this class.
Note: The point of this task is to get you thinking about the data that TravelGraph
needs to keep track of, by thinking through specific use cases of the graph. You may find that, after working through this and the next questions, you need to change some decisions you made. This is fine and the expected point of these tasks!
Draw out exactly how each new City
looks in the heap, what your proposed field(s) are for tg
, and draw arrows to indicate where the contents of these fields sit on the heap
Design Check Task: Now consider adding transit routes to tg
. Say your transport CSV contains the following lines:
Newport, Providence, 10, 65
Providence, Boston, 15, 83
One way to try to add these routes to tg
would be using the following method calls:
Adding to the previous diagram, draw out exactly how each new City
and new Transport
looks in the heap, list what your proposed field(s) are for tg
, and draw arrows to indicate where the contents of the fields in newProvBus
, provBosBus
and tg
sit on the heap
Design Check Task: Assess your work. Does this drawing make sense (i.e. are data that are supposed to be related actually related in the correct way in this diagram)? Why or why not? Hint: would you be able to compute a route between Newport and Boston using your graph?
If the answer is no, propose new fields(s) (and what types/data structures they are) and method(s) in TravelGraph
that connect the relevant objects in the correct ways. Revise the method calls that instantiate and add newProvBus
and provBosBus
to tg
to use your new method(s).
How can you ensure that newProvBus
and provBosBus
both point to the same "Providence" City in the heap?
Design Check Task: Based on your answers above, draw an updated association/containment diagram of how you want your classes to fit together. Also list any new fields and methods that you are proposing for the TravelGraph
class.
You may have noticed that path-finding algorithms like Dijkstra’s and the Travel Planner bear a striking resemblance to other software with maps and navigation like Google Maps, Uber, or DoorDash. In Travel Planner, we offer the user a set of choices upon which to prioritize (time, price, directness), but there are often scenarios where a programmer may want to pick one prioritization (or at least a default) to reflect the needs of the application or just for simplicity’s sake.
Here you will be answering a software engineering (SWE) interview style question! When most people think of SWE interview questions, they may imagine being asked coding questions like how to reverse a linked list. However, many companies also ask more design-oriented questions that look for your understanding of tradeoffs and how to design software. This question in particular is inspired by a platform used by Lyft for their interviews. Even if you do not intend to do SWE interviews, this exercise is useful for reasoning through how and why you might make certain design decisions and the pros and cons of your decisions.
Note: For all the tasks in this section, you and your partner should work together and submit one answer as a group.
Design Check Task: Read through these two possible design proposals below for a food delivery app (think DoorDash, Postmates, UberEats, etc.). This app will feature local restaurants that customers can order from and have their food delivered to them by the app’s drivers. These proposals outline two different options for how the app will decide which routes and directions to give to delivery drivers.
Design Check Task: Pick one of the above proposals and defend it! Why is your chosen proposal better than the other one? How will your proposal benefit the application? Are there any concerns you have about your chosen proposal? List at least three reasons why you think yours makes the most sense and at least two hesitancies or concerns you have.
Note: If your hestancies and concerns apply to the other proposal as well, that’s fine!
One exercise that can be helpful when evaluating design decisions is creating a list of stakeholders. Stakeholders are people or groups of people who have an interest in or are affected by a system’s existence, operation, design, and impacts.
For example, stakeholders in CS200 include those directly involved in the class day-to-day: the professors, students, and TAs. Stakeholders also include those that aren’t directly involved in the class: the university, future CS students, others in the CIT during CS200 debugging and conceptual hours, etc. This list is not exhaustive, but it should give you a more concrete sense of what we mean when we say “stakeholders”.
Design Check Task: Create a list of stakeholders in the food delivery app! It will be helpful to think about both those who are directly involved and those who are not directly involved. Who will interact with the app? Who might be impacted by the app? Who might care about how the app operates?
After you have created your list take a look at the one we have made! It’s likely that neither our list nor yours is exhaustive, but pay attention to any differences. Is there anything that we have on ours that you do not? Is there anything you thought of that we didn’t?
Important Note: Please do not update your stakeholder list after reading ours. You will not be graded on the “correctness” or length of your list. As long as you’ve come up with at least a few examples of stakeholders on your own, you will receive full credit.
Design Check Task: Write about any differences you observed between your stakeholder list and ours. Why might you or we not have thought of certain stakeholders? Are there any on the list that surprised you? Are there any you don’t understand or disagree with?
Often when we talk about prioritization and tradeoffs in programming we are also talking about stakeholders. Different design choices will almost always favor some stakeholders over others. This question of who to prioritize becomes even more complex when we consider power dynamics between stakeholders. Here we’ll define power dynamics as how different people or groups of people interact when one of them has more power than the other or power over the other.
Going back to our CS200 example, select a few stakeholders from the list and for each consider the power dynamics between them and the other stakeholders. Do any have direct power over the others? Who has more control over the existence, operation, and design of the course? You don’t need to write anything down for this, but do take a moment to discuss with your partner how these power dynamics could affect whose needs or wants are prioritized.
Design Check Task: Now that you have learned about stakeholders and power dynamics, return to the proposal you chose for the food delivery app’s routing and directions algorithm.
Which stakeholders does your chosen proposal prioritize? What role might power dynamics between stakeholders play in choosing what the routing and directions algorithm prioritizes? Are there any stakeholders who would be unhappy with both proposals? What kind of harm or social costs comes with these prioritizations?
Please write a reflection of at least 8-10 sentences considering the questions above.
If you can’t think of who may be harmed, consider what could happen if our algorithm determines that a particular street is almost always included in the optimal path.
Design Check Task: We have shown you how to read in the cities CSV in the code. Based on this example, propose a method call and helper function object that will help you read in the transports CSV and add the relevant transports to the graph (it might help to think through the design decisions you made above for adding the Newport-Providence and Providence-Boston bus routes).
Design Check Task: Take a look at the DFS code we wrote in class (3/9 lecture). What needs to change in this code to write a BFS implementation that takes in an IGraph
? What data do you need to track at each step of BFS in order to make sure you're returning the correct output?
The code we wrote in class only tracked vertices, but in this project, we have both vertex (City
) and edge (Transit
) classes. How does that change the code we wrote in class? Think about the purpose of each step in the code, and think about how you will have to adapt it here.
In particular, think about the way we kept track of the path we went down for BFS/DFS (3/11 lecture). This algorithm worked because we assumed one possible edge going from one vertex to another. Does it make sense to use a HashMap from vertices to vertices for Travel Planner, or do you need to store something else to get back the exact edges? How would you use your chosen data structure to get back the route? It may help to draw out a concrete example here, just like we prompted you in the 3/11 handout/notes.
Design Check Task: Similarly, take a look at the Dijkstra pseudocode from class (3/11 lecture). What needs to be fleshed out in this pseudocode to make it compatible with an IGraph
? What data do you need to track at each step of the algorithm in order to make sure you're returning the correct output?
Think through the way you select an edge weight to compute on. How exactly will you use Function
objects and comparators here?
Design Check Task: Draw a small example graph and write out a testing plan for it, writing down the rationale for each test case as explained in the Testing section, above.
Note: While we are only requiring you to draw one example graph for the design check, we encourage you to draw a second one so that you can talk through it with your TA.
Please hand in your design check in the Project 2 - Design Check assignment in Gradescope before your design check.
Submit your final handin via Gradescope. Instructions on how to do so can be found in the Handing in Guide. When submitting with a partner, only one of you should submit the assignment and add your partner to the submission on Gradescope.
Make sure you submit your entire sol
and test
folders, which should contain your implementation and your tests. Specifically, sol
must contain:
BFS.java
Dijkstra.java
TravelController.java
TravelGraph.java
and test
must contain:
GraphTest.java
BFSTest.java
DijkstraTest.java
If you write your own classes (which aren't in the stencil code), you must put those in sol
as well.
Also, be sure to submit a .pdf of your testing plan along with the above files, as well as any custom .csv files you may have created.
Segment: One section (edge/Transport
) of a route. For example, if you go from New York City to Boston through Providence, there would be two segments, one from New York City to Providence, and one from Providence to Boston.
Source (of a segment): the city where the segment originates. For example, for the segment from Boston to Providence, Boston is the source.
Destination (of a segment): the city where the segment leads. For example, for the segment from Boston to Providence, Providence is the destination.
Route: An ordered sequence of segments, where the destination of one segment is the source of the subsequent segment. Saying "a route from New York to Providence" means that the first segment's source is New York and the last segment's destination is Providence.
What should I do first? You should start by thinking through the data structures and methods that you will need for your TravelGraph
. The design check provides a starting point, and we have provided you with the SimpleEdge
, SimpleVertex
, and SimpleGraph
classes in test/simple
. You can use these classes to test your graph design before setting up CSV parsing. Make sure your graph construction is working the way you expect it to before moving on!
Why does the ITravelController
load
method return a String? When using a REPL, all printing should be done within the actual REPL. Within the REPL code we provide, the load method you will override will be called, and the response that is returned will be printed. Therefore, you do not need to worry about printing at all, and instead, just return the response (whether that be an error or success message). The REPL also is in charge of transforming the paths that you compute in the other ITravelController
methods to a string, so you never need to worry about formatting/printing your paths.
Do I need to edit the files in the src
folder? Nope! For all of the interfaces provided, you will need to flesh out classes that implement these interfaces and override all of the declared methods. REPL.java
, TravelCSVParser.java
, City.java
, and Transport.java
are fully written for you, but it might be helpful to take a look at the methods in these classes, as some of them will be useful to you when coding your project. Lastly, TransportType.java
defines a helpful enum that you may use to represent the different types of transportation.
For Dijkstra's, how does the PriorityQueue
know how to order the vertices? The Java PriorityQueue
class has a constructor that takes in a Comparator. Refer to the Comparators lab for a detailed example of how to define and pass in a comparator to be used in a PriorityQueue
.
How do I make the same algorithms work for different ways of determining edge weights? You want to be able to calculate paths based on price, speed, and number of connections, but you don't want to have to write more than one Dijkstra's/BFS method just because you need to use different methods of determining edge weights. A great solution is using Java Function
s (see more in the sections above about Function
s). You can write different Function
s that get an edge's weight using different fields (price, time, etc.) and then pass the functions into your algo methods. This way, you can reuse the same methods for your different tasks.
The autograder is failing but the program is working on my computer! Help! Make sure you haven't edited any of the files in the src
folder, and make sure that you've implemented ALL methods from all of the provided interfaces in the classes you write. If you've done this and the autograder is failing to run but you think it should work, post on Ed / come to hours.
Please let us know if you find any mistakes, inconsistencies, or confusing language in this or any other CSCI0200 document by filling out the anonymous feedback form.