The Mail Program

An Overview of the Design of the Application

The architecture of the data classes

The basic unit of information that this application deals with is the Message, which represents an email message. Messages are organized into Accounts, with all of the messages belonging to a particular user of the email system stored in a list in the user's Account object. The ultimate container in this application is the AccountMaster, which stores a list of Accounts and can retrieve an Account given a user name as a key.

The architecture of the server

The server uses a traditional server architecture, with the main program launching threads to conduct conversations with clients. The HandleAClient thread class uses a simple protocol to serve client requests. Each client request starts with a numeric code identifying the nature of the request. Those request codes are defined by symbolic constants defined in the MailActions interface. Since both the server thread and the client class that communicates with the server implement this interface we maintain coherence between the client and the server.

The server thread routes requests that come across the network to an Account object associated with the user the client logs in as. Occasionally the thread will need to communicate with the AccountMaster as well, so it also has a link to that object.

The architecture of the client

The client application is a traditional GUI application with a main frame, dialogs, and single data class, the MessageManager.

The MessageManager provides a single point of entry into the data classes. The interesting twist in this case is that the data itself lies across the network on the server. Because the MessageManager has to communicate with server using a protocol defined in the MailActions interface it implements that interface.

The MessageManager serves a second function. The main window contains a JList that displays a list of message headers for the user to select from. To make the JList work correctly we need a ListModel. It is natural to make the MessageManager serve as the ListModel, because the MessageManager is the gateway to the data.

There are three dialogs in this application. Each dialog is invoked by pressing a button in the ClientFrame.

General Design Principles Used in This Application

Next I would like to focus on some general principles of good design. In each case I will demonstrate just how this application follows those design principles.

Encapsulation

A class is well encapsulated when it is self-contained and does not expose any of its internal details to the outside world. A properly encapsulated class keeps all of its data members private and provides public methods for querying and manipulating its contents.

For example, the Account class maintains a list of Message objects in a linked list. That list is kept strictly private. Instead, the Account class exposes a set of methods for adding and removing messages.

Without this level of encapsulation there is a strong temptation to reach directly into the internals of this object. For example, there is a strong temptation to dispose of the Account class's deleteMessage method altogether and just write the HandleAClient's deleteMessage() method like this:

public void deleteMessage() {
	   try {
	    String str = in.readLine();
      int whichMsg = Integer.parseInt(str);
      account.messages.remove(whichMsg);
      out.println(STATUS_OK);
      out.flush();
    }
    catch (Exception e) {
      dropConnection();
    }
 }

This approach appears to offer a decrease in complexity. By allowing the HandleAClient class to reach directly into the Account class and delete a Message we are eliminating the need to even write a deleteMessage() method for the Account class.

There are several things wrong with this approach. The first and most dangerous is that allowing direct access to internal data structures makes it virtually impossible to enforce design choices you have made. Here is the code for the Account.deleteMessage method.

  public synchronized void deleteMessage(int n) {
    messages.remove(n);
    save();
  }

It turns out that the Account class needs to enforce a policy of always backing its contents up to a file whenever the contents change. By allowing open access to the messages we immediately make it possible for code outside the Account class to subvert this policy. This is analogous to buying an expensive rug and laying it out on the sidewalk. You want to put that rug inside the house and be able to enforce a policy that says that visitors with muddy boots have to remove their boots at the door.

The second problem with this approach is that it tends to dilute the code for the Account class, which ultimately ends up increasing the cognitive load on the programmer. As soon as you expose the internal details of a class and make it possible to write non-trivial code outside the source file where that class is defined you are effectively diluting the code for your class by spreading it over several files. This ultimately increases the cognitive load on the programmer, because now the code for deleting messages can not be found in Account.java and the programmer has to remember that the deleting code now resides in HandleAClient.java. This is like installing the light switch for your living room in the basement of your house. When visitors want to turn on the lights in the living room they have to search all over the house to find the switch.

The final problem with this approach is that it eliminates flexibility. If we decide at some point to replace the messages linked list with an array we can't do it easily because there is code outside the Account class that relies on the messages being stored in a linked list.

Clean separation of GUI code from data and network code

Once you begin to enforce a policy of strong encapsulation throughout your application it immediately makes the design cleaner and more modular. By making it virtually impossible to do anything complicated outside the data classes you gain the useful side effect of making your user interface code dramatically more simple.

