Software Engineering: A Modern Approach
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 a design pattern. 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 recurring 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 the following 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.

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 apply a tested and validated design solution in their implementation.
When using 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 intend to reuse.
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 lacks 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 puts developers at 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):
Creational: patterns proposing flexible solutions for creating objects. They 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. They 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. They 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: 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 neither
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 rather 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 inherit from
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 use 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,
any code outside the class that tries to create a new instance using
new Logger()
will cause a compiler error. 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 implicit 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 one 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 another criticism regarding the use of Singletons:
they make automated testing more difficult. 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., caching, 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 from 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 functionality 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 remain 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:
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.
Allocating memory on demand for objects that consume a lot of memory. For instance, a class might need to use a high-resolution image. In this case, we can use a proxy to prevent the image from constantly being stored in the main memory. It is only loaded, possibly from disk, when certain methods are invoked.
Controlling 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. As a result, the base object’s class can focus on implementing only functional requirements.
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 could be classes from various manufacturers.
Furthermore, we’re showcasing only one method from each class, but they
likely contain other methods. The presented method is responsible for
turning on the projector. 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
turns on 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
match 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 field 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.
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 comparable 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 internal workings 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 system’s internal classes. 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 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
functionality to channels, such as buffers, message compression,
logging, etc. However, these functionalities are optional; depending on
the customer’s requirements, only some features—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 could be created (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. 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
inheritance-based solution is impractical, as it results in an explosion
of the number of communication channel classes.
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, the Decorator pattern 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 features.
Now, let’s examine the classes that serve as the decorators, 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 characteristics:
It implements the
Channel
interface, allowing it to be used wherever aChannel
is expected.It has an internal
Channel
object, to which the calls tosend
andreceive
are delegated.
Now, let’s examine the actual decorators. These classes must extend
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:
First,
ZipChannel.send
is executed, which compresses the message.After compression,
ZipChannel.send
invokessuper.send
, which, in turn, executesChannelDecorator.send
.ChannelDecorator.send
delegates the call to its internalChannel
, which, in this example, is aTCPChannel
.Finally, we reach
TCPChannel.send
, which transmits the compressed message via TCP.
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 regarding 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 replaceable. 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 algorithm 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 provided 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
update automatically.
Problem: We don’t want to couple
Temperature
(the domain class) to Thermometer
(the user interface class). The reason is straightforward: user
interface classes are prone to 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 using
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 operations:
addObserver
: In the example, this method is used in themain
function to add two thermometers as observers of aTemperature
object.notifyObservers
: In the example, this method is called byTemperature
when its value changes.
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()
.
The Observer pattern offers two key advantages:
Decoupling of Subjects and Observers: Subjects, such as
Temperature
, remain unaware of their observers. They publish events to announce changes in their state by callingnotifyObservers
. This decoupling allows for flexible reuse of subjects in different scenarios and enables the implementation of various types of observers for the same subject.Reusability of the notification mechanism: The Observer pattern provides a reusable notification mechanism for different subject-observer pairs. For example, the
Subject
class and theObserver
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 addresses 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 three
required 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
, 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 later 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 provide 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, because it contains
objects of different classes, all subclassing 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 commonly required.
However, we want to implement these operations outside 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 when selecting 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 and Julia.
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 to include 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 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 handling 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 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 object
attributes to be modified 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 is used, for example, 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 functionality, 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 creating 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:
Before implementing a factory, we should ask the following questions: Will our system require the creation of objects of different types? Is it likely that these new types will be required? If the answer is yes, then using a factory is justified. Otherwise, it’s better to create the objects using the
new
operator, which is the standard method for object creation in languages like Java.Before using the Strategy pattern, we should ask ourselves: Is there a need to parameterize the algorithms used by this class? Do our clients require alternative algorithms? If so, using the Strategy pattern is justified. Otherwise, it’s better to implement the algorithm directly in the class.
Although we used only two design patterns as examples, similar questions apply to other patterns.
However, many systems overuse design patterns, to the point where the
supposed gains in flexibility and extensibility become debatable.
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
into 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, deferring 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:
Offers a high-level interface that makes a system easier to use.
Ensures that a class has, at most, one instance and provides a unique access point to it.
Facilitates the construction of complex objects with various attributes, some of which are optional.
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.
Offers an interface or abstract class for creating a family of objects.
Provides a method to centralize the creation of a type of object.
Serves as an intermediary that controls access to a base object.
Allows adding new functionalities dynamically to a class.
Provides a standardized interface for navigating data structures.
Allows changes in the algorithms used by a class.
Makes a data structure open to extensions, allowing the addition of a function to each element without changing their code.
Allows an object to notify other objects when its state has changed.
Defines the skeleton of an algorithm in a base class and delegates the implementation of some steps to subclasses.
3. Among the design patterns identified in question (2), which ones are creational patterns?
4. Considering the answers to question (2), list design patterns that:
Make a class open to extension without needing to modify its source code, i.e., patterns that put into practice the Open/Closed principle.
Decouple two kinds of classes.
Increase a class’s cohesion, i.e., ensure the class has a single responsibility.
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 LGProjector
class to the
Projector
interface (the code for both is provided in
Section 6.5). Name this class LGProjectorAdapter
.
7. Suppose we have a base class A. Now 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 would we need to implement? And if we use a solution based on 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 logs the message to be transmitted or received to the console. Name
this decorator class LogChannel
.
9. Consider the following code for 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
(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. As we
discussed in Section 6.13, the buffering mechanism will be activated by
default in this new class. However, how would you implement an option to
disable buffering when needed?
11. Consider the Visitor example from 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 a 2009 interview with the InformIT website, marking 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, they would likely retain the original patterns and add some new ones that have become common since the book’s release in 1995. One of the new patterns they discussed in this interview is the Null Object. To understand its operation and benefits, you can find several articles online. For books, a good reference is Chapter 25 of Agile Principles, Patterns, and Practices in C# by Robert C. Martin and Micah Martin, or the refactoring “Introduce Null Object” in Martin Fowler’s refactoring book.