Principles of Software Engineering - 2Pages

profilewillymax93
CHAPTER1CreatingaProgram.docx

CHAPTER 1: Creating a Program

OBJECTIVES

· Analyze some of the issues involved in producing a simple program:

· Requirements (functional, nonfunctional)

· Design constraints and design decisions

· Testing

· Effort estimation

· Implementation details

· Understand the activities involved in writing even a simple program.

· Preview many additional software engineering topics found in the later chapters.

1.1 A Simple Problem

In this chapter we will analyze the tasks involved in writing a relatively simple program. This will serve as a contrast to what is involved in developing a large system, which is described in  Chapter 2 .

Assume that you have been given the following simple problem: “Given a collection of lines of text (strings) stored in a file, sort them in alphabetical order, and write them to another file.” This is probably one of the simplest problems you will be involved with. You have probably done similar assignments for some of your introduction to programming classes.

1.1.1 Decisions, Decisions

A problem statement such as the one mentioned in the above simple problem does not completely specify the problem. You need to clarify the requirements in order to produce a program that better satisfies the real problem. You need to understand all the  program requirements  and the  design constraints  imposed by the client on the design, and you need to make important technical decisions. A complete problem statement would include the requirements, which state and qualify what the program does, and the design constraints, which depict the ways in which you can design and implement it.

Program requirements Statements that define and qualify what the program needs to do.

Design constraints Statements that constrain the ways in which the software can be designed and implemented.

The most important thing to realize is that the word requirements is not used as it is in colloquial English. In many business transactions, a requirement is something that absolutely must happen. However, in software engineering many items are negotiable. Given that every requirement will have a cost, the clients may decide that they do not really need it after they understand the related cost. Requirements are often grouped into those that are “needed” and those that are “nice to have.”

It is also useful to distinguish between  functional requirements —what the program does—and  nonfunctional requirements —the manner in which the program must behave. In a way, a function is similar to that of a direct and indirect object in grammar. Thus the functional requirements for our problem will describe what it does: sort a file (with all the detail required); the nonfunctional requirements will describe items such as performance, usability, and maintainability. Functional requirements tend to have a Boolean measurement where the requirement is either satisfied or not satisfied, but nonfunctional requirements tend to apply to things measured on a linear scale where the measurements can vary much more. Performance and maintainability requirements, as examples, may be measured in degrees of satisfaction.

Functional requirements What a program needs to do.

Nonfunctional requirements The manner in which the functional requirements need to be achieved.

Nonfunctional requirements are informally referred as the “ilities,” because the words describing most of them will end in -ility. Some of the typical characteristics defined as nonfunctional requirements are performance, modifiability, usability, configurability, reliability, availability, security, and scalability.

Besides requirements, you will also be given design constraints, such as the choice of programming language, platforms the system runs on, and other systems it interfaces with. These design constraints are sometimes considered nonfunctional requirements. This is not a very crisp or easy-to-define distinction (similar to where requirement analysis ends and design starts); and in borderline cases, it is defined mainly by consensus. Most developers will include usability as a nonfunctional requirement, and the choice of a specific user interface such as graphical user interface (GUI) or web-based as a design constraint. However, it can also be defined as a functional requirement as follows: “the program displays a dialog box 60 by 80 pixels, and then . . .”

Requirements are established by the client, with help from the software engineer, while the technical decisions are often made by the software engineer without much client input. Oftentimes, some of the technical decisions such as which programming languages or tools to use can be given as requirements because the program needs to interoperate with other programs or the client organization has expertise or strategic investments in particular technologies.

In the following pages we will illustrate the various issues that software engineers confront, even for simple programs. We will categorize these decisions into functional and nonfunctional requirements, design constraints, and design decisions. But do keep in mind that other software engineers may put some of these issues into a different category. We will use the simple sorting problem presented previously as an example.

1.1.2 Functional Requirements

We will have to consider several aspects of the problem and ask many questions prior to designing and programming the solution. The following is an informal summary of the thinking process involved with functional requirements:

