These lecture notes will serve both as a review of some fundamental concepts you learned in CMSC 150 and in introduction to some design ideas we will be using extensively in this course. At the end of these notes you will find our first programming assignment.
Well-designed Java applications are composed of collections of objects that interact with each other to perform some task. As we progress through this course, the applications we build will gradually grow in size and complexity. Eventually, we will be constructing applications made up of dozens of classes. As our projects grow in scale and complexity, we will find it ever more important to follow good design principles for object-oriented design. These lecture notes will serve as your first introduction to some of the most important design principles we will use this term.
Our design philosophy emphasizes two primary ideas: cohesion and loose coupling.
Cohesion requires that each class we design have a single, cohesive, and well-defined purpose. The most immediate consequence of this principle is that you will frequently see me break an application into a number of small, highly focused classes. Indeed, I will frequently create multiple classes where you may have designed a solution that uses a single class. As I show examples in this course and talk about why I created the classes I created, you will frequently see me invoke cohesion as the rationale for setting up and using the classes I use. Below in these lecture notes you will see a first example of a design driven by this principle.
Another design principle that is closely related to cohesion is information hiding. Many of the classes I design will not expose many of their internal details to public scrutiny or manipulation: most data members will be declared private, which keeps code from other classes from being able to see or manipulate those data members. Other classes will interact with the classes we design exclusively via the public methods that our classes expose to the world. Those public methods express what our classes are and what they do, and they help to present a single, cohesive model to the world of what our class is all about.
Loose coupling is a design principle that states that in a system composed of interacting classes the classes that depend on each other to accomplish a task should have the weakest possible dependencies. In particular, if one class (the client) is using another class (the provider) to accomplish a task, the client should not know or care about the inner workings of the provider class. The provider will expose a set of methods to the outside world that will express to clients what the provider can do for them, and this is all that clients should know or care about the provider. Loose coupling provides a number of benefits for application designers. The first benefit is that it allows classes to be developed independently of each other. Once a design has been set and decisions have been made about what methods each class will offer, a team of programmers can construct those classes independently of each other. Also, these independently developed classes can be designed and constructed and then placed into a class library for future use in other projects. Indeed, one of the major benefits that Java provides for programmers is an extensive library of useful classes that are meant to be used in a wide variety of different settings. As we progress through the term I will frequently comment on the design of classes in the Java class library and how the designers of that class library frequently use loose coupling as an explicit design strategy. Another benefit that comes with loose coupling is that providers that don't expose their internal details allow you to change those internal details without affecting their clients. This is important, because all software systems will evolve over time as requirements change. As much as possible, we will want to put ourselves in a situation where changes can be made locally in a class without having to propagate those changes out to many other classes in an application.
Although the concepts of cohesion and loose coupling are easy to understand, it will still take you some time for your skill at designing classes to fully incorporate these principles. As we progress through this course you will have many opportunities to encounter good design. My hope is that over time you will absorb these good design principles and eventually they will be second nature to you.
For the remainder of these lecture notes I am going to walk you through the design of a simple Java application. I will set up the requirements for this simple application and describe to you a set of classes that you will use to build the application. I will then leave as an exercise for you to fill in all the details and construct the full application.
The first project will also serve as a review of some things you learned in CMSC 150. In particular, this application will make use of the Scanner class to read text data from a file and do console interation with the user. You can find an overview of the Scanner class in chapter 8 of the text.
The application we are going to build is a simple address lookup application. The application will read a set of records from a text file. Each record consists of a phone number, a person's name, and that person's address. When the application starts up it will read those records from a text file and place them in a data structure for later retrieval. The application will then prompt the user to enter one or more phone numbers. For each phone number entered the application will find the corresponding record and display the name and address of the person with that phone number. To keep things simple, the application will only do lookups; we will not be required to create and store new records.
Here is what the user of this application will typically see when they run the application.
Loading data... Done. Enter a number to search for, or "quit" to quit: 832-6736 Joe Gregg 413 Briggs Hall Enter a number to search for, or "quit" to quit: 832-0000 There is no entry for the number 832-0000 Enter a number to search for, or "quit" to quit: quit
The records will be stored in a text file using a simple file format. The first line in the text file will be an integer that tells us how many records are in the file. The records will follow, with each record consisting of three lines, the number, the name, and the address.
Here is what a typical data file will look like.
2 832-7000 Mickey Mouse 711 E. Boldt Way 832-6736 Joe Gregg 413 Briggs Hall
The first class we are going to create for this application represents a single data record for the application. This is a very simple class with an obvious structure.
public class PhoneData {
private String phoneNumber;
private String name;
private String address;
public PhoneData(String number,String name,String address)
{
this.phoneNumber = number;
this.name = name;
this.address = address;
}
public String getNumber() { return phoneNumber; }
public String getName() { return name; }
public String getAddress() { return address; }
}
The design of this class reflects some of the design principles I mentioned earlier. This is first and foremost a class that demonstrates cohesion very clearly. Since this application treats data records as discrete entities containing three separate pieces of information, it makes perfect sense to consolidate those three pieces of data into a single, cohesive class.
This class also demonstrates information hiding. The three data members are all declared private, so that code from other classes can not modify the record data once it has been encapsulated in the PhoneData object by the constructor. This design decision is further reinforced by the fact that each of these data elements has a getter method, but no corresponding setter methods are provided.
The message communicated by this design is that PhoneData objects are immutable objects - once created, they can not be modified in any way. This is a direct requirement of the application we are building, so it is appropriate for the design to express this requirement.
All applications have to manage data, so a common feature we are going to see in almost every application we construct is some sort of top level data class. The purpose of this class is to give us a single entry point to the application's data. This main data class will typically manage the data structures that store the application's data and handle common data maintenance tasks such as save and restoring the application's data from external data stores such as files or databases.
The main data class will have the following structure.
public class PhoneBook {
// Read the phone book data from the file with the
// given name.
public PhoneBook(String fileName) {}
// Return true if the phone book contains and entry
// with the given phone number
public boolean hasEntry(String number) {}
// Fetch the entry with the given number
public PhoneData getEntry(String number) {}
}
Note that this class exposes just enough information so that clients that need to use this class to look things up can do so. Beyond that, the public interface of the PhoneBook class provides few clues to its internal workings. This is important, because not exposing internal class details means that clients that use this class can not inadvertently put themselves in a position where they are dependent on some details concerning how the PhoneBook class actually works. This gives us the flexibility to change the internal workings of the PhoneBook class without potentially breaking any code that uses the PhoneBook class.
The design we use here allows us to say that the PhoneBook class is loosely coupled to any clients that use the class.
Hiding the internal details of a class is called encapsulation. Encapsulation helps make loosely coupled designs possible.
Hiding internal details and loosely coupling the PhoneBook class to other classes that use it allows us to change our mind about how the PhoneBook class will be implemented. Here is another alternative design that makes it possible to make even more dramatic changes to the PhoneBook without breaking client code. In this alternative arrangement, we isolate the functionality of the PhoneBook in an interface and then come along later and construct a concrete class that implements that interface:
public interface PhoneBook {
// Return true if the phone book contains and entry
// with the given phone number
public boolean hasEntry(String number);
// Fetch the entry with the given number
public PhoneData getEntry(String number);
}
An interface is nothing more than a listing of methods. These methods describe a set of services that any class that implements this interface promises to provide. In this case, we are promising to provide a phonebook look-up service implemented through the methods hasEntry() and getEntry().
In our eventual implementation we would construct a main data class for our application that implements the PhoneBook interface:
public class FileBasedPhoneBook implements PhoneBook {
// Read the phone book data from the file with the
// given name.
public FileBasedPhoneBook(String fileName) {}
// Return true if the phone book contains and entry
// with the given phone number
public boolean hasEntry(String number) {}
// Fetch the entry with the given number
public PhoneData getEntry(String number) {}
}
Why would we want to do something like this? The advantage that this design gives us is that it couples the client (our application's user interface code) even more loosely to the main data class that will implement the PhoneBook functionality. This even looser coupling gives us the flexibility to change important details later. For example, we could come along later and replace the FileBasedPhoneBook class with an alternative class that gets phone book data from a database instead of a file.
public class DatabasePhoneBook implements PhoneBook {
// Read the phone book data from the file with the
// given name.
public DatabasePhoneBook(String url) {}
// Return true if the phone book contains and entry
// with the given phone number
public boolean hasEntry(String number) {}
// Fetch the entry with the given number
public PhoneData getEntry(String number) {}
}
Here is some sample code for an application's main method that demonstrates how easy it would be to change our mind about the nature of the phone book. A version that uses a file based phone book would look like this:
public static void main(String args) {
PhoneBook book = new FileBasedPhoneBook("numbers.txt");
String str = "555-1212";
if(book.hasEntry(str))
System.out.println("555-1212 is the number for " + book.getEntry(str).getName());
}
If we later decide to rewrite the application to use a database instead of a file to store our data, the code above has to change in only one place: we would replace FileBasedPhoneBook with DatabasePhoneBook and leave all the other code alone.
Going forward, let us assume that our application will use the file based version of the main data class. We can set this up either by using the simple PhoneData class shown above or the combination of the PhoneBook interface and a FileBasedPhoneBook class that implements that interface.
In constructing our main data class we will require some sort of data structure to store the PhoneData objects we read from the text file. Most importantly, we will want a data structure that not only makes it easy to store the objects, but also makes it easy to find a record given a phone number.
Storing and retrieving objects is a common task in Java programming, so it is not surprising that the Java class library offers a number of classes that can do this for us. For this application we are going to be using a map class. A map is a data structure that allows you to store objects and retrieve them by using a key. The key is a piece of data that acts as a unique descriptor for each object stored in the map. In this application we will be storing PhoneData objects in a map with the phone number strings acting as the keys to retrieve the objects.
The Java class library presents the map concept as an interface, the java.util.Map interface. You can find a full description of this interface in the online documentation. The only two methods in this interface that we are going to use are the put and get methods. The put method takes two parameters, a key and an associated value. In our case, we will use Strings as the keys and PhoneData objects as the values. The get method takes a single parameter, a key to search for, and returns the associated object if there is an entry in the map with that key. If there is no entry in the map with that key, get will return null.
Since java.util.Map is an interface, to make use of it we will need a class that implements that interface. The Java class library offers a couple of classes that implement this interface. We will be using the java.util.TreeMap class as our Map implementation. The TreeMap class uses something called a binary search tree to make a very efficient implementation of a Map - you will learn more about the binary search tree data structure in CMSC 270. For now, we are just going to use the TreeMap class without having to know much at all about its implementation or internal details. This is a nice example of loose coupling in action: the TreeMap class acts as a provider for us, with our application code acting as the client loosely coupled to the TreeMap.
Here is some sample code that shows how to set up and use a TreeMap.
Map<String,PhoneData> myMap = new TreeMap<String,PhoneData>();
PhoneData aRecord =
new PhoneData("832-6736","Joe Gregg","413 Briggs Hall");
myMap.put(aRecord.getNumber(),aRecord);
PhoneData lookUp = myMap.get("832-6736");
if(lookUp != null)
System.out.println(lookUp.getName());
Before we can use our Map object to look up records, we will have to read those record objects from the input text file and place them in the Map. The process of reading those records from the input file and turning them into a collection of objects is sufficiently complex that you may find it useful to see some code that demonstrates how to do this.
TreeMap<String, PhoneData> dictionary
= new TreeMap<String, PhoneData>();
Scanner in = new Scanner(new File("phones.txt"));
int count = in.nextInt(); // Read the int at the top
// of the file that tells us how many records are in
// the file.
in.nextLine(); // Skip to the next line to start reading data.
int read = 0;
while(read < count) {
String number = in.nextLine();
String name = in.nextLine();
String address = in.nextLine();
PhoneData nextData = new PhoneData(number,name,address);
dictionary.put(number, nextData);
read++;
}
A final class you will need to create for this project is an application class. The only purpose this class will serve is to give us a place to put our application's main method.
The main method will construct a PhoneBook object and then implement a simple text-based user interface. The user interface will implement user interaction as shown in the example at the beginning of these notes:
Loading data... Done. Enter a number to search for, or "quit" to quit: 832-6736 Joe Gregg 413 Briggs Hall Enter a number to search for, or "quit" to quit: 832-0000 There is no entry for the number 832-0000 Enter a number to search for, or "quit" to quit: quit
Construct the application as outlined above. When your work is done, place the NetBeans project folder for your application into a zip archive and email the archive to greggj@lawrence.edu.
This project is due by the start of class on Friday, Sept. 16.