Book cover
All rights reserved. Version for personal use only.
This web version is subjected to minor edits. To report errors or typos, use this form.

Home | Dark Mode | Cite

Software Engineering: A Modern Approach

Marco Tulio Valente

6 Design Patterns

A design that doesn’t take change into account risks major redesign in the future. – Gang of Four Book

This chapter begins with an introduction to the concept and objectives of design patterns (Section 6.1). We then discuss in detail ten design patterns: Factory, Singleton, Proxy, Adapter, Facade, Decorator, Strategy, Observer, Template Method, and Visitor. Each of these patterns is discussed in a separate section (Sections 6.2 to 6.11). The presentation of each pattern is organized into three parts: (1) a context, that is, a system where the pattern is useful; (2) a problem in the design of this system; (3) a solution to this problem using patterns. In Section 6.12, we briefly discuss a few more patterns. We finish the chapter by commenting that design patterns are not a silver bullet, i.e., we discuss situations in which the use of design patterns is not recommended (Section 6.13).

6.1 Introduction

Design patterns are inspired by an idea proposed by Christopher Alexander, an architect and professor at the University of California, Berkeley. In 1977, Alexander published a book called A Pattern Language: Towns, Buildings, Construction, where he documents various patterns for city and building construction. According to Alexander:

Each pattern describes a problem which occurs over and over again in our environment, and then describes the core of the solution to that problem, in such a way that you can use this solution a million times over, without ever doing it the same way twice.

In 1995, Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides published a book adapting Alexander’s ideas to the world of software development (link). Instead of proposing a catalog of solutions for designing cities and buildings, they presented a catalog with solutions for recurrent problems in software design. They called the solutions in the book design patterns which are more precisely defined as follows:

Design patterns are descriptions of communicating objects and classes that are customized to solve a general design problem in a particular context.

Hence, to understand the patterns proposed by the Gang of Four—the nickname by which the authors and the book are known—we need to understand: (1) the problem that the pattern intends to solve; (2) the context in which this problem occurs; (3) the proposed solution. In this chapter, we will describe some design patterns using this format: context, problem, and solution. We will also show several examples of code.

Apart from providing predefined solutions for design problems, design patterns have become a vocabulary widely adopted by developers. For example, it is common to hear developers saying that they used a factory to solve a certain problem, while a second problem was solved by using decorators. In other words, developers mention the pattern’s name and assume that the adopted solution is already clear. Similarly, the vocabulary proposed by design patterns is widely used in documentation. For example, the following figure shows the documentation of one of the classes of the Java standard library. We can see that the class name ends in Factory, which is one of the design patterns we will study shortly. In the class description, it is mentioned again that it is a factory. Thus, developers who know this design pattern are better prepared to understand and use the class in question.

Documentation of a Factory class from the Java API

Developers can also benefit from knowing design patterns in two scenarios:

  • When implementing their own code. In this case, knowledge of design patterns can help them to use a tested and validated design solution in their implementation.

  • When using a third-party code, such as the DocumentBuilderFactory class in the figure. In this case, knowledge of design patterns can assist them in understanding the behavior and structure of the code they plan to reuse.

It is also important to highlight that design patterns aim to create flexible and extensible designs. In this chapter, before explaining each pattern, we will present a context and an existing implementation that works and produces a result. However, it does not have a flexible design. To make this inflexibility clear, we will propose a change in the current implementation, for example, to support a new requirement. However, implementing this change demands some effort, which is minimized if we use a design pattern.

The authors of the design patterns book argue that when designing a system we should also plan for inevitable changes. They refer to this concern as design for change. As they state in the quote that opens this chapter, neglecting design for change as a consideration puts developers at the risk of having to make a significant redesign of their systems in the near future.

The design patterns book proposes 23 patterns, divided into the following three categories (the patterns we will study in this chapter are in bold, followed by the section number in which they are presented):

  • Creational: patterns proposing flexible solutions for creating objects. These include Abstract Factory (6.2), Factory Method, Singleton (6.3), Builder (6.12), and Prototype.

  • Structural: patterns proposing flexible solutions for composing classes and objects. These include Proxy (6.4), Adapter (6.5), Facade (6.6), Decorator (6.7), Bridge, Composite, and Flyweight.

  • Behavioral: patterns proposing flexible solutions for interaction and division of responsibilities among classes and objects. These include Strategy (6.8), Observer (6.9), Template Method (6.10), Visitor (6.11), Chain of Responsibility, Command, Interpreter, Iterator (6.12), Mediator, Memento, and State.

6.2 Factory

Context: Suppose a distributed system based on TCP/IP. In this system, three functions f, g, and h need to create TCPChannel objects for remote communication, as shown next:

void f() {
  TCPChannel c = new TCPChannel();  
  ...
}

void g() {
  TCPChannel c = new TCPChannel();
  ...
}

void h() {
  TCPChannel c = new TCPChannel();
  ...
}

Problem: Suppose that, in certain customers, we need to use UDP for communication. Therefore, considering this requirement, the previous code does not adhere to the Open/Closed Principle, meaning it is not closed to modifications and open to extensions concerning the communication protocols used. To be more precise, we would like to parameterize the above code so that it can create TCPChannel or UDPChannel objects depending on the clients. However, the new operator requires the literal name of a class. Thus, in languages like Java, C++, and C#, we cannot pass the class name as a parameter to new.