· Input formats: What is the format for the input data? How should data be stored? What is a character? In our case, we need to define what separates the lines on the file. This is especially critical because several different platforms may use different separator characters. Usually some combination of new-line and carriage return may be considered. In order to know exactly where the boundaries are, we also need to know the input character set. The most common representation uses 1 byte per character, which is enough for English and most Latin-derived languages. But some representations, such as Chinese or Arabic, require 2 bytes per character because there are more than 256 characters involved. Others require a combination of the two types. With the combination of both single- and double-byte character representations, there is usually a need for an escape character to allow the change of mode from single byte to double byte or vice versa. For our sorting problem, we will assume the simple situation of 1 byte per character.

· Sorting: Although it seems to be a well-defined problem, there are many slightly and not so slightly different meanings for sorting. For starters—and of course, assuming that we have English characters only—do we sort in ascending or descending order? What do we do with nonalphabetic characters? Do numbers go before or after letters in the order? How about lowercase and uppercase characters? To simplify our problem, we define sorting among characters as being in numerical order, and the sorting of the file to be in ascending order.

· Special cases, boundaries, and error conditions: Are there any special cases? How should we handle boundary cases such as empty lines and empty files? How should different error conditions be handled? It is common, although not good practice, to not have all of these requirements completely specified until the detailed design or even the implementation stages. For our program, we do not treat empty lines in any special manner except to specify that when the input file is empty the output file should be created but empty. We do not specify any special error-handling mechanism as long as all errors are signaled to the user and the input file is not corrupted in any way.

1.1.3 Nonfunctional Requirements

The thinking process involved in nonfunctional requirements can be informally summarized as follows:

· Performance requirements: Although it is not as important as most people may think, performance is always an issue. The program needs to finish most or all inputs within a certain amount of time. For our sorting problem, we define the performance requirements as taking less than 1 minute to sort a file of 100 lines of 100 characters each.

· Real-time requirements: When a program needs to perform in real-time, which means it must complete the processing within a given amount of time, performance is an issue. The variability of the running time is also a big issue. We may need to choose an algorithm with a less than average performance, if it has a better worst-case performance. For example, Quick Sort is regarded as one of the fastest sorting algorithms; however, for some inputs, it can have poor performance. In algorithmic terms, its expected running time is on the order of n log(n), but its worst-case performance is on the order of n squared. If you have real-time requirements in which the average case is acceptable but the worst case is not, then you may want to choose an algorithm with less variability, such as Heap Sort or Merge Sort. Run-time performance analysis is discussed further in  Main and Savitch (2010) .

· Modifiability requirements: Before writing a program, it is important to know the life expectancy of the program and whether there is any plan to modify the program. If the program is to be used only once, then modifiability is not a big issue. On the other hand, if it is going to be used for 10 years or more, then we need to worry about making it easy to maintain and modify. Surely, the requirements will change during that 10-year period. If we know that there are plans to extend the program in certain ways, or that the requirements will change in specific ways, then we should prepare the program for those modifications as the program is designed and implemented. Notice that even if the modifiability requirements are low, this is not a license to write bad code, because we still need to be able to understand the program for debugging purposes. For our sorting example, consider how we might design and implement the solution if we know that down the road the requirement may change from descending to ascending order or may change to include both ascending and descending orders.

· Security requirements: The client organization and the developers of the software need to agree on security definitions derived from the client’s business application goals, potential threats to project assets, and management controls to protect from loss, inaccuracy, alteration, unavailability, or misuse of the data and resources. Security might be functional or nonfunctional. For example, a software developer may argue that a system must protect against denial-of-service attacks in order to fulfill its mission. Security quality requirements engineering (SQUARE) is discussed in  Mead and Stehney (2005) .

· Usability requirements: The end users for the program have specific background, education, experience, needs, and interaction styles that are considered in the development of the software. The user, product, and environmental characteristics of the program are gathered and studied for the design of the user interface. This nonfunctional requirement is centered in the interaction between the program and the end user. This interaction is rated by the end user with regards to its effectiveness, efficiency, and success. Evaluation of usability requirements is not directly measurable since it is qualified by the usability attributes that are reported by the end users in specific usability testing.

1.1.4 Design Constraints

The thinking process related to design constraints can be summarized as follows:

