Returning Routes
Now that we have hasRoute working (and terminating!), let’s extend the problem to return the actual route to the given city. A route here will be a LinkedList of the nodes along the root, in order from the initial node to the named city.
Let’s start with the test cases.
manc.findRoute("Manchester") should return a list containing manc.
prov.findRoute("Hartford") should return a list containing prov and hart, in that order.
hart.findRoute("Providence") should return an empty list.
What about bost.findRoute("Boston")? How many possible answers are there? The route containing only Boston, the route that goes Boston-Worcester-Boston, the route that goes Boston-Worcester-Boston-Worcester-Boston, ... hmm. What do we do here?
One option would be to somehow use our test harness idea: we write a function that recognizes a valid route and make sure that we got back one of them. For example, for this particular case, we could say that a route is valid if it only contains Boston and Worcester.
You might suggest that we just expect the list containing only Boston, since we know how the code will work (we know from last class that it stops if it has already visited a city). Thoughts? This is an AWFUL approach. Why? Two reasons: (1) Test cases should be developed before you decide on the algorithm, which means (2) test cases should be independent of any particular implementation. The problem statement said "find a route". Our implementation would be justified in returning a route that cycles around Boston and Worcester several times.
Perhaps this is really telling us that our problem statement is too general. Let’s refine our goal to "find a route to the named city that does not include any city more than once" (this refined purpose should be included in the Javadocs for all relevant methods). This requires the route on bost.findRoute("Boston") to be the list containing only Boston.
What if we added a connection from Worcester to Hartford, then developed the test bost.findRoute("Hartford")? Now, we really do have two legitimate answers: one that goes through Providence and one that goes through Worcester. By the same reasoning as the previous case, our test case can’t dictate which of these answers to produce. Here, we would have no choice but to provide some sort of method to test whether the answer was valid. That method could check whether the actual answer was one of several expected concrete answers (in other words, it doesn’t have to give some abstract tests on the answer), but we still can’t propose a single answer up front.
With all that in hand, let’s actually write out the Java checkExpect statements for some tests:
boolean testhp (Tester t) { |
return t.checkExpect(hart.findRoute("Providence"), |
new LinkedList<Node>()); |
} |
|
boolean testbb (Tester t) { |
LinkedList<Node> expected = new LinkedList<Node>(); |
expected.add(bost); |
return t.checkExpect(bost.findRoute("Boston"), expected); |
} |
|
boolean testbw (Tester t) { |
LinkedList<Node> expected = new LinkedList<Node>(); |
expected.add(bost); |
expected.add(worc); |
return t.checkExpect(bost.findRoute("Worcester"), expected); |
} |
With our test cases in hand, let’s turn to writing the function. We start by simply renaming our previous code from hasRoute to findRoute (similarly for the helper methods) and changing the return type to a list of nodes:
LinkedList<Node> findRoute(String tocity) { |
return this.findRouteVisit(tocity, new LinkedList<Node>()); |
} |
|
private LinkedList<Node> findRouteVisit(String tocity, |
LinkedList<Node> visited) { |
if (visited.contains(this)) |
return false; |
else if (this.cityname.equals(tocity)) |
return true; |
else { |
visited.add(this); |
return this.findRouteConnects(tocity, visited); |
} |
} |
|
private LinkedList<Node> |
findRouteConnects(String tocity, LinkedList<Node> visited) { |
for (Node c : this.connects) { |
if (c.findRouteVisit(tocity, visited)) |
return true; |
} |
return false; |
} |
Clearly, this code won’t compile because it is returning booleans (the old return type) instead of lists. At the minimum, we need to replace all the boolean return values with appropriate lists.
The false cases are easiest: we used to return false when there was no route. We denote that now by returning an empty list. So we will edit each return false to return an empty list:
LinkedList<Node> findRoute(String tocity) { |
return this.findRouteVisit(tocity, new LinkedList<Node>()); |
} |
|
private LinkedList<Node> findRouteVisit(String tocity, |
LinkedList<Node> visited) { |
if (visited.contains(this)) |
return new LinkedList<Node>(); |
else if (this.cityname.equals(tocity)) |
return true; |
else { |
visited.add(this); |
return this.findRouteConnects(tocity, visited); |
} |
} |
|
private LinkedList<Node> |
findRouteConnects(String tocity, LinkedList<Node> visited) { |
for (Node c : this.connects) { |
if (c.findRouteVisit(tocity, visited)) |
return true; |
} |
return new LinkedList<Node>(); |
} |
This leaves updating the former true return values. Previously, we returned true when we had found a route that involved the current this node. The only difference now is that we have to return a route that includes this. When the cityname equals the target city, the whole route consists of the this node. The following version of findRouteVisit shows the code:
private LinkedList<Node> findRouteVisit(String tocity, |
LinkedList<Node> visited) { |
if (visited.contains(this)) |
return new LinkedList<Node>(); |
else if (this.cityname.equals(tocity)) { |
LinkedList<Node> prevRoute = new LinkedList<Node>(); |
prevRoute.addFirst(this); |
return prevRoute; |
} |
else { |
visited.add(this); |
return this.findRouteConnects(tocity, visited); |
} |
} |
private LinkedList<Node> |
findRouteConnects(String tocity, LinkedList<Node> visited) { |
for (Node c : this.connects) { |
LinkedList<Node> connectRoute = c.findRouteVisit(tocity, |
visited); |
if (connectRoute.size() > 0) { |
connectRoute.addFirst(this); |
return connectRoute; |
} |
} |
return new LinkedList<Node>(); |
} |
Both pieces of code illustrate a new concept in Java: defining local variables (prevRoute and connectRoute). The first line mentioning each of these variables gives it a type and an initial value. The variable is visible only within its local area of code (the else if body for prevRoute and the for body for connectRoute).
How do we argue that this program terminates? The argument is identical to that for hasRoute, since we didn’t change the traversal code, only the return value on each node.
1 Summary
From the graphs perspective, this problem mostly just gave another example of a program that traverses a graph. However, we used it to illustrate several other important points:
When writing tests for functions that return LinkedLists, your test cases have to get more complicated to build-up the expected answers.
When writing tests for functions that may return more than one answer, we need to write validity-methods, not single concrete answers, as part of our tests.
If a function purpose seems to allow more than one answer, ask whether you can reasonably refine the problem (in the case of replicated cities, we could; in the case of multiple routes, we could not).
How to write methods that return LinkedLists during traversals of complex data structures.
How to declare and initialize local variables in Java.