16. Building a Software System

Key concepts

  1. Basic program behavior: get input, do work, perform output
  2. Example: A Banking Application
  3. Wrapper Classes
  4. Model-View-Controller Pattern (low calorie version)

Introduction

At this point, you know enough to build a pretty interesting program. Most programs - even though they may look very different on the surface - follow the following basic pattern of behavior. They get input from the user, do some work, and then produce some output. They probably repeat this cycle a number of times before the user decides to quit. We'll sometimes call a program a system to emphasize the point that it is in fact a system of interacting objects that work together to get some interesting job done. The art of building a robust, extensible, and reusable system is partitioning the work between the different objects in an appropriate manner. This partitioning takes experience and practice, but in some cases there are well known patterns that we can identify. These patterns are motifs or idioms that are shared by a great number of real-world programs. By identifying and understanding these patterns, we can integrate them into our toolbox of design strategies. In this lesson we'll introduce one classic pattern, known as the Model-View-Controller pattern, which will help us design classes that maximize flexibility and reuse.

The Banking Domain: Analysis

Let's imagine that we want to build a system that can be used by bank tellers to access and update customers' bank accounts. Let's think for a moment about some of the things that a bank teller might do in the course of his or her day:
  1. Open a new account
  2. Close an existing account
  3. Deposit money into an account
  4. Get the balance from an account
  5. Withdraw money from an account
  6. Print a list of all accounts
The above list of uses implies at least two important objects, a BankAccount object, and some sort of collection object that keeps track of all of the bank's accounts. We've already seen an example of a BankAccount class in a prior lesson. A BankAccount is a simple object that represents a single bank account. It aggregates the balance, owner name, and some other information about a single account. Here is an example implementation of a BankAccount class that we'll use in our system. It's identical to the implementation we built in prior lessons.

/**
  The BankAccount class implements a simple bank account.
  @author Ben Dugan
  @version 11/11/2001
*/
public class BankAccount {
  private int number;
  private double balance;
  private String name;

  /** 
    Create a new BankAccount object, with the given initial values.
    @param acctNumber an account number to use
    @param initBalance an initial balance
    @param acctName the name of the account owner
  */
  public BankAccount(int acctNumber, double initBalance, String acctName) {
    this.number = acctNumber;
    this.balance = initBalance;
    this.name = acctName;
  }

  /** 
    Increase the balance by the given amount.
    @param amount the amount to deposit  
  */
  public void deposit(double amount) {
    this.balance = this.balance + amount;
  }

  /** 
    Answer the current balance. 
    @return the current balance
  */
  public double getBalance() {
    return this.balance;
  }

  /** 
    Answer the account number.
    @return the account number
  */
  public int getAccountNumber() {
    return number;
  }

  /** 
    Decrease the balance by the given amount.  
    @param amount the amount to deposit 
    @return true if and only if there were sufficient funds for the withdrawal
  */
  public boolean withdraw(double amount) {
    if (amount <= this.balance) {
      this.balance = this.balance - amount;
      return true;
    }
    else {
      return false;
    }
  }

  /** 
    Transfer money between accounts.
    @param other a BankAccount to tranfer from
    @param amount an amount to transfer
  */
  public void transferFrom(BankAccount other, double amount) {
    other.withdraw(amount);
    this.deposit(amount);
  }
}
What about the collection of BankAccounts in the bank? Let's call this kind of object Accounts. What kind of object is it? The Accounts object is responsible for maintaining a collection of accounts for the bank. Is the collection ordered? Even though we probably access an account via its account number, this does not mean that there is an inherent order to the accounts. An account number is just a unique key used to access a single account. Whereas there may be two people named "Bill Wattie", they ought to have two separate bank accounts, each with their own unique account number. Let's use a HashMap that associates account numbers with BankAccounts as the fundamental representational component of our Accounts class:
public class Accounts {
  private HashMap accounts;
 
  /** Create a new, empty Accounts collection. */ 
  public Accounts() {
    this.accounts = new HashMap();
  }

  /** Add an account to the collection. */
  public void addAccount(BankAccount account) {
    int accountNumber = account.getAccountNumber();    
    Integer key = new Integer(accountNumber);
    accounts.put(key, account);
  }