For example, this is the code associated with the 'Compose a new message' button in the ClientFrame class.

  void newButton_actionPerformed(ActionEvent e) {
    WritingDialog dlg = new WritingDialog(this);
    Message msg = dlg.doDialog(mgr.getUserName());
    if(msg != null)
      mgr.sendMessage(msg);
  }

This code delegates the job of composing the message to a dialog class with a very clean and simple interface. The WritingDialog class completely encapsulates the details of putting up an appropriate user interface for message writing and assembling the various bits of the message into a Message object. Once the Message object comes out of the dialog, this code hands the Message off to the MessageManager, which deals with all of the details of getting the message sent across the network to the server.

This again leads to a useful concentration of complexity. Even though we are writing a networking client application, absolutely none of the user interface classes are even aware that this is a networking application. All of the details of sockets and such are concentrated in the MessageManager class and hidden from view.

Network Design

Because this is a networking application, I should also make some specific remarks about the design of this application from a networking perspective.

Classic client - server design

The first thing to say is that this application uses a classic client-server design. This assumes the following.

Here is the heart of the server code:

  public void run() {
    try {
     out = new PrintWriter(s.getOutputStream());
     in = new BufferedReader(
              new InputStreamReader(s.getInputStream()));

      while (active) {
        String str = in.readLine();
        int request = Integer.parseInt(str);
        switch (request) {
          case NEW_ACCOUNT:
            newAccount();
            break;
          case LOG_IN:
            logIn();
            break;
          case GET_HEADERS:
            getHeaders();
            break;
          case GET_MESSAGE:
            getMessage();
            break;
          case DELETE_MESSAGE:
            deleteMessage();
            break;
          case SEND_MESSAGE:
            sendMessage();
            break;
          case LOG_OUT:
            dropConnection();
            break;
        }
      }
    }
    catch (Exception ex) {
      System.err.println(ex);
    }
  }

Using a protocol and delegating the work to distinct methods makes this code as clean as possible. (By the way, this also happens to be the longest method in the entire application. The average method in the application is about half this size.)

Optimizing the application for maximum performance

An important consideration in the design of networked applications is making sure that we make efficient use of the network and also avoid delays caused by use of the network. The mail application design has a number of features to help in this regard.

  public synchronized String getMessageHeader(int n) {
    Message msg = messages.get(n);
    return msg.getSender() + ": " + msg.getSubject();
  }

The classes on the server side are also carefully optimized for good memory performance. For example, Account objects are designed to exist in two states, active and inactive. In the active state an Account contains its full list of Message objects. In the inactive state all of the Messages have been dumped out to the backup file and unloaded from the Account object to save memory space. To correctly determine whether an Account object should be in the active or inactive state, the Account class uses a technique called reference counting. Accounts are only accessible by checking them out from the AccountMaster by calling the AccountMaster's checkOut() method. Each Account object has an internal counter that gets incremented each time the Account is checked out for use and decremented whenever it gets checked back in. Whenever the counter goes back to 0, the Account unloads its messages and goes back to the inactive state.

A further optimization in this application is the use of separate backup files for each Account. This is more efficient than using a single large file to store all messages for all users.

The proxy design pattern

The MessageManager class on the client is cleverly designed to create the illusion that the Message data is stored on the client side.

The data is actually stored on the server, and the MessageManager uses the network to access it. For example, here is the code for the getMessage() method.

  public Message getMessage(int n) {
    Message msg = null;
    try {
        out.println(GET_MESSAGE);
        out.println(n);
        out.flush();

        String response = in.readLine();
        int status = Integer.parseInt(response);
        if (status == STATUS_OK)
          msg = new Message(in);
      }
      catch (Exception e) {
        shutDown();
      }
    return msg;
 }

This strategy of using a single object on the client to act as the interface for a complex collection of data (that isn't even stored in the object) is called the proxy design pattern. It simplifies the design because clients of this class don't have to know any of the details of how Messages are stored.

Thread safety issues

Since we are using a standard multithreaded server design we have to be careful to avoid collisions between threads. Again encapsulation comes in handy, since all data is accessed through method calls and the corresponding methods can simply be made synchronized to avoid thread conflicts.