Solution: Our solution is based on the Factory design pattern. This pattern has some variations, but in our problem, we will use a static method that: (1) creates and returns objects of a particular class; (2) and hides the type of these objects behind an interface. An example is shown below:

class ChannelFactory {
  public static Channel create() { // static factory method
    return new TCPChannel();
  }
}

void f() {
  Channel c = ChannelFactory.create();
  ...
}

void g() {
  Channel c = ChannelFactory.create();
  ...
}

void h() {
  Channel c = ChannelFactory.create();
  ...
}

In this new implementation, functions f, g, and h are not aware of the Channel they are going to use. They call a Static Factory Method, which creates and returns an object of a concrete class. Indeed, this variant of the Factory pattern was not proposed in the Gang of Four book but a few years later by Joshua Bloch (link). It’s also important to mention that all functions use the Channel interface to manipulate the objects created by the factory method. In other words, we also observe the Prefer Interfaces to Classes principle (or Dependency Inversion) here.

Indeed, in this new implementation, the system is again working with TCPChannel. However, if we want to change the channel type, all we need to modify is a single code element: the create method of the ChannelFactory class. In other words, a static factory method encapsulates and confines the new calls, which were spread over the code in our first implementation.

There are also variations of the Factory pattern. In one variation, an abstract class is used to implement the factory methods. This class is then referred to as an Abstract Factory. An example is shown in the following code:

abstract class ProtocolFactory { // Abstract Factory
  abstract Channel createChannel();
  abstract Port createPort();  
  ...
}
void f(ProtocolFactory pf) {
  Channel c = pf.createChannel();
  Port p = pf.createPort();
  ...
}

In this example, we’ve omitted the classes that extend ProtocolFactory. They are responsible for implementing the methods that really create the channels and ports. For instance, we can have two subclasses: TCPProtocolFactory and UDPProtocolFactory.

6.3 Singleton

Context: Consider a Logger class, used to log the operations performed in a system, as in the following code:

void f() {
  Logger log = new Logger();
  log.println("Executing f");
  ...
}

void g() {
  Logger log = new Logger();
  log.println("Executing g");
  ...
}

void h() {
  Logger log = new Logger();
  log.println("Executing h");
  ...
}

Problem: In this implementation, each method that needs to log events creates its own Logger instance. However, we would like all usages of Logger to share the same instance. In other words, we do not want a proliferation of Logger objects. Instead, we want to have a single instance of this class used in all parts of the system that have to log particular events. This is crucial, for instance, if the log is performed in a file. Allowing multiple Logger objects would result in each one overwriting the file created by another object.

Solution: The solution to this problem involves transforming the Logger class into a Singleton. This design pattern defines how to implement classes that, as the name suggests, have at most one instance. Next, we present a new implementation of the Logger class that functions as a Singleton:

class Logger {

  private Logger() {} // prohibits new Logger() in clients

  private static Logger instance; // single instance

  public static Logger getInstance() {
    if (instance == null)  // 1st time getInstance is called
       instance = new Logger();
    return instance;
  }

  public void println(String msg) {
    // logs msg to console, but it can be to a file
    System.out.println(msg);
  }
}

Firstly, this class has a private default constructor. Consequently, a compiler error will occur whenever a code outside the class requests a new Logger(). Additionally, a static attribute holds the single class instance. When a client code needs this instance, it should call the getInstance() method. An example is shown below:

void f() {
  Logger log = Logger.getInstance();
  log.println("Executing f");
  ...
}

void g() {
  Logger log = Logger.getInstance();
  log.println("Executing g");
  ...
}

void h() {
  Logger log = Logger.getInstance();
  log.println("Executing h");
  ...
}

In this new implementation, the getInstance calls return the same Logger instance. Thus, all messages are logged using this instance.

Among the design patterns proposed in the Gang of Four book, Singleton is probably the most controversial and criticized. The reason is that it can be used to create global variables and data structures. In our case, the single instance of Logger is, in practical terms, a global variable that can be accessed anywhere in the program by calling Logger.getInstance(). As we studied in Chapter 5, global variables represent a form of poor coupling between classes, i.e., a type of coupling that is not mediated through stable interfaces. However, the use of a Singleton in our example does not raise concerns because it provides the exact solution for the constraint we need to implement. Since we should have a unique log file in the whole system, this resource should be represented by a class that, by design, has at most a single instance.

In summary, Singleton should be used to model resources that, conceptually, have a single instance during a program’s execution. However, an abusive use of the design pattern occurs when it is used to replace global variables.

Finally, there is one more criticism regarding the use of Singletons: they make automated testing more complicated. The reason is that a method’s result can now depend on a global state stored in a Singleton. For example, consider a method m that returns the value of x + y, where x is an input parameter and y is a global variable, which is part of a Singleton. Thus, to test this method, we need to provide the value x, which is easy since it’s a parameter. However, we also need to ensure that y has a known value, which is more challenging because it is an attribute of a Singleton class.

6.4 Proxy

Context: Let’s consider a BookSearch class with a method that searches for a book given its ISBN number:

class BookSearch {
  ...
  Book getBook(String ISBN) { ... }
  ...
}

