Book cover

Web Version | 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; and (3) a solution to this problem using patterns. In Section 6.12, we briefly discuss a few more patterns. We finish the chapter by explaining 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 documented 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 of solutions for recurrent problems in software design. They called the solutions in the book design patterns and defined them as follows:

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

To understand the patterns proposed by the Gang of Four—the nickname by which the authors and the book are known—we need to consider three key elements: (1) the problem that the pattern intends to solve; (2) the context in which this problem occurs; and (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 code examples.

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 say 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 in 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 familiar with 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:

It is also important to emphasize 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, this implementation does not have a flexible design. To illustrate this inflexibility, we will propose a change in the current implementation, for example, to support a new requirement. Implementing this change requires some effort, which can be minimized by employing an appropriate 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 introduces 23 patterns, categorized into three main groups (the patterns we will study in this chapter are followed by the section number in which they are presented):

6.2 Factory

Context: Consider 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 below:

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

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

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

Problem: For certain customers, we need to use UDP for communication. Therefore, considering this requirement, the previous code does not adhere to the Open/Closed Principle, as it is not closed to modifications nor open to extensions regarding the communication protocols used. More precisely, we need to parameterize the above code so that it can create TCPChannel or UDPChannel objects depending on the customer requirements. 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. While this pattern has several variations, in our case, we will use a static method that (1) creates and returns objects of a particular class; and (2) hides the type of these objects behind an interface. Here’s an example:

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 specific 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 was introduced 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, the Prefer Interfaces to Classes principle (or Dependency Inversion) is also used here.

In this new implementation, the system still works with TCPChannel. However, if we want to change the channel type, we only need to modify 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 previously spread throughout the code in our initial implementation.

There is also a variation of the Factory pattern that relies on an abstract class to define the factory methods. This abstract class is then referred to as an Abstract Factory. The following example shows this:

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. These subclasses are responsible for implementing the methods that actually create the channels and ports. For instance, we could have two subclasses: TCPProtocolFactory and UDPProtocolFactory.

6.3 Singleton

Context: Consider a Logger class, used to log 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 the current implementation, each method that needs to log events creates its own Logger instance. However, we want all usages of Logger to share the same instance. Our goal is to avoid a proliferation of Logger objects and instead have a single instance used throughout the system for logging events. This is particularly important if the logs are written to a file, as multiple Logger objects could result in each instance overwriting the others’ log files.

Solution: The solution to this problem is to transform the Logger class into a Singleton. This design pattern ensures that a class, as the name suggests, has at most one instance. Below is a revised 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);
  }
}

