Are we there yet? We’ve been working on lots of individual ways to improve your software, but now it’s time to put it all together. This is it, what you’ve been waiting for: we’re going to take everything you’ve been learning, and show you how it’s all really part of a single process that you can use over and over again to write great software.
You’ve got a lot of new tools, techniques, and ideas about how to develop great software by now... but we still haven’t really put it all together.
That’s what this chapter is all about: taking all the individual things you know how to do—like figuring out requirements, writing up use cases, and applying design patterns—and turning it into a reusable process that you can use to tackle even your trickiest software problems, over and over again.
So what does that process look like?
Let’s build a software project, from start to finish, using this process.
We’ve used all of the different parts of this process to work on software projects throughout the book, but we haven’t yet really put it all together. But that’s all about to change... we’re going to let you build a pretty complex piece of software in this chapter, from feature list to implementation and delivery.
Along the way, you’ll see for yourself how all the things you’ve been learning really do help you build great software. Get ready, this will be your final test.
Here’s the project we’re going to work through in this chapter, from beginning to end:
This page left intentionally blank, so you can cut out the cool Objectville Map on the next page and hang it up on your cubicle wall.
With a feature list in hand, you should have a good understanding of the things that your app needs to do. You probably even can begin to think about the structure of your application, although we’ll spend a lot more time on that in just a bit.
Once you’ve got your feature list down, you should move on to use case diagrams. Those will help you connect what your app does to how it will be used—and that’s what customers really are interested in.
Your feature lists are all about understanding what your software is supposed to do.
Your use case diagrams let you start thinking about how your software will be used, without getting into a bunch of unnecessary details.
Let’s look more closely at one of the feature-use case matches we showed you on the last page:
The “Load network of subway lines” use case really does not directly use our feature that deals with representing the subway. Obviously, we have to be able to represent the subway for this use case to work, but it’s a stretch to tie the two together like this.
That’s not a mistake, though; when we’re writing use cases, we’re dealing with just the interactions between actors and a system. We’re just talking about the ways that your system is used (which is where the term “use case” came from).
The features in your system reflect your system’s functionality. Your system must do those things in order for the use cases to actually work, even though the functionality isn’t always an explicit part of any particular use case.
Once you’ve broken up your software into several individual pieces of functionality, you’re ready to start iterating over each piece of functionality, until the application is complete.
At this point, we need to take our big-picture view of the system, from our use case diagram, and refine that into requirements that we can begin to tackle, one by one. For this first iteration, let’s take the “Load network of subway lines” use case, and turn that into a set of requirements that isn’t so big-picture. Then we can take care of that use case, and iterate again, working on the next use case.
Not too sure about this use case? It’s OK... turn the page for some help...
You’ll often have to do some extra work between breaking up the problem and writing your use cases.
You’ve just uncovered a “secret step” in our process for writing software the OOA&D way:
If you get stuck writing a use case, there’s nothing wrong with taking a step back, and examining the problem you’re trying to solve a bit. Then, you can go back to your use case, and have a better chance of getting it written correctly.
Before we can figure out how to load a subway line, there are two things we need to get a good grasp of:
Understanding the basics of what a subway system is.
Understanding the information an administrator would have when they’re loading a set of subway stations and lines.
A subway system has stations, and connections between those stations, and lines that are groups of connections. So let’s begin by figuring out exactly what a station is.
As soon as you start adding several stations, you’ve got to deal with the connections between those stations:
We’ve got a basic idea now of what a subway is, so let’s see what kind of data we’ve got to work with. Remember, Objectville Travel said they would send us a file with all the stations and lines, so this should give us an idea of what an administrator will use to load the lines into the subway system.
The use case for loading a network is a little tricky, and has several groups of steps that repeat. Let’s check the flow of things against our text file before we go on to analyzing the use case, and starting to design the classes in our system.
It was pretty easy to look at our use case and figure out that we need a Station
, Connection
, and Subway
class—those are fundamental to our system. But then we decided to not create a Line
class. Instead, we just assigned a line name to each connection:
We made this decision based on one thing: we know how the system is going to be used. In the original Statement of Work (back in The problem) from Objectville Travel, we were told we needed to represent a subway, and get directions between one station and another. Once we have those directions, we can simply ask each connection for its line; there doesn’t seem to be a need for an actual Line
class.
Your design decisions should be based on how your system will be used, as well as good OO principles.
We’ve got requirements in the form of a use case, a class diagram, and we know the Station
class will fit into our Subway
model. Now we’re ready to start writing code:
Next up is the Subway
class itself. With Station
and Connection
done, and a good class diagram, nothing here should be a surprise:
We threw a couple of new things into the Subway
class; first, you’ll see this line of code quite a bit:
Station station = new Station(stationName);
For example, when we create a new Connection
, we have code like this:
Station station1 = new Station(station1Name); Station station2 = new Station(station2Name); Connection connection = new Connection(station1, station2, lineName);
Lots of programmers would take station1Name
, and iterate through the list of stations in the Subway
class to find the Station
object that has a name of station1Name
. But that takes a lot of time, and there’s a better way. Remember how we defined an equals()
and hashCode()
method on our Station
class?
These methods allowed us to tell Java that when it compares two Station
objects, just see if their name is the same. If the names are equal, even if the objects don’t refer to the same location in memory, they should be treated as the same. So instead of looking for a particular Station object in the Subway class’s list of station, it’s much easier to just create a new Station and use it.
Normally, equals() in Java just checks to see if two objects actually are the SAME object... in other words, it looks to see if they are actually both references to the same place in memory. But that’s NOT what we want to use for comparison of two Station objects
Because we overrode equals() and hashCode(), we can save search time and complexity in our code. Your design decisions should always make your implementation better, not more complicated or harder to understand.
Frank: That’s true. So we could change our Subway class to look more like this:
Jill: But then you’re exposing the internals of your application!
Joe: Whoa... not sure what that means, but it sure doesn’t sound like something I want to do. What are you talking about?
Jill: Well, look at our code right now. You don’t have to work with a Station or Connection at all to load up the subway. You can just call methods on our new Subway class.
Frank: How is that any different from what we’re suggesting?
Jill: If we went with your ideas, people that use the Subway class would have to also work with Station and Connection. In our version right now, they just work with Strings: the name of a station, and the name of a line.
Joe: And that’s bad because...
Frank: Wait, I think I get it. Their code is getting tied in to how we implement the Station and Connection classes, since they’re having to work with those classes directly.
Jill: Exactly! But with our version, we could change up Connection or Station, and we’d only have to change our Subway class. Their code would stay the same, since they’re abstracted away from our implementations of Connection and Station.
Frank, Joe, and Jill are really talking about just one more form of abstraction. Let’s take a closer look:
You should only expose clients of your code to the classes that they NEED to interact with.
Classes that the clients don’t interact with can be changed with minimal client code being affected.
Note
In this application, we could change how Station and Connection work, and it wouldn’t affect code that only uses our Subway object; they’re protected from changes to our implementation
We’re almost done with our first iteration, and our first use case. All that’s left is to code the class that loads a subway based on the test file we got from Objectville Travel, Inc.
Our test proves that we really have finished up our first iteration. The “Load network of subway lines” use case is complete, and that means it’s time to iterate again. Now, we can take on our next use case—“Get directions”—and return to the Requirements phase and work through this use case.
It’s been a LONG iteration, and you’ve done some great work. STOP, take a BREAK, and eat a bite or drink some water. Give your brain a chance to REST.
Then, once you’ve caught your breath, turn the page, and let’s knock out that last use case. Are you ready? Then let’s iterate again.
We’ve made a lot of progress, on both our use cases, and our feature list. Below is the feature list and use case diagram we developed earlier in the chapter:
Now that we’re ready to take on the next use case, we have to go back to the requirements phase, and work through this use case the same way we did the first one. So we’ll start by taking our use case title from our use case diagram, “Get directions,” and developing that into a full-blown use case.
When we started breaking our application up into different modules way back in The Big Break-Up, Solved, we were really talking about the structure of our application, and how we are going to break up our application. We have a Subway
and Station
class in the Subway module, and a SubwayLoader
class in the Loader module, and so on. In other words, we’re focusing on our code.
But when we’re working on use cases, we’re focusing on how the customer uses the system—we looked at the format of an input file to load lines, and began to focus on the customer’s interaction with your system. So we’ve really been going back and forth between our code (in the Break Up the Problem step) and our customer (in the Requirements step):
When you’re developing software, there’s going to be a lot of this back-and-forth. You have to make sure your software does what it’s supposed to, but it’s your code that makes the software actually do something.
It’s your job to balance making sure the customer gets the functionality they want with making sure your code stays flexible and well-designed.
The class diagram on the last page really isn’t that much different from our class diagram from the first iteration (flip back to Let’s see if our use case works to take a look at that earlier version). That’s because we did a lot of work that applies to all our iterations during our first iteration.
Once you’ve completed your first iteration, your successive iterations are often a lot easier, because so much of what you’ve already done makes those later iterations easier.
Figuring out a route between two stations turns out to be a particularly tricky problem, and gets into some of that graph stuff we talked about briefly back in There are no Dumb Questions. To help you out, we’ve included some Ready-bake Code that you can use to get a route between two stations.
Ready-bake Code
Ready-bake Code
Sometimes the best way to get the job done is find someone else who has already done the job for you.
It might seem weird that at this stage, we’re giving you the code for getting a route between two stations. But that’s part of what makes a good developer: a willingness to look around for existing solutions to hard problems.
In fact, we had some help from a college student on implementing a version of Dijkstra’s algorithm that would work with the subway (seriously!). Sure, you probably can come up with your own totally original solution to every problem, but why would you want to if someone has already done the work for you?
Sometimes the best code for a particular problem has already been written. Don’t get hung up on writing code yourself if someone already has a working solution.
The getDirections()
method we just added to Subway
takes in two String
s: the name of the starting station, and the name of the station that a tourist is trying to get to:
getDirections()
then returns a List
, which is filled with Connection
objects. Each Connection
is one part of the path between the two stations:
So the entire route returned from getDirections()
looks like a series of Connection
objects:
All that we need to do now is put everything together. Below is the SubwayTester
class we wrote to load the Objectville Subway system, take in two stations from the command line, and print out directions between those two stations using our new getDirections()
method and printer class.
It’s time to sit back and enjoy the fruits of your labor. Compile all your classes for the Objectville Subway application, and try out SubwayTester with a few different starting and stopping stations. Here’s one of our favorites:
No, we’re not going to launch into any more design problems. But, you should realize that there is plenty more that you could do to improve the design of our RouteFinder application. We thought we’d give you just a few suggestions, in case you’re dying to take another pass through the OOA&D lifecycle.
Right now, we’ve just got a single class that handles loading, and it only accepts a Java File
as input. See if you can come up with a solution that allows you to load a subway from several types of input sources (try starting with a File
and InputStream
). Also make it easy to add new input sources, like a database. Remember, you want to minimize changes to existing code when you’re adding new functionality, so you may end up with an interface or abstract base class before you’re through.
We can only print subway directions to a file, and the directions are formatted in a particular way. See if you can design a flexible Printing module that allows you to print a route to different output sources (like a File
, OutputStream
, and Writer
), in different formats (perhaps a verbose form that matches what we’ve done already, a compact form that only indicates where to changes lines, and an XML form for other programs to use in web services).
We’ve loved having you here in Objectville, and we’re sad to see you go. But there’s nothing like taking what you’ve learned and putting it to use on your own development projects. So don’t stop enjoying OOA&D just yet... you’ve still got a few more gems in the back of the book, an index to read through, and then it’s time to take all these new ideas and put them into practice. We’re dying to hear how things go, so drop us a line at the Head First Labs web site, http://www.headfirstlabs.com, and let us know how OOA&D is paying off for YOU.