Problem: Suppose that our book searching service is becoming popular and gaining users. To enhance its performance, we have considered implementing a cache system: before searching for a book, we will check if it is already in the cache. If so, the book is immediately returned. Otherwise, the search proceeds following the regular logic of the getBook() method. However, we do not want to implement this new requirement, i.e., cache search, in the BookSearch class. The reason is that we want to keep the class cohesive and respecting the Single Responsibility Principle. Indeed, the cache will be implemented by a different developer than the one responsible for maintaining BookSearch. Also, we are going to use a third-party cache library, with many features and customizations. For these reasons, we intend to separate the interest of searching books by ISBN (which is a functional requirement) from the interest of using a cache in the book searches (which is a non-functional requirement).

Solution: The Proxy design pattern advocates the use of an intermediary object, also known as a proxy, between a base object and its clients. As a result, the clients no longer have a direct reference to the base object but rather to the proxy. Moreover, the proxy has a reference to the base object and should implement the same interfaces as this object.

The purpose of a proxy is to mediate the access to a base object, adding functionalities to it without its knowledge. In our case, the base object is a BookSearch instance; the functionality we intend to add is a cache, and the proxy is an object from the following class:

class BookSearchProxy implements BookSearchInterface {

  private BookSearchInterface base;

  BookSearchProxy (BookSearchInterface base) {
    this.base = base;
  }

  Book getBook(String ISBN) {
    if("book with ISBN in cache")
      return "book from cache"
    else {
      Book book = base.getBook(ISBN);
      if (book != null)
         "add book to cache"
      return book;
    }
  }
  ...
}

We should also create a BookSearchInterface interface, which is not shown in the code. Both the base class and the proxy class should implement this interface, allowing clients to be unaware of the existence of a proxy between them and the base object. Once again, we are applying the Prefer Interfaces to Classes principle.

Next, we show the main program that creates the mentioned objects. First, we present the code before the proxy. In this code, a BookSearch object is created in the main function and then passed as an argument to a class that needs the book search service, like the View class.

void main() {
  BookSearch bs = new BookSearch();
  ...
  View view = new View(bs);
  ...
}

When using a proxy, we need to modify this code, as shown next. In this new code, the View receives a reference to the proxy instead of a reference to the base object.

void main() {
  BookSearch bs = new BookSearch();
  BookSearchProxy pbs = new BookSearchProxy(bs);
  ...
  View view = new View(pbs);
  ...
}

The following figure shows the objects involved in the solution using a proxy:

Proxy Design pattern

Besides assisting in the implementation of caches, proxies can be used to implement other non-functional requirements, such as:

  • Communication with a remote client, by encapsulating protocols and communication details. These proxies, also referred to as stubs, facilitate remote communication.

  • Allocate memory by demand for objects that consume a lot of memory. For instance, a class might need to use a high-resolution image. Hence, we can use a proxy to prevent the image from always being kept in the main memory. It is only loaded, possibly from disk, before certain methods are executed.

  • Control the access to a base object by multiple clients. For example, the clients might need to authenticate to execute specific operations on the base object. Hence, the base object’s class can focus on implementing only functional requirements.

6.5 Adapter

Context: Consider a system to control multimedia projectors. To achieve this goal, the system needs to use objects from classes provided by the projector manufacturers, as presented below:

class SamsungProjector {
  public void start() { ... }
  ...

class LGProjector {
  public void enable(int timer) { ... }
  ...
}

To simplify, we’re only showing two classes. However, in a real scenario, there might be classes from various manufacturers. Additionally, we’re showcasing only one method from each class, but they may contain other methods. Specifically, the presented method is responsible for turning the projector on. For Samsung projectors, this method is named start and it has no parameters. In the case of LG projectors, the method is named enable, and we have the option to pass a time interval in minutes to enable the projector. If this parameter equals zero, the projector starts immediately.

Problem: In the system we are developing, we want to use a unified interface for turning on projectors, irrespective of their brand. The following code presents this interface and a corresponding class of our system:

interface Projector {
  void turnOn();
}

class ProjectorControlSystem {

  void init(Projector projector) {
    projector.turnOn();  // turns on any projector
  }

}

However, as presented earlier, the classes from each projector are implemented by their respective manufacturers and are closed for modification. In other words, we do not have access to their code to make these classes implement the Projector interface.

Solution: The Adapter design pattern, also known as Wrapper, provides a solution for our problem. This pattern is recommended when we need to convert a class interface to another one. In our example, it can be employed to convert the Projector interface used in the projector control system to the interfaces (public methods) of the manufacturers’ classes. An example of an adapter class, from SamsungProjector to Projector, is presented next:

class SamsungProjectorAdapter implements Projector {

  private SamsungProjector projector;

  SamsungProjectorAdapter (SamsungProjector projector) {
    this.projector = projector;
  }

  public void turnOn() {
    projector.start();
  }
}

The SamsungProjectorAdapter class implements the Projector interface. Therefore, objects of this class can be passed to the init() method of the projector control system. This adapter class also holds a private attribute of type SamsungProjector. The sequence of calls is as follows (see also the next UML sequence diagram): first, the client, represented by the init method, calls turnOn() from the adapter class; next, the execution of this method invokes the equivalent method on the object being adapted, which, in this case, is the start method of Samsung projectors.

Adapter design pattern

If we need to manipulate LG projectors, we have to implement a second adapter class. However, its code will resemble SamsungProjectorAdapter.

6.6 Facade

Context: Suppose we’ve implemented an interpreter for a language ABC. This interpreter enables the execution of ABC programs within a host language, specifically Java in our case. To make the example more concrete, let’s assume that ABC is a query language, similar to SQL. The execution of ABC programs from Java code involves the following steps:

Scanner s = new Scanner("prog1.abc");
Parser p = new Parser(s);
AST ast = p.parse();
CodeGenerator code = new CodeGenerator(ast);
code.eval();

Problem: As the ABC language is gaining popularity, developers are expressing concerns about the complexity of the previous code. It demands an understanding of the internals of the ABC interpreter. Consequently, users frequently request a simplified interface for our language.

Solution: The Facade design pattern provides a solution to our problem. A Facade is a class that provides a simplified interface to a system. The objective is to shield users from the need to understand the internal classes of this system. Instead, they interact solely with the Facade class, while the internal complexity remains encapsulated behind it.

In our problem, the Facade can be this class:

class Interpreter {