· User interface: What kind of  user interface  should the program have? Should it be a command-line interface (CLI) or a graphical user interface (GUI)? Should we use a web-based interface? For the sorting problem, a web-based interface doesn’t sound appropriate because users would need to upload the file and download the sorted one. Although GUIs have become the norm over the past decade or so, a CLI can be just as appropriate for our sorting problem, especially because it would make it easier to invoke inside a script, allowing for automation of manual processes and reuse of this program as a module for future ones. This is one of those design considerations that also involves user interface. In Section 1.4, we will create several implementations, some CLI based and some GUI based.  Chapter 7  also discusses user-interface design in more detail.

· Typical and maximum input sizes: Depending on the typical input sizes, we may want to spend different amounts of time on algorithms and performance optimizations. Also, certain kinds of inputs are particularly good or bad for certain algorithms; for example, inputs that are almost sorted make the naive Quick Sort implementations take more time. Note that you will sometimes be given inaccurate estimates, but even ballpark figures can help anticipate problems or guide you toward an appropriate algorithm. In this example, if you have small input sizes, you can use almost any sorting algorithm. Thus you should choose the simplest one to implement. If you have larger inputs but they can still fit into the random access memory (RAM), you need to use an efficient algorithm. If the input does not fit on RAM, then you need to choose a specialized algorithm for on-disk sorting.

· Platforms: On which platforms does the program need to run? This is an important business decision that may include architecture, operating system, and available libraries and will almost always be expressed in the requirements. Keep in mind that, although cross-platform development has become easier and there are many languages designed to be portable across platforms, not all the libraries will be available in all platforms. There is always an extra cost on explicitly supporting a new platform. On the other hand, good programming practices help achieve portability, even when not needed. A little extra consideration when designing and implementing a program can minimize the potentially extensive work required to port to a new platform. It is good practice to perform a quick cost-benefit analysis on whether to support additional platforms and to use technologies and programming practices that minimize portability pains, even when the need for supporting new platforms is not anticipated.

· Schedule requirements: The final deadline for completing a project comes from the client, with input from the technical side on feasibility and cost. For example, a dialogue on schedule might take the following form: Your client may make a request such as “I need it by next month.” You respond by saying, “Well, that will cost you twice as much than if you wait two months” or “That just can’t be done. It usually takes three months. We can push it to two, but no less.” The client may agree to this, or could also say, “If it’s not done by next month, then it is not useful,” and cancel the project.

User interface What the user sees, feels and hears from the system.

1.1.5 Design Decisions

The steps and thoughts related to design decisions for the sorting problem can be summarized as follows:

· Programming language: Typically this will be a technical design decision, although it is not uncommon to be given as a design constraint. The type of programming needed, the performance and portability requirements, and the technical expertise of the developers often heavily influence the choice of the programming language.

· Algorithms: When implementing systems, there are usually several pieces that can be influenced by the choice of algorithms. In our example, of course, there are a variety of algorithms we can choose from to sort a collection of objects. The language used and the libraries available will influence the choice of algorithms. For example, to sort, the easiest solution would be to use a standard facility provided by the programming language rather than to implement your own. Thus, use whatever algorithm that implementation chooses. Performance will usually be the most important influence in the choice of an algorithm, but it needs to be balanced with the effort required to implement it, and the familiarity of the developers with it. Algorithms are usually design decisions, but they can be given as design constraints or even considered functional requirements. In many business environments there are regulations that mandate specific algorithms or mathematical formulas to be used, and in many scientific applications the goal is to test several algorithms, which means that you must use certain algorithms.

1.2 Testing

It is always a good idea to test a program, while it is being defined, developed, and after it is completed. This may sound like obvious advice, but it is not always followed. There are several kinds of testing, including acceptance testing, which refers to testing done by clients, or somebody on their behalf, to make sure the program runs as specified. If this testing fails, the client can reject the program. A simple validation test at the beginning of the project can be done by showing hand-drawn screens of the “problem solution” to the client. This practice solidifies your perception of the problem and the client’s solution expectations. The developers run their own internal tests to determine if the program works and is correct. These tests are called verification tests. Validation tests determine whether the developers are building the correct system for the client, and verification tests determine if the system build is correct.

