Skip to main content


Lecture 16: Design patterns

Design patterns are coding idioms that help build better programs. The goal is often to help make programs more modular by decoupling communicating code modules. Some design patterns just help avoid mistakes. Design patterns give programmers a common vocabulary for explaining their designs and aid in quick understanding of the advantages and disadvantages of particular designs.

The term design pattern was introduced by the very influential “Gang of Four” book, Design Patterns: Elements of Reusable Object-Oriented Software, by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides. The book discusses object-oriented programming and introduces (or gives names to) more than 20 design patterns. Many other design patterns or variations of design patterns have been identified since, some more useful and meaningful than others. In this lecture we look at some of the more important design patterns.

We have already seen some important design patterns in this course. One key idea that we have talked about is the importance of programming clients against an interface rather than against a particular implementation, a principle that we enforce by making implementation-specific details hidden from clients. An even stronger way to enforce this principle is to separate the interface into a Java "interface", leaving the implementation in a Java class that implements the interface. This separation of interface and class is what the gang of four called the Bridge pattern. It allows many different clients to use the same implementation without necessarily being aware of which implementation is being used. Therefore, the implementation can be replaced and there can be multiple implementations of the same interface used at the same time.

Iterator pattern

One of the common problems we face in designing programs modularly is how to set up a stream of information from one producer module A to another consumer module B while keeping A and B decoupled so they don't have to know who they are communicating with. Assuming that the values communicated have some type T, the picture looks like this:

The Iterator design pattern is one way to solve this programming problem. Module A constructs objects that provide the ability for the consumer to “pull” values from the producer. These object provide an interface like the following:

interface Iterator<T> {
    boolean hasNext();
    T next() throws NoSuchElementException;
}

The key operation here is next(), which the consumer uses to get objects. This is a polling-style interface, in which the consumer can ask any time for a new object, but might have to wait until something is available. Iterators can be used by calling only next(), but to detect the end of the iteration without using exceptions, it is standard to use the hasNext() method instead.

Once consumer B has obtained an iterator from producer A, it can keep getting new elements from the iterator without mentioning A in any way. The producer code doesn't need know about B, either. Thus, we have complete decoupling of A and B.

An additional advantage of this pattern is that multiple consumers can obtain streams of information from a single producer without interfering with each other. Whatever state is needed to keep track of the position in the stream is stored in the iterator object, not in the producer.

Java provides a very convenient syntactic sugar for invoking iterators. A statement of the form for (T x : c) { ...body... } is syntactic sugar for the following code:

Iterator<T> i = c.iterator();
while (i.hasNext()) {
    T x = i.next();
    ...body....
}

To use this syntactic sugar, it is necessary that c implement the interface Iterable<T>:

interface Iterable<T> {
    Iterator<T> iterator();
}

The main problem with iterators is that implementing iterators can be tricky in some cases, as discussed in recitation. One reason is that the iterator needs to keep track of the current state of the iteration so that it can resume at the right place in the stream on each call to next(). For tree data structures, this is particularly awkward. The state of the iteration is a path from the root to the current node; this path must be updated on each call to next.

A second challenge for implementing iterators is dealing with changes to the underlying data structure during iteration. This is typically forbidden. In the Java collections framework, collection classes throw an exception ConcurrentModificationException if an element is requested from an iterator after a mutation to the collection that occurred during the iteration. Note that a concurrent modification can happen even if there is no real concurrency in the system. To detect such requests, every collection class has a hidden version number that is incremented after each mutation. Iterator objects record the collection's version number when they are created, and compare this version number against the collection's on each call to next(). A mismatch causes the exception to be thrown.

A commonly desired change to the collection is to remove the element currently referenced by the iterator. Iterators may support a remove() method whose job it is to remove the current element; this operation is not considered a concurrent modification. Implementing remove() correctly can be challenging.

Some languages other than Java support another language construct for implementing iterators. The C#, Python, Ruby languages support generators that send results to the consumer using the yield statement. An extended version of Java that supports yield is JMatch. In these languages, you can think of the iterator as running concurrently with the consumer, but only when the consumer requests a new value. The iterator and the loop body are coroutines. For example, with generators, an iterator for trees can be implemented very easily using recursion:

Iterator iterator() {
    if (left != null)
	for (T x : left)
	    yield x;
    yield data;
    if (right != null)
	for (T x : right)
	    yield x;
}

By contrast, a Java implementation of the same iterator will take at least 50 lines of code and offer more opportunities for introducing bugs.