  private String file;

  Interpreter(file) {
    this.file = file;
  }

  void eval() {
    Scanner s = new Scanner(file);
    Parser p = new Parser(s);
    AST ast = p.parse();
    CodeGenerator code = new CodeGenerator(ast);
    code.eval();
  }
}

Thus, to execute ABC programs developers can use a single line of code:

new Interpreter("prog1.abc").eval();

Before the existence of the facade, clients had to create three internal objects and make two method calls. With the facade in place, it is now sufficient to create a single object and invoke the eval method.

6.7 Decorator

Context: Let’s return to the remote communication system used to explain the Factory Pattern. Suppose the TCPChannel and UDPChannel classes implement the following Channel interface:

interface Channel {
  void send(String msg);
  String receive();
}

class TCPChannel implements Channel {
  ...
}

class UDPChannel implements Channel {
  ...
}

Problem: Clients of these classes need to add extra functionalities to channels, such as buffers, message compression, logging, etc. However, these functionalities are optional; depending on the customer’s requirements, only some functionalities or none of them are needed. An initial solution to this problem involves using inheritance to create subclasses with each possible selection of features. The list below illustrates some of the subclasses that we can create(note that extends indicates inheritance):

  • TCPZipChannel extends TCPChannel
  • TCPBufferedChannel extends TCPChannel
  • TCPBufferedZipChannel extends TCPZipChannel extends TCPChannel
  • TCPLogChannel extends TCPChannel
  • TCPLogBufferedZipChannel extends TCPBufferedZipChannel extends TCPZipChannel extends TCPChannel
  • UDPZipChannel extends UDPChannel
  • UDPBufferedChannel extends UDPChannel
  • UDPBufferedZipChannel extends UDPZipChannel extends UDPChannel
  • UDPLogChannel extends UDPChannel
  • UDPLogBufferedZipChannel extends UDPBufferedZipChannel extends UDPZipChannel extends UDPChannel

In this solution, there is a subclass for each combination of features. Suppose the customer needs a UDP channel with a buffer and compression. For that, we have to implement UDPBufferedZipChannel as a subclass of UDPZipChannel, which, in turn, is a subclass of UDPChannel. As the reader may have realized, this solution using inheritance is almost unfeasible, as it results in an explosion in the number of classes related to communication channels.

Solution: The Decorator pattern offers an alternative to inheritance when we need to add new functionalities to a base class. Instead of using inheritance, composition is employed to dynamically add these functionalities to base objects. Therefore, Decorator is an example of applying the Prefer Composition over Inheritance principle, as we studied in Chapter 5.

When adopting a decorator-based solution, the client configures a Channel as in these examples:

channel = new ZipChannel(new TCPChannel());
// TCPChannel that compresses/decompresses data 

channel = new BufferChannel(new TCPChannel());
// TCPChannel with a buffer 

channel = new BufferChannel(new UDPChannel());
// UDPChannel with a buffer 

channel= new BufferChannel(new ZipChannel(new TCPChannel()));
// TCPChannel with compression and buffer

Therefore, the configuration of a Channel takes place at instantiation time using a chain of new operators. The innermost new creates the base object, which in our case can be a TCPChannel or UDPChannel object. Subsequently, the outer operators decorate this base object with additional functionalities.

Now, we will explain the classes that are the decorators themselves, like ZipChannel and BufferChannel. First, they are subclasses of the following class:

class ChannelDecorator implements Channel {

  private Channel channel;

  public ChannelDecorator(Channel channel) {
    this.channel = channel;
  }

  public void send(String msg) {
    channel.send(msg);
  }

  public String receive() {
    return channel.receive();
  }

}

This class has two important features:

  • It is a Channel, since it implements this interface. Therefore, it can be used wherever a Channel is expected.

  • Besides that, internally, it contains a Channel object, and the calls to send and receive are delegated to this object.

Finally, we proceed to the actual decorators. They must be subclasses of ChannelDecorator, as shown in the following code, which implements a decorator responsible for compressing and decompressing the messages passing through the channel:

class ZipChannel extends ChannelDecorator {

  public ZipChannel(Channel c) {
    super(c);
  }  

  public void send(String msg) {
    "compress msg"
    super.send(msg);
  }

  public String receive() {
    String msg = super.receive();
    "decompress msg"
    return msg;
  }

}

To understand the implementation of ZipChannel, let’s consider this code:

Channel c = new ZipChannel(new TCPChannel());
c.send("Hello, world")

The send call in the last line triggers the following method executions:

  • Firstly, ZipChannel.send is executed, which is responsible for compressing the message.

  • After compression, ZipChannel.send invokes super.send, which, in turn, executes ChannelDecorator.send.