Although there are many types of testing performed by the development organization, the most important kind of verification testing for the individual programmer is unit testing—a process followed by a programmer to test each piece or unit of software. When writing code, you must also write tests to check each module, function, or method you have written. Some methodologies, notably Extreme Programming, go as far as saying that programmers should write the test cases before writing the code; see the discussion on Extreme Programming in  Beck and Andres (2004) . Inexperienced programmers often do not realize the importance of testing. They write functions or methods that depend on other functions or methods that have not been properly tested. When a method fails, they do not know which function or method is actually failing.

Another useful distinction is between black-box and white-box testing. In black-box testing, the test cases are based only on the requirement specifications, not on the implementation code. In white-box testing, the test cases can be designed while looking at the design and code implementation. While doing unit testing, the programmer has access to the implementation but should still perform a mixture of black-box and white-box testing. When we discuss implementations for our simple program, we will perform unit testing on it. Testing will be discussed more extensively in  Chapter 10 .

1.3 Estimating Effort

One of the most important aspects of a software project is estimating how much effort it involves. The effort estimate is required to produce a cost estimate and a schedule. Before producing a complete effort estimate, the requirements must be understood. An interesting exercise illustrates this point.

Try the following exercise:

Estimate how much time, in minutes, it will take you, using your favorite language and technology, to write a program that reads lines from one file and writes the sorted lines to another file. Assume that you will be writing the sort routine yourself and will implement a simple GUI like the one shown in  Figure 1.21 , with two text boxes for providing two file names, and two buttons next to each text box. Pressing one of the two buttons displays a File Open dialog, like the one shown in  Figure 1.22 , where the user can navigate the computer’s file system and choose a file. Assume that you can work only on this one task, with no interruptions. Provide an estimate within 1 minute (in Step 1).

Step 1.

Estimated ideal total time: _________________

Is the assumption that you will be able to work straight through on this task with no interruptions realistic? Won’t you need to go to the restroom or drink some water? When can you spend the time on this task? If you were asked to do this task as soon as reasonably possible, starting right now, can you estimate when you would be finished? Given that you start now, estimate when you think you will have this program done to hand over to the client. Also give an estimate of the time you will not be on task (e.g., eating, sleeping, other courses, etc.) in Step 2.

Step 2.

Estimated calendar time started: _________ ended:___________breaks:_____

Now, let’s create a new estimate where you divide the entire program into separate developmental tasks, which could be divided into several subtasks, where applicable. Your current task is a planning task, which includes a subtask: ESTIMATION. When thinking of the requirements for the project, assume you will create a class, called StringSorter, with three public methods: Read, Write, and Sort. For the sorting routine, assume that your algorithm involves finding the largest element, putting it at the end of the array, and then sorting the rest of the array using the same mechanism. Assume you will create a method called IndexOfBiggest that returns the index of the biggest element on the array. Using the following chart, estimate how much time it will take you to do each task (and the GUI) in Step 3.

Step 3.

How close is this estimate to the previous one you did? What kind of formula did you use to convert from ideal time to calendar time? What date would you give the client as the delivery date?

Now, design and implement your solution while keeping track of the time in Step 4.

Step 4.

Keeping track of the time you actually spend on each task as well as the interruptions you experience is a worthwhile data collection activity. Compare these times with your estimates. How high or low did you go? Is there a pattern? How accurate is the total with respect to your original estimate?

If you performed the activities in this exercise, chances are that you found the estimate was more accurate after dividing it into subtasks. You will also find that estimates in general tend to be somewhat inaccurate, even for well-defined tasks. Project estimation and effort estimation is one of the toughest problems in software project management and software engineering. This topic will be revisited in detail in  Chapter 13 . For further reading on why individuals should keep track of their development time, see the Personal Software Process (PSP) in  Humphrey (1996) . Accurate estimation is very hard to achieve. Dividing tasks into smaller ones and keeping data about previous tasks and estimates are usually helpful beginnings.

It is important that the estimation is done by the people who do the job, which is often the programmer. The client also needs to check the estimates for reasonableness. One big problem with estimating is that it is conceptually performed during the bid for the job, which is before the project is started. In reality a lot of the development tasks and information, possibly up to design, is needed in order to be able to provide a good estimate. We will talk more about estimating in  Chapter 13 .

1.4 Implementations