  /** Retrieve an account from the collection. */
  public BankAccount getAccount(int accountNumber) {
    Integer key = new Integer(accountNumber);
    return accounts.get(key);
  }
}
First, let's emphasize that this class looks a lot like the MusicCollection class. Its main job is to maintain a collection of things and provide a nice interface for adding and getting access to those things. There are some strange differences though. Can you spot them?

Wrapper Objects

There's something strange about the getAccount and addAccount methods. Both methods create an Integer object to use as a key for accessing the HashMap. The reason we do this is related to the discussion of casting in a prior lesson. Recall the interface for HashMaps:
public class HashMap {
  
  /** Answer the value associated with the given key. */
  public Object get(Object key);

  /** Associate the given key with the given value. */
  public void put(Object key, Object value);

  /** Remove the key and its corresponding value from the table. 
      @return the value if it exists, null otherwise */
  public Object remove(Object key);
}
This interface tells us that HashMaps can associate any key Object with a value Object. Recall that the primitive types int, char, and double - unlike most other classes/types in Java - do not have the "is-a" relationship with the Object class. This exception is a famous "wart" in the definition of the Java language. The reason for this exception is related to efficiency considerations that worried the original implementors of the Java language. We want to implement a method like getAccount as follows:
public BankAccount getAccount(int accountNumber) {
  return accounts.get(accountNumber);
}
However, if we do this, the compiler will refuse to compile our program. Why? Because the type of accountNumber in the above example is int and the type int does not have an "is-a" relationship to the type expected by the get method on HashMaps, which is Object. To deal with this situation, the Java language provides us a set of classes whose only job it is to "wrap" primitive types such as char, int, and double. These classes are called Character, Integer, and Double, respectively. Once we wrap a primitive thing like an int in an Integer object, that object can be used in places where Objects are expected. Here's a fragment of the Integer class interface (interfaces to Character and Double are similar):
public class Integer {
  /** Create a new Integer containing the given value. */
  public Integer(int value);

  /** Answer the integer value of this object. */
  public int intValue();
}
In our Accounts class, we use Integer objects to wrap the account number value before we associate new BankAccounts with their account numbers, and before we look up a BankAccount by its number.

The User Interface

We have now implemented two fundamental classes for our banking world: BankAccount and Accounts. A BankAccount represents a single customer's account and an Accounts object maintains a collection of those BankAccounts so we can access them at later times. While these classes will form the basis for our banking application, we still need to do more work to make the system usable by a teller. In particular, we need to figure out how to handle the input and output to the system. For instance, the teller must input the account number into the system when he or she is looking up an account. What object in our system should be responsible for handling the input, and producing the output?

As usual, we have several options for assigning these responsibilities. The first and perhaps most obvious option is to assign them to the Accounts object. We could, for instance, add a method to the Accounts class that prompts the user for an account number, then looks up that account, and prints the result to the screen. The method might look like this:

public void printAccount(Input in, Output out) {
  int number = in.readInt("Enter account number: ");
  BankAccount account = this.getAccount(number);
  out.println(account);
}
This turns out to not be a very good idea. Why? Because it violates a number of important principles of software design.

First, it violates what we might call the "Hedgehog Principle". The name "Hedgehog Principle" comes from the saying "The fox knows many things, but the hedgehog knows one great thing." The hedgehog is, above all, an expert at rolling itself into a ball to defend itself. The fox, for all of its general cunning cannnot defeat the expert hedgehog. Taken to a programming context, this principle states that classes should prefer to be excellent at doing one thing, rather than be pretty good at a variety of things. By adding a method like printAccount to the Account class, we're asking it to be good at both doing input and output, as well as managing a collection of BankAccounts.

Second, the above design does not typically result in maintainable software. Imagine trying to ship the above software to a country where another language is spoken. We've hardcoded the English language prompt into the definition of the Accounts class. To ship the software to Germany, for instance, we'd have to make a copy of the class (call it something like GermanAccounts), and then go change the prompts in the GermanAccounts class. While this approach would work, what happens when we discover a bug in the original Accounts class? We'd have to fix the bug in the Accounts class, as well as GermanAccounts, FrenchAccounts, and so on. Doing this in practice would be a maintinence nightmare.

Finally, it does not partition the work between different programmers very well. In a real system, getting the user interface right is a big job, and we'd like to allow some programmers to focus just on building an excellent interface, while other programmers might focus on building the underlying system objects, such as BankAccount and Accounts.