Ironically, the term iterator originally referred to this style of implementing iteration, which was invented in the language CLU in the 70's. The term generator originally referred to what we now know as the iterator design pattern.

Observer pattern

Sometimes we want to send a stream of information from a producer to a consumer, but it's not convenient to have the consumer polling the producer. Instead, we want to push information from the producer to the consumer. We can think of the information being pushed as events that the consumer wants to know about. This is the idea behind the Observer pattern, which works in the opposite way as the Iterator pattern:

In the Observer pattern, the consumer provides an object implementing the interface Observer<T>:

interface Observer<T> {
    void notify(T event);
}

Whenever the producer has a new event x to report to the consumer, it calls the observer's method notify(x). The observer then does something with the data it receives that is appropriate for the consumer. Since the observer is provided by the consumer, it knows what operations the consumer is and is typically inside the consumer's abstraction boundary, perhaps implemented as an inner class.

How does the producer know which observers to notify? This is accomplished by registering the observer(s) with the producer. The producer implements an interface similar to this:

interface Observable<T> {
    void registerObserver(Observer<T> observer);
}

When the producer receives a call to registerObserver, it records the observer in its collection of observers to be notified. When the producer has a new event to provide to consumers, it iterates over the collection, calling notify on each observer in the collection.

We have already seen an instance of the observer pattern: Swing listeners. For example, ActionListeners are observers with a notify named actionPerformed. If one is setting up listener for button clicks, the Observable in question is the JButton object, and an observable is registered by calling addActionListener(l).

Like the Iterator pattern, the Observer pattern has the benefit that the producer and consumer can exchange information without tying either implementation to the other. An observable can also provide information to multiple observers simultaneously.

We can see that there is a symmetry to Iterators and Observers. We can make this a bit more compelling. Using A→B to represent the type of a function that takes in an A and returns a B, and using () to represent the type of an empty argument list (which is really the same thing as void), we have the following types:

Iterator:

    next: ()→T
    iterator: () → (()→T)

Observer:

    notify: T→()
    registerObserver: (T→()) → ()

The types of the Iterator operations are exactly the same as the types of the Observer operations, except that all the arrows are flipped! This shows that we have a duality between Iterator and Observer.

Abstract Factory pattern