  • ChannelDecorator.send delegates the call to its internal Channel, which, in our example, is a TCPChannel.

  • Finally, we reach TCPChannel.send, which transmits the compressed message via TCP.

6.8 Strategy

Context: Suppose we are implementing a data structures library with the following class:

class MyList {

  ... // list data
  ... // list methods: add, delete, search

  public void sort() {
    ... // sorts the list using Quicksort
  }
}

Problem: Our clients desire to use various algorithms for sorting the list elements. However, the current implementation is fixed on the Quicksort algorithm. Thus, considering the design principles discussed in Chapter 5, it becomes evident that MyList does not conform to the Open/Closed principle regarding the sorting algorithm.

Solution: The Strategy pattern provides a solution to our challenge of enabling MyList to accommodate new sorting algorithms. This pattern aims to parameterize the algorithms used by a class, promoting the encapsulation of a family of algorithms and making them interchangeable. It is recommended when a class needs to perform a specific task, such as sorting in our example. Given the existence of multiple algorithms for this purpose, the intention is to avoid committing to a single one in advance, as occurred in our initial version of MyList.

Next, we show the new MyList implementation, which uses the Strategy Pattern for configuring the sorting algorithm:

class MyList {

  ... // list data
  ... // list methods: add, delete, search

  private SortStrategy strategy;

  public MyList() {
    strategy = new QuickSortStrategy();
  }

  public void setSortStrategy(SortStrategy strategy) {
    this.strategy = strategy;
  }

  public void sort() {
    strategy.sort(this);
  }
}

In this revised implementation, the sorting algorithm is an attribute of the MyList class, and a setSortStrategy method is implemented to configure this algorithm. The sort method delegates the sorting task to a method with the same name in the object with the sorting strategy. In this call, this is used as a parameter, as the sorting algorithm needs to access the list elements.

To conclude the presentation, we show the classes that implement the strategies, i.e., the sorting algorithms:

abstract class SortStrategy {
  abstract void sort(MyList list);
}

class QuickSortStrategy extends SortStrategy {
  void sort(MyList list) { ... }
}

class ShellSortStrategy extends SortStrategy {
  void sort(MyList list) { ... }
}

6.9 Observer

Context: Suppose we are designing a system for controlling weather stations. In this system, we have to manipulate objects of two classes: Temperature, which is a domain class that stores the temperatures monitored by the station; and Thermometer, a class used to display the current temperatures. Whenever the temperature changes, the thermometers should be updated.

Problem: We don’t want to couple Temperature (the domain class) to Thermometer (the user interface class). The reason is straightforward: user interface classes often change. At its current version, the system has a textual interface, which displays temperatures in Celsius on the system’s console. But, soon, we plan to introduce web and mobile interfaces, among others. We also plan to offer different thermometer layouts, including digital and analog ones. Finally, we have other classes like Temperature and Thermometer in our application, such as: AtmosphericPressure and Barometer, AirHumidity and Hygrometer, WindSpeed and Anemometer. Therefore, when possible, we like to reuse the notification mechanism in other classes.

Solution: The Observer pattern is the recommended solution for our context and problem. This pattern defines how to implement a one-to-many relationship between subject and observer objects. When the state of a subject changes, the observers are notified.

First, let’s present the main function for our weather station system:

void main() {
  Temperature t = new Temperature();
  t.addObserver(new CelsiusThermometer());
  t.addObserver(new FahrenheitThermometer());
  t.setTemp(100.0);
}

This function creates a Temperature object (a subject) and then adds two observers to it: a CelsiusThermometer and a FahrenheitThermometer. Finally, it sets the temperature to 100 degrees Celsius. We assume that temperatures are monitored in the Celsius scale by default.

The Temperature and CelsiusThermometer classes are shown below:

class Temperature extends Subject {

  private double temp;

  public double getTemp() {
    return temp;
  }

  public void setTemp(double temp) {
    this.temp = temp;
    notifyObservers();
  }
}
class CelsiusThermometer implements Observer {

  public void update(Subject s){
    double temp = ((Temperature) s).getTemp();
    System.out.println("Celsius Temperature: " + temp);
  }
}

Note how Temperature inherits from a class called Subject. In the proposed solution, every subject should extend this class. By doing this, they inherit two methods:

  • addObserver: In the example, this method is used in the main function to add two thermometers as observers of a Temperature.

  • notifyObservers: In the example, this method is called by Temperature when its value changes.

The implementation of notifyObservers, which is not showed here, calls the update method of all observers of a given Temperature. The update method is part of the Observer interface and must be implemented by every observer, such as CelsiusThermometer.

The following figure shows a UML sequence diagram illustrating the communication between a temperature (subject) and three thermometers (observers). The sequence of calls begins with the temperature receiving a request to execute setTemp().

Observer Design Pattern

The Observer pattern offers two key advantages:

  1. Decoupling Subjects and Observers: Subjects, such as Temperature, remain unaware of their observers. They publish events to announce changes in their state by calling notifyObservers. This decoupling allows for flexible reuse of subjects in different scenarios and facilitates the implementation of various types of observers for the same subject.