These issues encourage us to create a separate class whose job it was only to be an expert at performing input and output. We'll call this class BankUI (short for BankUserInterface). It will have methods that support the following typical bank teller activities:

  1. Open a new account
  2. Close an existing account
  3. Deposit money into an account
  4. Get the balance from an account
  5. Withdraw money from an account
  6. Print a list of all accounts
Let's look at how the BankUI class might be implemented:
public class BankUI {
  private Accounts accounts;
  private Input input;
  private Output output;
  private nextAccountNumber;

  /** Create a new BankUI for the given objects. */
  public BankUI(Accounts accounts, Input in, Output out) {
    this.accounts = accounts;
    this.input = in;
    this.output = out;
    this.nextAccountNumber = 0;
  }

  /** Create a new account */
  public void createAccount() {
    int number = nextAccountNumber;
    nextAccountNumber = nextAccountNumber + 1;
    double balance = input.readDouble("Enter starting balance: ");
    String name = input.readString("Enter account name: ");
    BankAccount account = new BankAccount(number, balance, name);
    accounts.addAccount(account);
    output.println("Account# " + number + " created for " + name);
  }

  /** Get the balance from an account. */
  public void checkBalance() {
    int number = input.readInt("Enter account number: ");
    BankAccount account = accounts.getAccount(number);
    output.println("The balance is " + account.getBalance());
  }

  /** Deposit money into an account. */
  public void depositMoney() {
    int number = input.readInt("Enter account number: ");
    BankAccount account = accounts.getAccount(number);
    double amount = input.readDouble("Enter amount: ");
    account.deposit(amount);
    output.println("New balance: " + account.getBalance());
  }

  /** Withdraw money from an account. */
  public void withdrawMoney() {
    int number = input.readInt("Enter account number: ");
    BankAccount account = accounts.getAccount(number);
    double amount = input.readDouble("Enter amount: ");
    boolean success = account.withdraw(amount);
    if (success) {
      output.println("Withdrawal succeeded.");
    }
    else {
      output.println("Withdrawal failed.  Insufficient funds.");
    }
  }
}

Driving the interface

We now have a class that is pretty good at prompting the user (the bank teller) for the basic information (account numbers and dollar amounts) and then dispatching the work (looking up accounts, changing their balance, etc) to the objects that can handle those operations (the Accounts collection or individual BankAccounts). Let's look at how we might use the interpreter to test the system we've built so far:
prompt> Input in = new Input();
prompt> Output out = new Output();
prompt> Accounts accounts = new Accounts();
prompt> BankUI ui = new BankUI(accounts, in, out);
prompt> ui.createAccount();
Enter starting balance:
200.50
Enter account name:
Ahab
Account# 0 created for Ahab
prompt> ui.depositMoney();
Enter account number:
0
Enter amount:
50.0
New balance: 250.50
prompt> 
Notice that the first four lines are setup code. We need to create our basic system objects, and then we stitch them together with a BankUI object. After that, we send messages to the BankUI object and it handles the input and output required to access and update BankAccount objects.

In real life, we wouldn't expect the teller to actually type Java code to send messages to the BankUI. We can actually internalize the calling of the BankUI methods in the class itself:

public class BankUI {

  // All the other methods...
 
  /** Print a menu of options. */
  public void printMenu() {
    output.println("Please enter one of the following options:");
    output.println(" [n]ew account");
    output.println(" [d]eposit");
    output.println(" [w]ithdraw");
    output.println(" [q]uit");
  }

  /** Prompt teller and dispatch to appopriate method. 
      @return false if and only if the teller quit */
  public boolean dispatch() {
    char option = input.readChar("Enter Option: ");
    boolean quitting = false;
    if (option == 'n') {
      this.createAccount();
    }
    else if (option == 'd') {
      this.depositMoney();
    }
    else if (option == 'w') {
      this.withdrawMoney();
    }
    else if (option == 'q') {
      output.println("Goodbye!");
      quitting = true;
    }
    else {
      output.println("Invalid option.");
    }
    return quitting;
  }

