Stage | Artifacts Created |
---|---|
Analysis | Requirements |
Design | Functional Specification |
Implementation | Source code |
Testing | Test cases, bug reports, certification |
Maintenence | bug reports, feature requests |
The first stage involves an analysis of the problem domain, which requires careful thinking and investigation of the problem to be solved. It usually results in a requirements document, which is a description of what problem is being solved. The design phase starts with this document and sketches out a design, a blueprint, for a system that should be able to solve the stated problem. Given a design specification, programmers write source code, during the implementation phase of a project. This source code can then be tested, to help assure that it meets the original specification. Finally, if the program is deemed to behave correctly during the test phase, it is released into the field, at which point maintenence begins. Problems may arise in practice that were not discovered in testing, or perhaps users want small features to be added to the program.
This model of the software lifecycle was originally known as the waterfall model, because information and workflow was seen to move in one direction only -- from analysis through to deployment and maintinence. This unidirectional flow and rigid reliance on sharp boundaries between the various phases has been shown to be unworkable and ineffective in practice, especially with large scale software projects. We'll explore some of the reasons for its failure and improvements to the process in a later lesson. For now, however, it is still worth knowing the basic stages of the software lifecycle because it gives us a language for discussing the software development process.
Introductory programming courses typically focus primarily on the implementation phase of a piece of software. Usually, a specification is provided in the form of a homework assignment ("Write a program that does this and that. It should have the following pieces. Etc.") and the end goal is to produce a program that meets the given specification. This is unfortunate because it makes programming seem like an incredibly boring and uncreative task. Further, it seems to suggest that the programmers responsibilities begin with the specification and end with the testing phase. In reality, good programmers must understand the practices and issues faced by all phases of the lifecycle, even if they don't actively participate in those phases.
Let's think about compact disks for a moment. What are their properties? Here are a few: total running time, artist, year published, and the songs themselves. Let's go a level deeper. What about songs? Each song has some properties: the music (which is some sort of audio data), the name of the song, the length of the song.
Without knowing it, we've just done an analysis of the music collection domain. If we want to model it we need to define some objects for each of the main concepts. What are the concepts? Finding the right concepts takes experience and practice, but by and large, they fall directly out of our above analysis. Look at the nouns (objects) in the analysis:
Music collection, CD, time, artist name, date, audio data.Next, let's think about how the above objects are related to each other.
Song has a running time. Song has a name. Song has an audio data sample. CD has a collection of Songs. CD has a running time. CD has a name. CD has an artist. MusicCollection has a collection of CDsWe have just completed an initial analysis of the music collection domain. Are there any problems with it? Well, maybe a recording has more than one artist, so perhaps it's better to make the artist be associated with the Song. Furthermore, CDs usually contain liner notes, a publication date, and perhaps information about label, producer, and engineering information. Ultimately, we would probably want to include these properties, but for the sake of simplicity, let's just stick with our initial analysis.
The analysis will often hint at both the representation as well as the interface of the classes we want to define. It's usually a good strategy to delay worrying about representation and implementation as long as possible. Sketching the behavior of objects and imagining their interactions is much easier and flexible than actually writing the actual code for them. Good programmers build worlds in their imaginations before building them in a program.
Let's imagine some of the operations that our Song class should support. Again, let's think about what we can do with real world songs.
public class Song { // Create a new song // Get the running time of this song // Get the title of this song // Play this song onto an audio player }Notice that all we have so far is the basic class structure and some comments, but it's a good starting point. In many cases, these comments will be even more formal -- they will specify to a high degree of detail the behavior of the methods we are going to want to write. The operations we wish to provide hint at the state that each Song has to maintain: some notion of song length, title, and the audio data for the song itself. This allows us to add the instance variables to our class definition.
public class Song { private int seconds; private Sound sound; private String name; // Create a new song // Get the running time of this song // Get the title of this song // Play this song onto an audio player }Now, writing at constructor and the first two methods is pretty trivial.
public class Song { private int seconds; private Sound sound; private String name; // Create a new song public Song(String title, String lengthInSeconds, Sound theSound) { this.seconds = lengthInSeconds; this.sound = theSound; this.name = title; } // Get the running time of this song public int getLength() { return this.seconds; } // Get the title of this song public String getTitle() { return this.name; } // Play this song onto an audio player }What about playing the song? Let's imagine that we know about a class that specializes in playing Sound objects. It might have an interface that looks something like this:
class AudioPlayer { ... // Play the provided sound onto the audio device. public void playSound(Sound aSound); }Based on this interface, we can complete our implementation of the Song object. Playing a song is just a matter of passing the song's sound to a given AudioPlayer object, yielding a complete implementation of our Song class.
public class Song { private int seconds; private Sound sound; private String name; // Create a new song public Song(String title, String lengthInSeconds, Sound theSound) { this.seconds = lengthInSeconds; this.sound = theSound; this.name = title; } // Get the running time of this song public int getLength() { return this.seconds; } // Get the title of this song public String getTitle() { return this.name; } // Play this song onto an audio player public void play(AudioPlayer player) { player.play(sound); } }We've just implemented a class that we can use to represent songs. In future chapters, we'll see how to implement some of the other classes in our system, such as MusicCollection and CompactDisc.