In this section we will discuss several implementations of our sorting program, including two ways to implement the sort functionality and several variations of the user interface. We will also discuss unit testing for our implementations. Sample code will be provided in Java, using JUnit to aid in unit testing.

1.4.1 A Few Pointers on Implementation

Although software engineering tends to focus more on requirements analysis, design, and processes rather than implementation, a bad implementation will definitely mean a bad program even if all the other pieces are perfect. Although for simple programs almost anything will do, following a few simple rules will generally make all your programming easier. Here we will discuss only a few language-independent rules, and point you to other books in the References and Suggested Readings section at the end of this chapter.

· The most important rule is to be consistent—especially in your choice of names, capitalization, and programming conventions. If you are programming alone, the particular choice of conventions is not important as long as you are consistent. You should also try to follow the established conventions of the programming language you are using, even if it would not otherwise be your choice. This will ensure that you do not introduce two conventions. For example, it is established practice in Java to start class names with uppercase letters and variable names with lowercase letters. If your name has more than one word, use capitalization to signal the word boundaries. This results in names such as FileClass and fileVariable. In C, the convention is to use lowercase almost exclusively and to separate with an underscore. Thus, when we program in C, we follow the C conventions. The choice of words for common operations is also dictated by convention. For example, printing, displaying, showing, or echoing a variable are some of the terminologies meaning similar actions. Language conventions also provide hints as to default names for variables, preference for shorter or longer names, and other issues. Try to be as consistent as possible in your choice, and follow the conventions for your language.

· Choose names carefully. In addition to being consistent in naming, try to make sure names for functions and variables are descriptive. If the names are too cumbersome or if a good name cannot be easily found, that is usually a sign that there may be a problem in the design. A good rule of thumb is to choose long, descriptive names for things that will have global scope such as classes and public methods. Use short names for local references, which are used in a very limited scope such as local variables, private names, and so on.

· Test before using a function or method. Make sure that it works. That way if there are any errors, you know that they are in the module you are currently writing. Careful unit testing, with test cases written before or after the unit, will help you gain confidence in using that unit.

· Know thy standard library. In most modern programming languages, the standard library will implement many common functions, usually including sorting and collections of data, database access, utilities for web development, networking, and much more. Don’t reinvent or reimplement the wheel. Using the standard libraries will save extra work, make the code more understandable, and usually run faster with fewer errors, because the standard libraries are well debugged and optimized. Keep in mind that many exercises in introductory programming classes involve solving classic problems and implementing well-known data structures and algorithms. Although they are a valuable learning exercise, that does not mean you should use your own implementations in real life. For our sample programming problem, Java has a sorting routine that is robust and fast. Using it instead of writing your own would save time and effort and produce a better implementation. We will still implement our own for the sake of illustration but will also provide the implementation using the Java sorting routine.

· If possible, perform a review of your code. Software reviews are one of the most effective methods for reducing defects in software. Showing your code to other people will help detect not just functionality errors but also inconsistencies and bad naming. It will also help you learn from the other person’s experience. This is another habit that does not blend well with school projects. In most such projects, getting help from another student might be considered cheating. Perhaps the code can instead be reviewed after it is handed in. Reviews are good for school assignments as well as for real-world programs.

1.4.2 Basic Design

Given that we will be implementing different user interfaces, our basic design separates the sorting functionality from the user interface, which is a good practice anyway, because user interfaces tend to change much faster than functionality. We have a class, called StringSorter, that has four methods: (1) reading the strings from a file, (2) sorting the collection of strings, (3) writing the strings to a file, and (4) combining those three, taking the input and output file names. The different user interfaces will be implemented in separate classes. Given that StringSorter would not know what to do with exceptional conditions, such as errors when reading or writing streams, the exceptions pass through in the appropriate methods, with the user interface classes deciding what to do with them. We also have a class with all our unit tests, taking advantage of the JUnit framework.

1.4.3 Unit Testing with JUnit

JUnit is one of a family of unit testing frameworks, the J standing for Java. There are variations for many other languages—for example, cppUnit for C++; the original library was developed in Smalltalk. Here we discuss JUnit in a very basic way; JUnit is discussed further in  Chapter 10 . We just need to create a class that inherits from junit.framework. TestCase, which defines public methods whose names start with test. JUnit uses Java’s reflection capabilities to execute all those methods. Within each test method, assertEquals can be used to verify whether two values that should be equal are truly equal.