First, this class has a private default constructor. Consequently, a compiler error will occur whenever code outside the class attempts to create a new instance using new Logger(). Additionally, a static attribute holds the single instance of the class. When client code needs this instance, it should call the getInstance() method. An example is provided 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 single 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 significant concerns because it provides an appropriate solution for the constraint we need to address. Since we should have a unique log file throughout the 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, misuse of the design pattern occurs when it is merely 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 straightforward as it is 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. Additionally, we are going to use a third-party cache library, with many features and customizations. For these reasons, we aim to separate the concern of searching books by ISBN (a functional requirement) from using a cache in the book searches (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 instead interact with the proxy. The proxy has a reference to the base object and implements the same interface 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 of 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’s introduction. 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, such as 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 below. 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 illustrates 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:

6.5 Adapter

Context: Consider a system designed to control multimedia projectors. To implement this system, we need to use objects from classes provided by different projector manufacturers, as shown 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. Furthermore, we’re showcasing only one method from each class, but they may contain other methods. The presented method is responsible for turning the projector on. For Samsung projectors, this method is named start and takes no parameters. For LG projectors, the method is named enable, and it accepts a time interval in minutes to activate the projector. If this value is 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 shows this interface and a corresponding class in our system:

interface Projector {
  void turnOn();
}

class ProjectorControlSystem {

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

}

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

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

class SamsungProjectorAdapter implements Projector {

  private SamsungProjector projector;

  public 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 has a private attribute of type SamsungProjector. The sequence of calls is as follows (see also the accompanying UML sequence diagram): first, the client, represented by the init method, calls turnOn() on the adapter class; then, this method invokes the equivalent method on the object being adapted, which, in this case, is the start method of the Samsung projectors.

Adapter design pattern

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

6.6 Facade

Context: Suppose we’ve implemented an interpreter for a language called ABC. This interpreter enables the execution of ABC programs within a host language, 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 gains popularity, developers are expressing concerns about the complexity of the previous code. It requires an understanding of the internals of the ABC interpreter. As a result, 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 offers a simplified interface to a complex 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 case, the Facade can be implemented as follows:

class Interpreter {

  private String file;

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

  public 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, clients now only need to create a single object and invoke the eval method.

6.7 Decorator

Context: Let’s return to the remote communication system used in our discussion of the Factory Pattern. Consider a scenario where 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—are needed. An initial solution to this problem involves using inheritance to create subclasses with each possible selection of features. The following list illustrates some of the subclasses that we can create (note that extends indicates inheritance):

In this solution, there is a subclass for each combination of features. For example, suppose the customer needs a UDP channel with a buffer and compression. To meet this requirement, we have to implement UDPBufferedZipChannel as a subclass of UDPZipChannel, which, in turn, is a subclass of UDPChannel. As the reader may have noticed, this solution using inheritance is impractical, as it results in an explosion of 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, it employs composition to dynamically add these functionalities to base objects. Therefore, Decorator is an example of applying the Prefer Composition over Inheritance principle, which we studied in Chapter 5.

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

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

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

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

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

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

Now, let’s examine the classes that serve as the decorators themselves, such as ZipChannel and BufferChannel. These classes 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:

Now, let’s examine the actual decorators. These classes 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 the following code:

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

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

6.8 Strategy

Context: Let’s consider a scenario where 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 want to use various algorithms to sort the list elements. However, the current implementation uses only the Quicksort algorithm. Thus, considering the design principles discussed in Chapter 5, it is clear that MyList does not follow the Open/Closed principle with respect to the sorting algorithm.

Solution: The Strategy pattern offers a solution to our challenge of enabling MyList to accommodate new sorting algorithms. This pattern aims to parameterize the algorithms used by a class, encapsulating a family of algorithms and making them interchangeable. It is particularly useful when a class needs to perform a specific task, such as sorting in this example. Given that multiple algorithms exist for this purpose, the goal is to avoid committing to a single one in advance, as was the case in our initial version of MyList.

Next, we show the new MyList implementation, which uses the Strategy Pattern to configure 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 becomes an attribute of the MyList class, and a setSortStrategy method is implemented to configure this algorithm. The sort method delegates the sorting task to the method of the same name in the strategy object. In this call, this is passed as a parameter, allowing the sorting algorithm to access the list elements.

To conclude our discussion, we’ll 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 need to manipulate objects of two classes: Temperature, which is a domain class that stores the temperature monitored by the station; and Thermometer, a class used to display the current temperature. 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. In its current version, the system has a textual interface, which displays temperatures in Celsius on the system’s console. However, we plan to introduce web and mobile interfaces, among others, in the near future. We also plan to offer different thermometer layouts, including digital and analog ones. Furthermore, we have other pairs of classes similar to Temperature and Thermometer in our application, such as: AtmosphericPressure and Barometer, AirHumidity and Hygrometer, WindSpeed and Anemometer. Therefore, we intend to reuse the notification mechanism across other classes.

Solution: The Observer pattern is the recommended solution for the given context and problem. The pattern defines a way to implement a one-to-many relationship between subject and observer objects. When the state of a subject changes, all its 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 implemented as follows:

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 so, they inherit two key methods:

The implementation of notifyObservers, which is not shown here, calls the update method on all observers of a given Temperature object. 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 when the temperature receives a request to execute setTemp().

Observer design pattern

The Observer pattern offers two key advantages:

  1. Decoupling of 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. Reusability 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 other similar pairs.

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 be inherited by its subclasses. Furthermore, the subclasses should be able to adapt this template to their specific requirements. In fact, only the subclasses know the precise details related to computing an employee’s salary.

Solution: The Template Method design pattern address the problem we’ve stated. It specifies how to implement the template of an algorithm in an abstract class, which we’ll call X, while deferring some steps or methods for implementation in the subclasses of X. In summary, a Template Method allows subclasses to customize an algorithm without changing its fundamental structure, which is defined in the base class.

Here’s an example of a Template Method for our problem:

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 need to consider three deductions: for retirement, for the employee’s health plan, and other deductions. The net salary is computed by subtracting these deductions from the employee’s total salary. However, in the Employee class, we don’t yet know how to calculate the deductions, as they vary according to the type of employee (public or corporate, 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 you may have already noticed, the subclasses of Employee, such as PublicEmployee and CorporateEmployee, will inherit the calculateNetSalary method. However, they are required 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 can 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, in implementing frameworks, which are semi-finished applications that must be customized by their clients before use. While template methods are not the only solution for this purpose, they represent an effective 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 that 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. Furthermore, assume that all these vehicles are stored in a list. Thus, we consider this list to be a polymorphic data structure, since it contains objects of different classes, all of which are subclasses of Vehicle.

Problem: In the parking control system, there is a recurring 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 are frequently required.

However, we want to implement these operations outside of the Vehicle classes using code similar to 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, we intend to use the following code to visit all the vehicles in the parking lot:

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

However, in this code, the invocation of the visit method depends on the dynamic types of both the target object (visitor) and 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. In fact, 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)
// Same for Motorcycle  

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

Given these constraints, our problem is as follows: how can we simulate double dispatch in a language like Java? By doing so, we can avoid the compilation error that occurs in the presented code.

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

To implement this pattern, we need to add an accept method to each class in the hierarchy (see the code below). In the root class, this method is declared as abstract and has a parameter of type Visitor. In the subclasses, the 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 can determine that it should call the visit implementation that takes Car as a parameter. It’s important to note that 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 supported in languages like Java.

abstract class Vehicle {
  abstract 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 an argument.

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

In summary, visitors facilitate the addition of operations to a class hierarchy. A visitor groups related operations together—in this example, printing the data of Vehicle and its subclasses. Moreover, we can implement other visitors, with different operations, such as persisting the objects to a database. However, whenever we add a new class to the hierarchy, like Truck, we must update all visitors with a new method: visit(Truck).

Before we conclude, it’s important to note that visitors have a disadvantage: they may require a violation of encapsulation in the classes to be visited. For instance, Vehicle may need to implement public methods that expose its internal state to allow visitors to access this information.

6.12 Other Design Patterns

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

List<String> list = Arrays.asList("a", "b", "c");
Iterator<String> it = list.iterator();
while (it.hasNext()) {
  String s = 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 concurrent traversals of the same data structure.

Builder is a design pattern that simplifies the instantiation of objects with several attributes, some of which may be optional. When the values of these attributes are not provided, they are initialized with default values. Rather than creating multiple constructors, each representing a possible combination of parameters, the initialization process is 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 is to implement the instantiation through constructors. However, this approach would require creating multiple constructors for the Book class, one for each combination of attributes. Moreover, using these constructors may lead to confusion, as developers need to be aware of the specific order of the parameters.

With the Builder pattern, the set methods clearly indicate which attribute of the Book is being initialized. Another alternative approach would be to implement 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 object’s attributes at any time. In contrast, using a Builder ensures that attributes can only be defined during instantiation.

The version of the Builder pattern presented here differs from the original description found in the Gang of Four book. Instead, we have described a version proposed by Joshua Bloch (link), which, in modern practice, represents the most common use of Builders. This version, for example, 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 improve the flexibility of a system’s design. For example, factories facilitate changes in the types handled by a program. A decorator allows customizing a class with new features, making it adaptable to various use cases. The Strategy pattern enables configuring the algorithms used by a class, to name just a few examples.

However, like almost everything in software engineering, the use of patterns also has a cost. For instance, a factory requires implementing at least one additional class in the system. Similarly, the Strategy pattern requires the creation of an abstract class and an additional class for each algorithm. Therefore, adopting design patterns requires careful analysis. To illustrate this type of analysis, let’s continue using the examples of the Factory and Strategy patterns:

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

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

John Ousterhout comments on this issue in his book (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.

The author illustrates his argument by citing 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 opening process in Java. His main argument is that, as a general rule, using a buffer is beneficial when opening any file. Therefore, buffers 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. (in Portuguese)

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. (in Portuguese)

Exercises

1. Mark T (True) or F (False) for each statement.

( ) 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 to subclasses.

( ) Iterator provides a way to access the elements of an aggregate 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 the addition of 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 we have a base class A. Also, assume 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 to the console. Name this decorator class LogChannel.

9. Consider 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, indicated with an (A) above.

10. Imagine you are a developer tasked with implementing Java’s I/O API. To avoid what we referred to as patternitis, you decide 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 buffering when needed?

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

PrintVisitor visitor = new PrintVisitor();

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

Assume 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 the Gang of Four (GoF) book, three of the authors 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 Agile Principles, Patterns, and Practices in C# by Robert C. Martin and Micah Martin, or you can explore the refactoring called Introduce Null Object in Martin Fowler’s refactoring book.