  /** Run the UI until the teller decides to quit. */
  public void go() {
    boolean quitting = false;
    while (!quitting) {
      printMenu();
      quitting = dispatch();
    }
  }
}
We've added three methods to our BankUI class. The first prints a menu of options that are available to the bank teller. The second consumes a character from the teller (by which he or she selects an option), and calls the appropriate method. The third implements a loop, that continually prints the menu and dispatches the chosen option to the right method. Let's use this new class in our interpreter:
prompt> Input in = new Input();
prompt> Output out = new Output();
prompt> Accounts accounts = new Accounts();
prompt> BankUI ui = new BankUI(accounts, in, out);
prompt> ui.go();
Please enter one of the following options:
  [n]ew account
  [d]eposit
  [w]ithdraw
  [q]uit
Enter option:
n
Enter starting balance:
200.50
Enter account name:
Ahab
Account# 0 created for Ahab

Please enter one of the following options:
  [n]ew account
  [d]eposit
  [w]ithdraw
  [q]uit
Enter option:
d
Enter account number:
0
Enter amount:
50.0
New balance: 250.50

Please enter one of the following options:
  [n]ew account
  [d]eposit
  [w]ithdraw
  [q]uit
Enter option:
x
Invalid option.

Please enter one of the following options:
  [n]ew account
  [d]eposit
  [w]ithdraw
  [q]uit
Enter option:
q
Goodbye!

prompt >
Notice that the first four lines are identical to the original example -- we create and stitch together the system objects. After that, however, we make a single call to the go method on the BankUI object, and this method executes a loop which handles the interaction with the teller. In a way, the go method implements a simple interpreter that forever does the following three things: reads an option from the teller, executes that option, and then prints the result. This read-evaluate-print interpreter cycle is another common pattern in many real-world systems.

What's missing

The above system, consisting of three classes (Accounts, BankUI, and BankAccount), implements a simple application that could be used by a bankteller to access and update customers' bank accounts. It should be pointed out that it is far from complete, however. In particular, several issues remain unresolved:
  1. When the teller quits the application, all account information is lost. We'd like the application to be able to "open" and "save" account information from a file on a disk.
  2. What about non-existent accounts? Right now, we are assuming that the teller always enters a valid account number. What happens if an invalid account number is entered?
  3. In the real world, several people can share one account. Does the current system handle this situation? If not, how might we modify one or more of the classes to handle this case.
  4. People often forget their account numbers. We'd like a way to access accounts by a key other than account numbers. Can you think of another useful key and how we might modify the system to handle this situation?
Clearly, building a robust, user-friendly system takes a lot of work. Error checking and handling the wide range of real-world user activities are all part of the art of building usable software systems. Even though the above example only implements a fraction of the activities a real banking system should support, it should give you the flavor of what goes into building such a system.

The Model-View-Controller Pattern

In the above example, we have covered a great deal of territory. We've implemented a class to provide a simple facade for a collection of BankAccounts, the Accounts class. Implementing this class required a detour into the world of Java wrapper classes. We also implemented a class - the BankUI - that is responsible for handling the user-interface related tasks of our system. In doing so, we separated the user interface from the basic system objects. This separation is a primitive example of a fundamental design pattern known as Model-view-controller (MVC for short). This pattern posits three fundamental components to any system with a user interface. The Model is the set of objects that implements the basic model of the world. It does not perform any input or output. Rather, the input is handled by a component called the controller, and the output handled by a component called the view.

While we have combined the jobs of the view and the controller in the BankUI object, we are still faithful to the basic distinctions of the MVC pattern. By decoupling the components in this manner, we gain a great deal in terms of flexibility and reusability. For instance, assume that we need to implement a fancier version of our user interface. In this day and age, text-driven user interfaces are not very popular anymore. Most user interfaces are graphical in nature, and rely heavily on mouse inputs and menus for option selection. Given the above design, we can reuse the BankAccount and Accounts classes without modification, and simply implement a new class (perhaps called GraphicalBankUI) to manage the appropriate display objects and handle mouse inputs from the user. Furthermore, we can mix and match our user interface components in an interesting manner. We can, for instance, start our development by building a simple text-driven user interface, and then later start working on a graphical user interface. During the development and testing of this more complicated interface, we can still rely on the old text-driven interface to test-drive our system, or provide access to features that are not yet available in the nascent graphical user interface.


Ben Dugan & UW-CSE, Copyright (c) 2001.