1.4.4 Implementation of StringSorter

We will be presenting our implementation followed by the test cases. We are assuming a certain fundamental background with Java programming, although familiarity with another object-oriented programming language should be enough to understand this section. Although the methods could have been developed in a different order, we present them in the order we developed them, which is Read, then Sort, then Write. This is also the order in which the final program will execute, thus making it easier to test.

We import several namespaces, and declare the StringSorter class. The only instance variable is an ArrayList of lines. ArrayList is a container that can grow dynamically, and supports indexed access to its elements. It roughly corresponds to a vector in other programming languages. It is part of the standard Java collections library and another example of how using the standard library saves time. Notice we are not declaring the variable as private in  Figure 1.1 , because the test class needs access to it. By leaving it with default protection, all classes in the same package can access it because Java has no concept like friend classes in C++. This provides a decent compromise. Further options will be discussed in  Chapter 10 . Our first method involves reading lines from a file or stream, as seen in  Figure 1.2 . To make the method more general, we take a Reader, which is a class for reading text-based streams. A stream is a generalization of a file. By using a Reader rather than a class explicitly based on Files, we could use this same method for reading from standard input or even from the network. Also, because we do not know how to deal with exceptions here, we will just let the IOException pass through.

Figure 1.1 Class declaration and Import statements.

Figure 1.2 The readFromStream method.

For testing this method with JUnit, we create a class extending TestCase. We also define a utility method, called make123, that creates an ArrayList with three strings—one, two, and three—inserted in that order in  Figure 1.3 .

We then define our first method, testReadFromStream, in  Figure 1.4 . In this method we create an ArrayList and a StringSorter. We open a known file, and make the StringSorter read from it. Given that we know what is in the file, we know what the internal ArrayList in our StringSorter should be. We just assert that it should be equal to that known value.

We can run JUnit after setting the classpath and compiling both classes, by typing java junit.swingui.TestRunner. This will present us with a list of classes to choose from. When choosing our TestStringSorter class, we find a user interface like the one shown in  Figure 1.5 , which indicates that all tests are implemented and successfully run. Pressing the run button will rerun all tests, showing you how many tests were successful. If any test is unsuccessful, the bar will be red rather than green. Classes are reloaded by default, so you can leave that window open, modify, recompile, and just press run again.

After we verify that our test is successful, we can begin the next method—building the sorting functionality. We decided on a simple algorithm: find the largest element in the array, then swap it with the last element, placing the largest element at the end of the array, then repeat with the rest of the array. We need two supporting functions, one for swapping the two elements in the array and another for finding the index of the largest element. The code for a swap is shown in  Figure 1.6 .

Figure 1.3 TestStringSorter declaration and make123 method.

Figure 1.4 testReadFromStream.

Figure 1.5 JUnit GUI.

Figure 1.6 The code for swapping two integers.

Because swap is a generic function that could be reused in many situations, we decided to build it without any knowledge of the StringSorter class. Given that, it makes sense to have it as a static method. In C++ or other languages, it would be a function defined outside the class and not associated with any class. Static methods are the closest technique in Java. We get as parameters a List, where List is the generic interface that ArrayList implements, and the indexes of the two elements. The test for this method is shown in the testSwap method of TestStringSorter class in  Figure 1.7 .

The next method is the one that returns the index of the largest element on the list. Its name is findIdxBiggest, as shown in  Figure 1.8 . Idx as an abbreviation of index is ingrained in our minds. We debated whether to use largest, biggest, or max/maximum for the name (they are about equally appropriate in our minds). After settling on biggest, we just made sure that we did not use the other two for naming the variables.

Figure 1.7 The testSwap method.

Figure 1.8 The findIdxBiggest method.

We use the compareTo method of Strings, that returns –1 if the first element is less than the second, 0 if they are equal, and 1 if the first is largest. In this method we use the fact that the elements in the ArrayList are strings. Notice that Java (as of version 1.4) does not have support for generics (templates in C++), so the elements have to be explicitly casted to Strings. The test is shown in  Figure 1.9 .