  2. Reuse of the notification mechanism: The Observer pattern provides a reusable notification mechanism for different subject-observer pairs. For example, the Subject class and the Observer interface can be reused for notifications involving atmospheric pressure and barometers, air humidity and hygrometers, wind speed and anemometers, and so on.

6.10 Template Method

Context: Suppose we are implementing a payroll system. In this system, we have an Employee class with two subclasses: PublicEmployee and CorporateEmployee.

Problem: We intend to define a template for salary calculation in the base class Employee, which can then be inherited by its subclasses. Thus, these subclasses would only need to adapt the salary calculation template to their specifics. In fact, only the subclasses know exactly all the details related to computing an employee’s salary.

Solution: The Template Method design pattern solves the problem we’ve stated. It specifies how to implement the template of an algorithm in an abstract class, let’s call it X, but leaves some steps or methods pending. These methods are implemented in the subclasses of X. In summary, a Template Method allows subclasses to customize an algorithm without changing its principal structure, which is defined in the base class.

An example of a Template Method for our problem is shown below:

abstract class Employee {

  double salary;
  ...
  abstract double calculateRetirementDeductions();
  abstract double calculateHealthPlanDeductions();
  abstract double calculateOtherDeductions();

  public double calculateNetSalary() { // template method
    double retirement = calculateRetirementDeductions();
    double health = calculateHealthPlanDeductions();
    double other = calculateOtherDeductions();
    return salary - retirement - health - other;
  }
}

In this example, calculateNetSalary is a template method for calculating salaries. It standardizes this process, specifying that we have to consider three deductions: for retirement, for the employee’s health plan, and other deductions. After these deductions, the net salary is computed by subtracting them from the employee’s total salary. However, in the Employee class, we still don’t know how to calculate the deductions, as they vary according to the type of employee (public or private, for example). Therefore, the class declares three abstract methods representing each of these deductions. Since they are abstract, Employee is also an abstract class. As the reader may have already noticed, the subclasses of Employee, such as PublicEmployee and CorporateEmployee, will inherit the calcNetSalary method. However, they have to implement the three abstract methods: calculateRetirementDeductions, calculateHealthPlanDeductions, and calculateOtherDeductions.

Template methods enable old code to call new code. In the example, the Employee class was likely implemented before PublicEmployee and CorporateEmployee. Therefore, we say that Employee is older than its subclasses. Even so, Employee has a method that calls new code implemented in the subclasses. This characteristic of object-oriented systems is referred to as the inversion of control. It is crucial, for instance, for implementing frameworks, i.e., semi-finished applications that must be customized by their clients before use. Although template methods are not the only available solution for this purpose, they represent an interesting alternative for clients to implement the missing parts of a framework.

6.11 Visitor

Context: Consider the parking control system we used as an example in Chapter 5. Assume in this system there is a Vehicle class, with subclasses Car, Bus, and Motorcycle. These classes store information about the vehicles parked in the parking lot. Assume further that all these vehicles are stored in a list. Thus, we say this list is a polymorphic data structure, since it contains objects of different classes, as long as they are subclasses of Vehicle.

Problem: In the parking control system, there is a common need to perform operations on all parked vehicles. For instance, tasks such as printing information about the parked vehicles, saving this data to a file, or sending messages to vehicle owners often arise.

However, we want to implement these operations outside of the Vehicle classes using code like the following:

interface Visitor {
  void visit(Car c);
  void visit(Bus b);
  void visit(Motorcycle m);
}  

class PrintVisitor implements Visitor {
  public void visit(Car c) { "print car data" }
  public void visit(Bus b) { "print bus data" }
  public void visit(Motorcycle m) { "print motorcycle data"}
}

In this code, the class PrintVisitor contains methods for printing the data of a Car, Bus, and Motorcycle. Once this class is implemented, the intention is to use the following code to visit all the vehicles in the parking lot:

PrintVisitor visitor = new PrintVisitor();
foreach (Vehicle vehicle: parkedVehicleList) {
  visitor.visit(vehicle); // compilation error
}

However, in this code, the invocation of the visit method depends on the dynamic type of the target object (visitor) and the dynamic type of its argument (vehicle). Yet, in languages like Java, C++, or C#, only the target object type is considered in the selection of the method to be called. In other words, in Java and similar languages, the compiler only knows the static type of vehicle, which is Vehicle. Therefore, it cannot infer which visit implementation should be called.

To be clearer, the following error occurs when compiling this code:

visitor.visit(vehicle);  
         ^
method PrintVisitor.visit(Car) is not applicable
  (argument mismatch; Vehicle cannot be converted to Car)
method PrintVisitor.visit(Bus) is not applicable
  (argument mismatch; Vehicle cannot be converted to Bus)

Actually, this code only compiles in languages that offer double dispatch of method calls. In these languages, the types of the target object and one argument are used to determine the method that will be invoked. However, double dispatch is only available in a few languages, such as Common Lisp.

Therefore, our problem is as follows: how do we simulate double dispatch in a language like Java? By achieving this, we can circumvent the compilation error that occurs in the presented code.

Solution: The solution to our problem involves the utilization of the Visitor design pattern. This pattern outlines a way to add an operation to a family of objects without needing to modify the classes themselves. Furthermore, the Visitor pattern is designed to function seamlessly even in languages with single dispatching, such as Java.

As a first step, we need to implement an accept method in each class of the hierarchy (see the following code). In the root class, this method is an abstract method and has a parameter of type Visitor. In the subclasses, its implementation simply calls the visit method of this Visitor, passing this as a parameter. Since this call occurs within the class body, the compiler knows the type of this. For example, in the Car class, the compiler recognizes that the type of this is Car. Consequently, it understands that it should call the visit implementation that takes Car as a parameter. To be precise, the specific method to be called depends on the dynamic type of the target object (v). However, this is not problematic as it represents a case of single dispatch, which is permitted in languages like Java.

abstract class Vehicle {
  abstract public void accept(Visitor v);
}

class Car extends Vehicle {
  ...
  public void accept(Visitor v) {
    v.visit(this);
  }
  ...
}

class Bus extends Vehicle {
  ...
  public void accept(Visitor v) {
    v.visit(this);
  }
  ...
}

// Same for Motorcycle

Finally, we need to modify the loop that traverses the list of parked vehicles. In the new code, we call the accept method of each vehicle, passing the visitor as a parameter.

PrintVisitor visitor = new PrintVisitor();
foreach (Vehicle vehicle: parkedVehicleList) {
  vehicle.accept(visitor);
}

In summary, visitors facilitate the addition of a method to a class hierarchy. A visitor groups related operations together—in this example, printing the data of Vehicle and its subclasses. However, it is easy to have another visitor, with different operations, such as persisting the objects to a database. On the other hand, adding a new class to the hierarchy, like Truck, requires updating all visitors with a new method: visit(Truck).

Before we conclude, it’s important to mention that visitors have a major disadvantage: they can require a violation of encapsulation of the classes to be visited. For instance, Vehicle might have to implement public methods that expose its internal state so that visitors can access them.

6.12 Other Design Patterns

Iterator is a design pattern that standardizes an interface for traversing a data structure. This interface includes methods such as hasNext() and next(), as shown in the following example:

List<String> list = Arrays.asList("a","b","c");
Iterator it = list.iterator();
while(it.hasNext()) {
  String s = (String) it.next();
  System.out.println(s);
}

An iterator allows the traversal of a data structure without the need to know the concrete type of its elements. Instead, it’s sufficient to be familiar with the methods provided by the Iterator interface. Additionally, iterators enable multiple traversals simultaneously on the same data structure.

Builder is a design pattern that simplifies the instantiation of objects with several attributes, some of which are optional. In cases where the value of these attributes is not provided, they should be initialized with a default value. Rather than creating multiple constructors, each representing a possible combination of parameters, the initialization process can be delegated to a Builder class. The following example illustrates this pattern using a Book class.

Book se = new Book.Builder().
                setName("Soft Eng: A Modern Approach").
                setPublisher("MT").setYear(2024).build();

Book gof = new Book.Builder().setName("Design Patterns").
                setAuthors("GoF").setYear(1995).build();

An alternative to using a Builder would involve implementing the instantiation through constructors. However, this approach requires creating multiple constructors for the Book class, for each combination of attributes. Moreover, using these constructors could lead to confusion, as developers need to be aware of the order of the different parameters.

With the Builder pattern, the set methods indicate which attribute of the Book is being initialized. Thus, another alternative could be implementing the set methods directly in the Book class. However, this approach would violate the principle of information hiding, as it would allow the modification of the attributes of the class at any time. In contrast, using a Builder ensures that attributes can only be defined at instantiation time.

This version of the Builder pattern that we presented does not align with the original description found in the Gang of Four book. Instead, we described a version proposed by Joshua Bloch (link), which, today, corresponds to the most common use of Builders. For example, this version is used in various classes of the Java API, such as Calendar.Builder.

6.13 When Not to Use Design Patterns?

Design patterns aim to enhance the flexibility of a system’s design. For example, factories facilitate changes in the types handled by a program. A decorator allows the customization of a class with new features, making it adaptable to other use cases. The Strategy pattern enables the configuration of the algorithms used by a class, just to name a few examples.

However, like almost everything in Software Engineering, the use of patterns also has a cost. For instance, a factory requires the implementation of at least one additional class in the system. To cite a second example, the Strategy pattern demands the creation of an abstract class and an extra class for each algorithm. Therefore, the adoption of design patterns demands careful analysis. To illustrate this type of analysis, let’s continue using the examples of Factory and Strategy:

  • Before using a factory, we should ask (and answer) the following question: Will we need to create objects of different types in our system? Is there a high likelihood that such objects will be necessary? If the answer is yes, then it’s worthwhile to use a factory to encapsulate the creation of such objects. Otherwise, it’s better to create the objects using the new operator, which is the native solution for object creation in languages like Java.

  • Before using the Strategy pattern, we should ask ourselves: Is it necessary to parameterize the algorithms used in this class? Do we have customers who require alternative algorithms? If so, it’s worth using the Strategy pattern. Otherwise, it’s preferable to implement the algorithm directly in the class.

Although we are using only two design patterns as examples, similar questions can be applied to other patterns.

However, many systems exhibit an observed overuse of design patterns, where the gains in terms of flexibility and extensibility become questionable. There’s even a term to refer to this situation: patternitis, an inflammation associated with the premature use of design patterns.

John Ousterhout has a comment related to this disease (link, Sec. 19.5):

As with many ideas in software design, the notion that design patterns are good doesn’t necessarily mean that more design patterns are better.

Ousterhout illustrates his argument by pointing to the use of decorators when opening a file in Java, as shown in the following code:

FileInputStream fs = new FileInputStream(fileName);
BufferedInputStream bs = new BufferedInputStream(fs);
ObjectInputStream os = new ObjectInputStream(bs);

According to the author, decorators introduce unnecessary complexity to the file creation process in Java. His main argument is that, as a general rule, the use of a buffer is beneficial when opening any file. Therefore, they should be provided by default, rather than through a decorator. Consequently, FileInputStream and BufferedInputStream could be merged into a single class.

Bibliography

Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, 1995.

Joshua Bloch. Effective Java. 3rd edition. Prentice Hall, 2017.

Eric Freeman, Elisabeth Robson, Bert Bates, Kathy Sierra. Head First Design Patterns: A Brain-Friendly Guide. O'Reilly, 2004.

Eduardo Guerra. Design Patterns com Java: Projeto Orientado a Objetos guiado por Padrões. Casa do Código, 2014.

Fernando Pereira, Marco Tulio Valente, Roberto Bigonha, Mariza Bigonha. Arcademis: A Framework for Object Oriented Communication Middleware Development. Software: Practice and Experience, 2006.

Fabio Tirelo, Roberto Bigonha, Mariza Bigonha, Marco Tulio Valente. Desenvolvimento de Software Orientado por Aspectos. XXIII Jornada de Atualização em Informática (JAI), 2004.

Exercises

1. Mark T (True) or F (False).

( ) Prototype is an example of a structural design pattern.

( ) Singleton ensures that a class has at least one instance and provides a global access point for it.

( ) Template Method defines the skeleton of an algorithm in an abstract class, postponing the definition of some steps for subclasses.

( ) Iterator provides a way to access the elements of an aggregated object sequentially without revealing its underlying representation.

2. Identify the names of the following design patterns:

