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.
1 Test Cases
Let’s start with the test cases.
G.findRoute(manc,manc) should return a list containing manc.
G.findRoute(prov,hart) should return a list containing prov and hart, in that order.
How about G.findRoute(hart,prov)? There is no route from Hartford to Providence, so the question is how we represent that there is no route. We could return an empty list. We could throw an exception. The latter doesn’t feel right though, because exceptions go with unexpected cases, and it’s perfectly reasonable to ask for a route and find that none exists. Empty list doesn’t seem quite right either, since the use of a list suggests there is a route. For now, let’s use an empty list for the non-route case. We’ll revisit that decision in a couple of days.
What about G.findRoute(bost,bost)? 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 requires the route on G.findRoute(bost,bost) to be the list containing only Boston.
What if we added a connection from Worcester to Hartford, then developed the test G.findRoute(bost,hart)? 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.
2 The FindRoute Method
Having discussed the test cases, let’s turn to writing the method. We will initially write this without worrying about termination, just to illustrate the heart of the computation. The method returns a list containing just the this node if this and to are equal, otherwise it searches from the getsTo node. If one of the getsTo nodes yields a route, the method adds this to the front of the route and returns it. If none of the getsTo nodes yields a route, the method returns an empty list (denoting the lack of a route).
class Node { |
private String cityname; // name of city at this node |
private LinkedList<Node> getsTo; // edges from this Node |
|
// produces a route from this node to the to Node, if one exists. |
// The route should not include the same city more than once |
LinkedList<Node> findRoute(Node to) { |
// if this is the node we are searching for, |
// return a route with one node |
if (this.equals(to)) { |
LinkedList<Node> newRoute = new LinkedList<Node>(); |
newRoute.addFirst(this); |
return newRoute; |
} |
// search for route through the getsTo nodes |
else { |
for (Node c : this.getsTo) { |
LinkedList<Node> cRoute = c.findRoute(to); |
if (cRoute.size() > 0) { |
cRoute.addFirst(this); |
return cRoute; |
} |
} |
// no route from children, so return empty list |
return new LinkedList<Node>(); |
} |
} |
} |
Note that this code is remarkably similar to that for hasRoute: the main difference lies in building and returning a LinkedList of Node rather than a boolean.
In order to make this code terminate, we use a visited parameter, just as we did for hasRoute. The code involving visited is the same in both methods:
class Node { |
private String cityname; // name of city at this node |
private LinkedList<Node> getsTo; // edges from this Node |
|
// produces a route from this node to the to Node, if one exists. |
// The route should not include the same city more than once |
LinkedList<Node> findRoute(Node to, LinkedList<Node> visited) { |
// if have tried this node before, there is no route |
if (visited.contains(this)) { |
return new LinkedList<Node>(); |
} |
// if this is the node we are searching for, |
// return a route with one node |
else if (this.equals(to)) { |
LinkedList<Node> newRoute = new LinkedList<Node>(); |
newRoute.addFirst(this); |
return newRoute; |
} |
// search for route through the getsTo nodes |
else { |
visited.add(this); |
for (Node c : this.getsTo) { |
LinkedList<Node> cRoute = c.findRoute(to,visited); |
if (cRoute.size() > 0) { //effectively a typecheck |
cRoute.addFirst(this); |
return cRoute; |
} |
} |
// no route from children, so return empty list |
return new LinkedList<Node>(); |
} |
} |
} |
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.
3 Cleaning Up FindRoute
The current code works fine, but the method is starting to get a bit long and hard to read. Remember that code needs to be readable by whoever has to maintain it several months from now (could be you, could be another programmer).
To improve the readability a bit, we will move the bulk of the else case that searches for routes via edges into another method, as follows:
class Node { |
private String cityname; // name of city at this node |
private LinkedList<Node> getsTo; // edges from this Node |
|
// produces a route from this node to the to Node, if one exists |
// The route should not include the same city more than once |
LinkedList<Node> findRoute(Node to, LinkedList<Node> visited) { |
// if have tried this node before, there is no route |
if (visited.contains(this)) { |
return new LinkedList<Node>(); |
} |
// if this is the node we are searching for, |
// return a route with one node |
else if (this.equals(to)) { |
LinkedList<Node> newRoute = new LinkedList<Node>(); |
newRoute.addFirst(this); |
return newRoute; |
} |
// search for route through the getsTo nodes |
else { |
visited.add(this); |
return this.findRouteEdges(to,this,visited); |
} |
} |
|
// checks for a route using the edges out of getsTo |
private LinkedList<Node> |
findRouteEdges(Node to, LinkedList<Node> visited) { |
for (Node c : this.getsTo) { |
LinkedList<Node> cRoute = c.findRoute(to,visited); |
if (cRoute.size() > 0) { |
cRoute.addFirst(this); |
return cRoute; |
} |
} |
// no route from children, so return empty list |
return new LinkedList<Node>(); |
} |
} |
Note that findRouteEdges has the same contents as the else statement in the previous statement. In creating this separate method, however, we have done two things:
We have made the code in findRoute more self-describing, since the method name findRouteEdges suggests what the algorithm is doing.
We have put the processing of the LinkedList data structure into its own method. Had we been using a class rather than a built-in LinkedList to capture edges, the code for processing edges would be in its own method anyway. Separating the code in this way follows the "one function per data structure/data definition" guideline that we taught in CS1101/1102.
Note also that we made findRouteEdges private, as it is not meant to be called from outside the Node class. Put differently, it is purely a helper method to findRoute.
4 Critiquing the FindRoute Code
Now that we have a working version of findRoute, let’s step back and critique it. What could be improved about this code?
Using an empty list for a non-existant route feels like a bit of a cheat. Since there is no route, there is no need for a list. While using an empty list satisfies the compiler, it doesn’t really convey the lack of a route.
The code fixes the representation of a route at a LinkedList, rather than encapsulate the notion of a route. An encapsulated route could hold additional attributes of a route, such as modes of transport needed to take it, whether it was scenic, etc.
The check of size in findRouteEdges is really a typecheck on which kind of route we got back. That violates the spirit of OO programming.
Next up, we’ll work on addressing these issues.