When we create objects using a constructor, we tie the calling code to a particular choice of implementation. For example, when creating a set, we specify exactly which implementation we are using (for simplicity, let's ignore type parameters):

    Set s = new HashSet();

One way to avoid binding the client code explicitly to an implementing class is to use factory methods (creators), which we have talked about earlier. We might declare a class with static methods that create appropriate data structures:

class DataStructs {
    static Set createSet() { return new HashSet(); }
    static Map createMap() { return new HashMap(); }
    static List createMap() { return new LinkedList(); }
    ...
}

Now the client can create sets without naming the implementation, and the choice of which implementations to use for all the data structures has been centralized in the DataStructs class.

Sometimes static factory methods still don't provide enough flexibility. The choice of implementation is still fixed at compile time even if the client code doesn't choose it explicitly. We can solve this problem by using the Abstract Factory pattern. The idea is to define an interface with non-static creator methods for the various kinds of things that need to be allocated.

interface DataStructs {
    Set createSet();
    Map createMap();
    List createList();
    ...
}

All the choices about what implementation to use can now be bound into an object that implements this interface. Assuming that object is in a variable ds, the client might contain:

DataStructs ds;
...
Set s = ds.createSet();

Of course the choice of implementation has to be made somewhere, where ds is initialized, but that can be far away from the uses of ds, in some other module. Since the abstract factory is an object, it can be chosen truly dynamically, at run time. There can even be multiple implementations of an abstract factory interface used within the same program.

One place where the abstract factory approach has been used successfully is for user interface libraries. We might define an interface for creating UI components:

interface UIFactory {
    Button createButton(String label);
    Label createLabel(String txt);
    Scrollbar createScrollbar();
    ...
}

Then, different UIFactory objects can encapsulate different choices of look and feel for the user interface. Swing doesn't take quite this approach, but the look-and-feel choices that make Swing UIs look different on Windows versus Mac OS X are in fact made by binding each Swing JComponent to a contained object of type UIComponent. The UIComponent controls the look and feel of the JComponent, and it is chosen dynamically based on the OS platform being used.

Singleton pattern

Sometimes classes don't need to have more than one instance. A class with just one instance is an example of the Singleton pattern. For example, if we wanted a class that represented empty linked lists, we might only allocate a single object of that class, since all empty lists are interchangeable anyway. We can store it into a static field of the class to expose it to clients, and hide the constructor since it shouldn't be used outside the class itself:

class EmptyList implements List {
    public static empty = new EmptyList();
    private EmptyList() {}
}

The Singleton pattern is also frequently used with the Abstract Factory pattern. There is no need to have more than one object of the class implementing, say, DataStructs or UIFactory in the examples above.

Model-View-Controller pattern

Since the UI components are used to manipulate the information managed by the application, it is tempting to store that information and the algorithms that manipulate it (the application logic) directly in components, perhaps by using inheritance. This is usually a bad idea. The code for graphical presentation of information is different from the underlying operations on that information. Interleaving them makes the code harder to understand and maintain. Further, it makes it difficult to port the application to a new platform. For example, you might implement the application in Swing and then want to port it to Android, whose UI toolkit is very different.

This observation leads to the Model-View-Controller pattern, in which the application classes are separated into one of three categories: the model, which contains the important application state and operations, and does not refer to the graphical UI classes; the view, which provides a graphical view of the model; and the controller, which handles user input and translates it into either changes to the view or commands to be performed on the model.

The idea is that the view may hold some state, but only state related to how the model is currently being displayed, or what part of the model is displayed. If the view were destroyed, some version of it could be created anew from the model. With this kind of structure, there can be more than one user interface built on top of the same model. In fact, multiple views can even coexist.

One of the challenges of the MVC pattern is how to allow the view to update when the model changes, without making the model depend on the view. This task is usually accomplished by using the Observer pattern. The model allows observers to be registered on its state; the view is then notified when the state changes.

This separation between model, view, and controller will be very important for Assignment 7, where you will build a distributed version of the critter simulation. The model will run on a shared server, with one or more clients viewing that model through a Swing user interface.

There are many variations of the MVC pattern. Some versions of the MVC pattern make less of a distinction between the view and the controller; this is usually indicated by talking about the M/VC pattern, in which the view and the controller are more tightly coupled, but strong separation is maintained between the model and these two parts of the design.

State machine pattern

Programming in an event-driven style can result in messy designs in which not all events are handled. One way to avoid this is to think about the program, or about parts of the program, as state machines. Other courses cover state machines as mathematical abstractions, but they are also a way to organize code: a design pattern.

A state machine has a set of states and a set of events that can occur. At any given moment, the machine is in one of the allowed states. However, when it receives a new event, it can change states. For each (state, event) pair, there is a new state to which the machine transitions when that event is received in that state.

A state machine can be represented as a graph in which the nodes are the states and the edges are the transitions between states. The edges are labeled with the event that causes that transition.

As a simple example, consider a window in a graphical user interface. Simplifying a bit, it can be in the following states: opened, closed, minimized, or maximized. (One reason that this is a simplification is that the window also has a size and position.) The following events can be received: open, close, minimize, and maximize, corresponding to buttons that can be clicked. As a graph, the window implements the following state machine:

A diagram like this helps understand what states the system can get into and how the system moves among states. It doesn't help as much with ensuring that all combinations of states and events are considered. One way to address this is to construct a state-transition table.

When the number of states in a state machine is finite, it is called a finite state machine, or finite-state automaton. In general, a state machine can have an infinite number of states, or a very large number of states. The rows in the table correspond to states, and the columns correspond to events. The entries in the table say what the next state given the current state and event.

Stateopencloseminimizemaximize
1. Opened 2 3 4
2. Closed
3. Minimized 1 2
4. Maximized 2 3? 1

The table helps us think systematically about all the possible things that can happen in the system, and make sure we have covered all the possibilities.Thinking about the various entries helps us not only missing event handlers but also missing states. For example, when minimizing a maximized window, the state machine above forgets that the window was maximized. When the window is reopened, it will no longer be maximized. If that is not the desired behavior, we'll need to add a fifth state to the state machine, keeping track of windows that are minimized from a maximized state.

Even the entries marked with —, which represent events that don't make sense in the current state, are interesting to think about because we need to make sure that the user interface doesn't permit those events to happen—perhaps by graying out the corresponding UI component.

Centralizing the code that implements a state machine, perhaps as a switch statement per state, or a dispatch table, also helps ensure that the implementation of the state machine is correct.

Antipatterns

Coding patterns that people think should be avoided are often dubbed “antipatterns”. For example, some Java programmers make heavy use of reflection in Java. This is generally bad practice, leading to slow, fragile code. A good reason to use reflection is if you are loading code dynamically at run time (for example, for plugins). This is unusual, so we do not talk about reflection in this course. A good and rather humorous list of antipatterns can be found on Wikipedia.