  1. Offers a high-level interface that makes a system easier to use.

  2. Ensures that a class has, at most, one instance and provides a unique access point to it.

  3. Facilitates the construction of complex objects with various attributes, some of which are optional.

  4. Converts the interface of one class into another interface that clients expect. This pattern allows classes to work together when it would not be possible due to the incompatibility of their interfaces.

  5. Offers an interface or abstract class for creating a family of objects.

  6. Provides a method to centralize the creation of a type of object.

  7. Serves as an intermediary that controls access to a base object.

  8. Allows adding new functionalities dynamically to a class.

  9. Provides a standardized interface for navigating data structures.

  10. Allows changes in the algorithms used by a class.

  11. Makes a data structure open to extensions, allowing adding a function to each element of a data structure without changing their code.

  12. Allows an object to notify other objects that its state has changed.

  13. Defines the skeleton of an algorithm in a base class and delegates the implementation of some steps to subclasses.

3. Among the design patterns answered in question (2), which ones are creational patterns?

4. Considering the answers to question (2), list design patterns that:

  1. Make a class open to extensions without needing to modify its source code, i.e., patterns that put into practice the Open/Closed principle.

  2. Decouple two types of classes.

  3. Increase class cohesion, i.e., make the class have a single responsibility.

  4. Simplify the use of a system.

5. What is the similarity among Proxy, Decorator, and Visitor? And what is the difference between these patterns?

6. In the Adapter example, we presented the code for a single adapter class (SamsungProjectorAdapter). Write the code for a similar class that adapts the Projector interface to the LGProjector interface (the code for both interfaces is provided in Section 6.5). Name this class LGProjectorAdapter.

7. Suppose a base class A. Suppose we want to add four optional features F1, F2, F3, and F4 to A. These features can be added in any order, i.e., the order is not important. If we use inheritance, how many subclasses of A will we have to implement? And if we choose a solution using decorators, how many classes will we have to implement (not counting class A)? Justify and explain your answer.

8. In the Decorator example, we presented the code for a single decorator (ZipChannel). Write the code for a similar class that prints the message to be transmitted or received in the console. Name this decorator class LogChannel.

9. Suppose the following code of a Subject class from the Observer pattern:

interface Observer {
  public void update(Subject s);
}

class Subject {

  private List<Observer> observers=new ArrayList<Observer>();

  public void addObserver(Observer observer) {
    observers.add(observer);
  }

  public void notifyObservers() {
    (A)
  }

}

Implement the code for notifyObservers, commented with an (A) above.

10. Consider you are a developer tasked with implementing Java’s I/O API. To avoid what we referred to as patternitis you decided to merge the FileInputStream and BufferedInputStream classes into a single class. Thus, as we discussed in Section 6.13, the buffering mechanism will be activated by default in this new class. However, how would you implement the option to disable such buffers when needed?

11. Suppose the Visitor example we used in Section 6.11. Specifically, suppose the following code, shown at the end of the section.

PrintVisitor visitor = new PrintVisitor();

foreach(Vehicle vehicle: parkedVehicleList) {
  vehicle.accept(visitor);
}

Suppose that parkedVehicleList contains three objects: aCar, aBus, and anotherCar. Draw a UML sequence diagram that shows the methods executed by this code (assume it is triggered by a main object).

12. In an interview given to the InformIT website in 2009, on the occasion of the 15th anniversary of the first edition of GoF, three of the authors of the book mentioned that, if they were to release a second edition of the work, they would likely retain the original patterns and incorporate some new ones that have become common since the first edition’s release in 1995. One of the new patterns they discussed in this interview is called the Null Object. To understand its operation and benefits, you can find several articles on the web. If you prefer books, a good reference is Chapter 25 of Robert C. Martin and Micah Martin’s book Agile Principles, Patterns, and Practices in C#, or you can explore the refactoring called Introduce Null Object from Martin Fowler’s refactoring book.