With swap and findIdxBiggest in place, the sort method, shown in  Figure 1.10 , becomes relatively easy to implement. The test for it is shown in  Figure 1.11 . Note that if we knew our standard library, we could have used a much easier implementation, using the sort function in the standard Java library, as shown in  Figure 1.12 . We would have also avoided writing swap and findIdxBiggest! It definitely pays to know your standard library.

Figure 1.9 The testFindIdxBiggest method.

Figure 1.10 The sort method.

Figure 1.11 The testSort1 method.

Figure 1.12 The sort method using Java’s standard library.

Now on to writing to the file; this is shown in  Figure 1.13 . We will test it by writing a known value to the file, then reading it again and performing the comparison in  Figure 1.14 . Now all that is needed is the sort method taking the file names as shown in  Figure 1.15 . Given that we have already seen how to do it for the test cases, it is very easy to do. The test for this method is shown in  Figure 1.16 .

Figure 1.13 The writeToStream method.

Figure 1.14 The testWriteToStream method.

Figure 1.15 The sort method (taking file names).

1.4.5 User Interfaces

We now have an implementation of StringSorter and a reasonable belief that it works as intended. We realize that our tests were not that extensive; however, we can go on to build a user interface, which is an actual program that lets us access the functionality of StringSorter. Our first implementation is a command-line, not GUI, version, as shown in  Figure 1.17 . It takes the names of the input and output files as command parameters. Its implementation is as shown in the figure.

We would use it by typing the following command:

java StringSorterCommandLine abc.txt abc_sorted.txt

Figure 1.16 The testSort2 method.

Figure 1.17 The StringSorter-CommandLine class, which implements a command-line interface for StringSorter functionality.

Do you believe this is a useful user interface? Actually, for many people it is. If you have a command-line window open all the time or if you are working without a GUI, then it is not that hard to type the command. Also, it is very easy to use this command inside a script, to sort many files. In fact, you could use a script to more thoroughly test your implementation. Another important advantage, besides scriptability, is how easy it is to build the interface. This means less effort, lower costs, and fewer errors.

However, for some people this would not be a useful interface. If you are accustomed to only using GUIs or if you do not usually have a command window open and are not going to be sorting many files, then a GUI would be better. Nevertheless, GUI is not necessarily a better interface than a CLI. It depends on the use and the user. Also, it is extremely easy to design bad GUIs, such as the implementation shown in  Figure 1.18 . The code in this figure would display the dialog box shown in  Figure 1.19 . After the user presses OK, the dialog box in  Figure 1.20  would be shown. Notice the title “Input” in the top of the dialog box and the message “Please enter output file name” in  Figure 1.20 . This could be a communication contradiction for the user.

This does not involve much more effort than the command-line version, but it is very inefficient to use. Although it is a GUI, it is worse than the CLI for almost every user. A better interface is shown in  Figure 1.21 . Although it is not a lot better, at least both inputs are in the same place. What makes it more useful is that the buttons on the right open a dialog box as shown in  Figure 1.22  for choosing a file.

Figure 1.18 StringSorterBadGUI class, which implements a hard-to-use GUI for StringSorter functionality.

Figure 1.19 An input file name dialog box for a hard-to-use GUI.

Figure 1.20 An output file name dialog for a hard-to-use GUI.

Figure 1.21 Input and Output file name dialog for GUI.

Figure 1.22 File Open dialog for GUI.

This would at least be a decent interface for most GUI users. Not terribly pretty, but simple and functional. The code for this GUI is available on the website for this book. We are not printing it because it requires knowledge of Java and Swing to be understood. We will note that the code is 75 lines long in Java, a language for which GUI building is one of its strengths, and it took us longer to produce than the StringSorter class! Sometimes GUIs come with a heavy cost. We will discuss user-interface design in  Chapter 7 .

1.5 Summary

In this chapter we have discussed some of the many issues involved in writing a simple program. By now, you should have realized that even for simple programs there is much more than just writing the code. One has to consider many of the following items:

· Requirements

· Code implementation

· Unit testing

· Personal effort estimation

· Design

· User interface

Much of that material belongs to software engineering, and in this text we will provide an overview of it.