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 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.
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 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):
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 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:
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 always being kept 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 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.
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):
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 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:
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 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:
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 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:
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 facilitates 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 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:
Before implementing a factory, we should ask (and answer) the following questions: Will our system require the creation of objects of different types? Is it likely that these new types will be needed? 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 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:
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 of a data structure without changing their code.
Allows an object to notify other objects that 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 answered in question (2), which ones are creational patterns?
4. Considering the answers to question (2), list design patterns that:
Make a class open to extensions without needing to modify its source code, i.e., patterns that put into practice the Open/Closed principle.
Decouple two types of classes.
Increase class cohesion, i.e., make the class have 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 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.