Introduction Software Design Java 2nd
Introduction Software Design Java 2nd
Robillard
Introduction to
Software Design
with Java
Second Edition
Introduction to Software Design with Java
Martin P. Robillard
Introduction to
Software Design
with Java
Second Edition
Martin P. Robillard
School of Computer Science
McGill University
Montreal, QC, Canada
This Springer imprint is published by the registered company Springer Nature Switzerland AG
The registered company address is: Gewerbestrasse 11, 6330 Cham, Switzerland
Preface
This book is inspired by well over a decade of teaching software design at McGill
University. At first, my focus was to explain the software design know-how avail-
able from high-quality references. Soon, however, I realized that the main challenge
of teaching software design lay elsewhere. Communicating how to apply a design
technique or use a programming language mechanism was relatively easy. The real
struggle was to convey in which context we want to use a certain design technique,
and why. To do this, I needed to explain what is going on in a software developer’s
head. Over time, my lectures came to be more about exploring the space of alterna-
tive design decisions one can make in a given context.
The goal of this book is to help readers learn software design by discovering the
experience of the design process. I share my knowledge and experience of software
design through a narrative that introduces each element of design know-how in con-
text, and explores alternative solutions in that context. The narrative is supported by
hundreds of code fragments and design diagrams.
My hope is that this book can serve as an effective resource and guide for learning
software design. However, I do not believe that it is possible to develop significant
design skills solely by reading a book. In my own learning process, I have benefited
hugely from reading other people’s code, regularly writing code, and relentlessly
refactoring existing code to experiment with alternative design solutions. For this
reason, this book emphasizes coding and experimentation as a necessary comple-
ment to reading the text. To support this aspect of the learning process, I provide a
companion website with practice problems, and two sample applications that cap-
ture numerous design decisions. An orientation through these sample applications
is provided in Code Exploration insets throughout the chapters.
As its title indicates, this book provides an introduction to software design using
the Java programming language. The code used throughout the book, as well as the
sample applications, are in Java (version 8). My use of the Java language, however,
is a means to communicate design ideas, and not the topic of the book. I aimed to
cover design concepts and techniques that are applicable in a host of technologies.
Many concepts (such as encapsulation), will be relevant in any technology. Others
(such as inheritance) will be paradigm-specific, but usable in multiple programming
v
vi Preface
The first chapter is a general introduction to software design. The subsequent chap-
ters provide a progressive coverage of design concepts and techniques presented as
a continuous narrative anchored in specific design problems. In addition to the main
content, the book includes different features to orient readers and help use the book
as a launchpad for further exploration and learning.
• Chapter Overview: At the beginning of each chapter, a callout lists the concepts,
principles, patterns, and antipatterns covered in the chapter.
• Design Context: Following the overview, a paragraph titled Design Context in-
troduces the design contexts that are used as running examples in the chapter. It is
thus not necessary to read all previous chapters to understand the code discussed
in a given chapter.
• Diagrams: Each chapter includes numerous diagrams that illustrate design ideas.
Although they are provided to illustrate the ideas in the text, the diagrams are also
realistic illustrations of diagrams that can be used in practice as part of design
discussions.
• Code Fragments: Each chapter includes many code fragments. The code gen-
erally follows the conventions presented in Appendix B, with occasional con-
cessions made to make the code more compact. A complete version of the code
fragments can be downloaded from the companion website (see below).
• Insights: In each chapter, the main numbered sections are followed by an un-
numbered section titled Insights. This section forms an actionable summary of
the key information and advice provided in the chapter. It is meant as a catalog of
applicable design knowledge, and assumes the material in the chapter has been
mostly assimilated. The insights are in bullet points to be easily perused.
Preface vii
• Code Exploration: At various points in the text, insets titled Code Exploration
provide a discussion of software design in practice. To facilitate good flow and
avoid getting lost in details, the design contexts discussed in the main chapters
are kept as simple as possible. As a result, some interesting aspects of the soft-
ware design experience do get lost in the simplification. The code exploration
activity is the opportunity to consider how some of the topics presented in the
chapter manifest themselves in reality. The Code Exploration insets points to
specific parts of the code of the sample applications. In concert with reading the
text of a Code Exploration inset, I recommend reviewing the code referenced
and trying to understand it as much as possible. The sample applications are de-
scribed in Appendix C. They include JetUML, the application used to create all
the diagrams in the book.
• Further Reading: The Further Reading section provides pointers to references
that complement the material presented in the chapter.
• Companion Website Additional resources for this book are available in the
repository https://github.com/prmr/DesignBook. The material in the repos-
itory includes a complete and commented version of the code that appears in the
chapter content, as well as practice exercises and their solutions.
• Sample Applications The two Java applications described in Appendix C were
developed following many of the principles and techniques described in the book,
and are provided as an accessible basis for additional study and exploration.
Acknowledgments
I am most grateful to Mathieu Nassif, who carried out a detailed technical review
of the entire manuscript of the first edition, providing me with hundreds of correc-
tions, suggestions, and interesting points for discussion. I warmly thank Jin Guo for
reviewing most of the chapters and testing some of the material in her own teach-
ing, and Alexa Hernandez, Kaylee Kutschera, Brigitte Pientka, and Clark Verbrugge
for feedback on various parts of the manuscript. I am also thankful to Ralf Gerst-
ner, the executive editor in charge of computer science at Springer, for believing
in the project from the start and for seeing it through with his usual diligence and
professionalism.
As this is the second edition, I am also very grateful to my readers and to the
instructors who have adopted the book to support their teaching. The enthusiasm
many have expressed has been a major source of motivation for me to continue this
project. The feedback I received, and the numerous interesting discussions I had
about the content, were an invaluable contribution to this revised text.
Martin P. Robillard
April 2019
December 2021
Contents
1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.1 Defining Software Design . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.2 Design in the Software Development Process . . . . . . . . . . . . . . . . . . . 6
1.3 Capturing Design Knowledge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.4 Sharing Design Know-How . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
Insights . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
Further Reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2 Encapsulation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.1 Encapsulation and Information Hiding . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.2 Encoding Abstractions as Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.3 Scopes and Accessibility . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.4 Object Diagrams . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
2.5 Escaping References . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
2.6 Immutability . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
2.7 Exposing Internal Data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
2.8 Input Validation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
2.9 Design by Contract . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
Insights . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
Further Reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
ix
x Contents
Insights . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
Further Reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
4 Object State . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
4.1 The Static and Dynamic Perspectives of a Software System . . . . . . . 67
4.2 Defining Object State . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
4.3 State Diagrams . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
4.4 Designing Object Life Cycles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
4.5 Nullability . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
4.6 Final Fields and Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
4.7 Object Identity, Equality, and Uniqueness . . . . . . . . . . . . . . . . . . . . . . 83
4.8 The F LYWEIGHT Design Pattern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
4.9 The S INGLETON Design Pattern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
4.10 Objects of Nested Classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
Insights . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
Further Reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
5 Unit Testing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
5.1 Introduction to Unit Testing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
5.2 Unit Testing Framework Fundamentals with JUnit . . . . . . . . . . . . . . . 102
5.3 Organizing Test Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104
5.4 Metaprogramming . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
5.5 Structuring Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
5.6 Tests and Exceptional Conditions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
5.7 Encapsulation and Unit Testing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116
5.8 Testing with Stubs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118
5.9 Test Coverage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120
Insights . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
Further Reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124
6 Composition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
6.1 Composition and Aggregation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
6.2 The C OMPOSITE Design Pattern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
6.3 Sequence Diagrams . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
6.4 The D ECORATOR Design Pattern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
6.5 Combining C OMPOSITE and D ECORATOR . . . . . . . . . . . . . . . . . . . . . 142
6.6 Polymorphic Copying . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
6.7 The P ROTOTYPE Design Pattern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
6.8 The C OMMAND Design Pattern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
6.9 The Law of Demeter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
Insights . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155
Further Reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156
Contents xi
7 Inheritance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157
7.1 The Case for Inheritance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
7.2 Inheritance and Typing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160
7.3 Inheriting Fields . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162
7.4 Inheriting Methods . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
7.5 Overloading Methods . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170
7.6 Polymorphic Copying with Inheritance . . . . . . . . . . . . . . . . . . . . . . . . 171
7.7 Inheritance Versus Composition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174
7.8 Abstract Classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177
7.9 Revisiting the D ECORATOR Design Pattern . . . . . . . . . . . . . . . . . . . . . 181
7.10 The T EMPLATE M ETHOD Design Pattern . . . . . . . . . . . . . . . . . . . . . . 182
7.11 Proper Use of Inheritance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186
Insights . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 192
Further Reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193
References . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 289
Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 291
Chapter 1
Introduction
In 1988, a fascinating little piece of code hits the limelight. That year, one of the
winners of the annual International Obfuscated C Code Contest features a program
that writes out to the terminal console the text of an eighteenth-century poem titled
The Twelve Days of Christmas. Figure 1.1 shows the first three verses of the text, as
they appear on the output console when executing the code. This poem is particular
in that its text has a regular structure. Text with such a structure is amenable to being
constructed by software in a way that goes beyond printing hard-coded data. With
a poem like The Twelve Days of Christmas, there was thus opportunity for creating
a clear and compact solution for displaying a poem on the console. However, as
promised by the name of the contest where it was featured, the program is anything
but clear. If fact, its inner workings are unfathomable. Figure 1.2 reproduces the
complete code of the program.
Fig. 1.1 Partial output of The Twelve Days of Christmas program of Figure 1.2
This quirky piece of computer science trivia illustrates the impact of a lack of
self-evident structure in software. Here, we have a programming problem with triv-
ial requirements: the functionality of interest requires no input and produces a sin-
Fig. 1.2 Source code of the 1988 The Twelve Days of Christmas C program by Ian Phillips. This
code compiles and executing it will produce the output illustrated in Figure 1.1. © 1988, Landon
Curt Noll and Larry Bassel. Reproduced with permission.
gle, unchangeable output. Yet, the code to support this functionality cannot be un-
derstood by a normal human being. But what is the problem, if the code works?
Software needs to change, and for software to change, at least one person must
be involved at some point. Software needs to change for a variety of reasons, from
fixing bugs to adapting the code to an evolving world. For example, many of the
gifts referred to in the poem are European birds (e.g., partridge, turtle doves, French
hens). Contemporary software development best practices include the localization
of software applications, namely, the option to tailor a software application to ac-
count for region-specific characteristics. It would thus be nice to adapt the code of
the application to replace the name of European birds to some that readers could re-
late to based on their own region (for example, to replace partridge with turkey for
North American users). To modify a piece of code, however, one must understand
its structure, and this structure must, to a certain extent, accommodate the change.
In the case of The Twelve Days of Christmas, any ambition to ever change the code
is hopeless.
The example of The Twelve Days of Christmas is facetious for sake of illustration.
Because this code was obfuscated on purpose, it would be comforting if we could
discount it as irrelevant. Unfortunately, because writing messy code is often the
path of least resistance in the complex social, technological, and economic reality
of software development, badly designed code is not hard to find. For example, in a
famous high-profile case where automotive software was determined by the courts
to be responsible for a fatal accident, the experts who reviewed the software likened
its structure to that of a bowl of spaghetti. Whether code is cryptic purposefully
or accidentally, the result is similar: it is hard to understand and change without
introducing errors.
1 Introduction 3
To explore the contrast, let us design a version of the program where the structure
is evident. Consistently with the rest of the code in this book, the program is in Java.
First, we can tackle the issue of producing the first line of a verse:
static String[] DAYS = {"first", "second", ..., "twelfth"};
This code is clear because the function is short, it abstracts an obvious concept
(the creation of the first line), and the only parameterization involved maps directly
to the problem domain (changing the day).
The second sub-problem is to create the list of gifts for a given day. In this case
we can leverage the inherent recursion in the poem’s structure to organize the code
in a function that creates a list of gifts by adding the last gift to a smaller list of gifts:
static String[] GIFTS = { "a partridge in a pear tree",
"two turtle doves", ... };
At a glance, we see the overall structure of the code: a special case for the first
verse, then an iteration through the remaining eleven verses, where each verse is
created by concatenating the output of two functions: one to create the first line, and
the other to create the list of gifts.
4 1 Introduction
ciples, and design techniques. In fact, the heuristic nature of the software design
process is what makes it an exciting creative activity.
Optimality
Dimension B
Acceptable
Possible
Dimension A
The quality attributes that constitute the dimensions of the design space also cor-
respond to the general goals of design. One of the most important goals for software
design is to reduce the complexity of software, which means making it easier to
understand. Cleanly-designed code that is easy to understand is less error-prone and
also easier to modify. Messy code obscures the important decisions of its original
developers. When developers ignore existing design constraints, they risk modifying
code in a way that does not agree with the original structure, and thereby introduce
errors and generally degrade the quality of the code. The problem of modifying code
in a way that does not respect the original structure has been called ignorant surgery
(see Further Reading).
In general, the relative importance of design goals depends on the context in
which a piece of software is being designed. A design context is a specific set of
requirements and constraints within a domain in which a design solution must be
found and integrated. For example, because of economic or contractual reasons, it
may be required to design a particular piece of software to maximize its reusability.
Or, if a piece of code is intended to be integrated into safety-critical applications,
it may be more important to prioritize robustness (i.e., resilience to errors). In this
book, I give a lot of importance to the understandability quality attribute. I try to
emphasize designs where the code itself reveals the underlying design decisions
and the intent behind these design decisions. The idea of having design decisions be
self-evident in code is a property I call sustainability.
6 1 Introduction
Design is only one of the many activities that take place during the development of
a software system. There is an abundant literature on different process models for
software development. A process model describes (and sometimes prescribes) how
the different steps required to create a system are organized. Different process mod-
els offer different ways of doing things for different reasons. In the early days of
the software engineering discipline it was believed that a planning-heavy process,
exemplified by the waterfall software process model, was the desirable way to build
high-quality software. However, in the mid-1990s this belief was challenged by a
movement towards a more organic approach to software development, also called
agile development. In practice, ideas about how to best develop software keep evolv-
ing, and in the end the important things are to have a development process in the first
place, and for that process to be well-adapted to the type of system being developed
and the organization that develops it. For example, the process used by an organiza-
tion to develop a prototype for a video game would probably be different from the
process used to develop banking or aeronautical software.
The issue of devising, adapting, or even following a software development pro-
cess is not the main focus of this book. However, even when beginning to learn
about software design, it is useful to have a general idea of software development
processes, if only to stay oriented in the wide and buzzword-laden realm of technol-
ogy.
One concept of the software development process literature that is related to soft-
ware design is the idea of a software development practice. A practice is a well-
understood way of doing something to achieve a certain benefit. An example of a
practice many programmers are familiar with is version control (the use of software
tools to keep track of changes to software development artifacts). Another example
of software development practice is pair programming (writing code as a team of
two in front of a single computer). In this book I refer to a number of software de-
velopment practices that directly support good design, including the use of coding
conventions (see Appendix B) and refactoring (see below).
1.3 Capturing Design Knowledge 7
A design (or design solution) is a collection of decisions, each of which is the result
of a search process through a design space for a particular design problem, or con-
text. This definition naturally leads to the question of what a design decision looks
like. Informally, we could say that a design decision is a statement about how to do
a particular thing, ideally coupled with the reason for this statement. An example
would be: We will store the appointments in a linked list because we will mostly be
performing additions to the list. For this decision to even exist, it has to be in at
least one developer’s brain at some point. We thus have a first medium for storing
design decisions: a person’s brain. For small projects, this could be sufficient. How-
ever, given the ephemerality of human memory, it can often be worthwhile to record
important design decisions externally. This opens up the question of how to capture
design knowledge. This fundamental question has been the subject of debate and
academic and industry research for decades. Entire books have been written on the
topic. Given that the present book is not one of them, the following is a concise
summary of the options for externalizing design knowledge:
8 1 Introduction
• Source code: Many design decisions can be captured directly in the source code.
The example above, of selecting a linked list as a data structure, would be one
case. The advantage of source code is that it is a formal language whose rules
are checked by the compiler. Unfortunately, source code is not a good substrate
for capturing the rationale of design decisions. For this purpose, code comments
can be of some assistance.
• Design documents: Design decisions can be captured in documents specifically
aimed at capturing such design decisions. There exists a wide variety of for-
mats for documents about software, from standardized design documents to blog
posts. Design documents may also include diagrams, which are another way to
represent design decisions.
• Email, discussion platforms, and version control systems: Design information
can be captured in email and comments stored in software development tools,
such as issue management systems and version control systems.
• Specialized models: In certain software development projects, developers use
formal models to specify many aspects of the software. These models can then
be automatically converted into code in a programming language. Such an ap-
proach is called generative programming or model-driven development (MDD).
In model-driven development, the models serve as design documents. As a soft-
ware construction approach, model-driven design and development is outside the
scope of this book.
Because the level of design abstraction covered by this book remains close to the
source code, many of the design decisions discussed will be at least partly reflected
in the code. Subsequent chapters will also contain many diagrams and accompany-
ing text that document design decisions.
There will often be situations where we need to discuss design problems and so-
lutions that are cumbersome, inconvenient, or too complex to describe using either
source code or natural language. For this purpose we can use a specialized mod-
eling language. This situation is not limited to software. For example, describing
instrumental music in plain language is near-impossible: instead, we use musical
notation.
Historically, many different modeling languages and notations have been devel-
oped for representing, at an abstract level, various aspects of a software system. This
disparity was, however, an obstacle to adoption because of the overhead involved in
interpreting models expressed in an unfamiliar notation. Thankfully, in the mid-
1990s the main software modeling notations were merged into a single one, the
Unified Modeling Language (UML), which was subsequently adopted as a standard
by the International Organization for Standardization (ISO).
The UML is a modeling language organized in terms of different types of dia-
grams intended to illustrate different aspects of software. Examples of design in-
formation than can be neatly captured in the UML include relationships between
1.4 Sharing Design Know-How 9
classes (e.g., A inherits from B), changes in the state of an object (e.g., the list ob-
ject goes from Empty to Non-Empty when the first element is added), and sequences
of calls dispatched on objects (e.g., a.m1() results in a call to b.m2()).
Not all development teams use the UML. However, those who do use it in dif-
ferent ways for different reasons. For example, UML can be used to produce formal
design documentation in waterfall-type development processes. Others use the UML
to describe enough of the software to be able to automatically generate the code from
the models, following the idea of generative programming. In this book, I use the
UML simply for sketching design ideas. The diagrams included in this book are not
expected to be automatically transformable into code. I also use the smallest subset
of the modeling language necessary, and introduce the notation progressively.
An important thing to remember about UML diagrams is that they are models.
This means that they are not intended to capture every single detail of a solution.
Ideally, a UML diagram will focus on illustrating a single main idea and only in-
clude the relevant information. In UML diagramming it is a common practice to
leave out the parts of a system and details that are not directly relevant to the point
being illustrated.
Capturing knowledge about the design of a particular system is one thing, but how
do we capture general know-how about the design process? Software design is influ-
enced by the skills and experience of the designer, and this type of heuristic knowl-
edge is not easy to synthesize and package for dissemination. In earlier days, orga-
nized approaches to disseminate design know-how centered around comprehensive
design methods, which prescribed a sequence of steps and the use of specialized
charts and other instruments. Such approaches peaked in the 1980s, and were re-
placed with adaptations suited to object-oriented programming, a paradigm that
was then quickly gaining adoption. Comprehensive object-oriented design meth-
ods themselves peaked in the mid-1990s. At that time, it was being observed that
some elements of design solutions tended to recur between many object-oriented
applications.
Design Patterns
The idea of reusing elements of object-oriented design was captured in the con-
cept of a design pattern in the book Design Patterns: Elements of Reusable Object-
Oriented Software [6]. This book, often referred to as the Gang of Four book from
the author list, is one of the most influential software design books in existence.
Following the concept of an architectural pattern originally proposed by an archi-
tect named Christopher Alexander, the book describes 23 patterns for addressing
common software design problems. Since then, countless other patterns have been
10 1 Introduction
documented. The idea to capture abstract design solutions that address specific prob-
lems was a breakthrough for software engineering, because it provided a practical
way to convey design know-how and experience without the requirement to adopt
a comprehensive design method. To this day, design patterns and close variants of
the concept have been a dominant way to capture design know-how. There currently
exist countless design catalogs for different programming languages, in the form of
books and websites.
According to the Gang of Four, a pattern has four essential elements:
The pattern name is a handle we can use to describe a design problem, its solutions, and
consequences in a word or two. Naming a pattern immediately increases our design vocab-
ulary. It lets us design at a higher level of abstraction. Having a vocabulary for patterns lets
us talk about them with our colleagues, in our documentation, and to ourselves...
The problem describes when to apply the pattern. It explains the problem and its con-
text...
The solution describes the elements that make up the design, their relationships, respon-
sibilities, and collaborations. The solution doesn’t describe a particular concrete design or
implementation, because a pattern is like a template...
The consequences are the results and trade-offs of applying the pattern... [6]
In this book, I present a subset of the original patterns by integrating them in the
flow of the material when they become relevant. I do not reproduce the structured
description that can be found in other pattern catalogs. I instead use a lightweight
description for a pattern that focuses on the link between the problem and solution,
and I include a discussion of important design decisions related to the pattern. I also
prefer to refer to the problem as the context for applying a pattern, because design
problems can sometimes be difficult to isolate. Finally, I will sometimes express
the solution embodied by a pattern as a UML diagram that captures the name of
the abstract elements of the pattern. Because these elements are abstract, I prefer
to refer to them as a solution template rather than a solution. A typical task when
attempting to apply a design pattern in a context is to map the abstract elements of
the solution template to concrete design elements in the code. In the text, the name
of design patterns are set in S MALL C APS FONT. This is to indicate that a term refers to
a well-defined design concept, as opposed to a general use of the term. For example,
one design pattern is called the Strategy pattern. Instead of continuously referring
to it as the Strategy design pattern, I will refer to it as the S TRATEGY, which will
distinguish it from the concept of a strategy as a general problem-solving approach.
Because solution templates for design patterns can be looked up in any number
of resources, the most important skill to develop with respect to design patterns is
to know where to apply them. For this reason, my coverage of design patterns em-
phasizes the rationale for using a pattern and a discussion of its strengths and weak-
nesses in different contexts, and de-emphasizes the focus on solution templates. One
potential pitfall when first learning about design patterns is to get over-enthusiastic
and try to apply them everywhere. Like all other elements of design solutions, a par-
ticular instance of a design pattern will occupy a specific point in the design space,
with attendant benefits and drawbacks. If I can make one generalization about the
use of design patterns, it is that employing one tends to make an overall design
more flexible. Sometimes, this flexibility is exactly what we need. At other times, it
Insights 11
is overkill and leads to unnecessary structures and clutter in the code. In other words,
using a particular design pattern in a particular way in a given context is a design
decision which, like most other design decisions, should be critically assessed.
Design Antipatterns
An interesting take on the idea of a design pattern is that of a design antipattern. Just
as it can be observed that some design solution elements recur between applications,
it is also the case that recognizable flaws can be abstracted from many similar cases
and catalogued. This influential idea took hold around the turn of the millennium
in a popular book on refactoring, which documents 22 antipatterns as motivation
to refactor the corresponding code [5]. Typical antipatterns include problems such
as D UPLICATED C ODE†, L ONG M ETHOD†, and others that will be covered in this book.
For reasons similar to design patterns, antipatterns are set in S MALL C APS†, but are
followed by a dagger symbol to distinguish them from actual patterns. Design an-
tipatterns are also known as code smells, or bad smells (in code), to convey the idea
of a symptom that something is not quite right.
Insights
This chapter introduced software design and placed it in the general context of soft-
ware development projects.
• The verb to design refers to the process we follow when we design software, and
the noun a design refers to the outcome of this process;
• The process of software design is the construction of abstractions of data and
computation and the organization of these abstractions into a working software
application;
• There is rarely a single right answer to a design problem, only solutions that are
better or worse in some dimensions;
• A design artifact is an external representation of one or more design decisions;
• Design is only one of many activities that take place during the development of a
software system. Software development follows a process that can vary from or-
ganization to organization, and vary from planning-heavy to agile. Development
processes typically involve iterations;
• A software development practice is a well-understood way of doing something to
achieve a certain benefit. Examples include version control, coding conventions,
and refactoring;
• Design knowledge can be captured in source code, code comments, specialized
documents, discussion forums, and models;
• The Unified Modeling Language, or UML, is a modeling language organized
in terms of different types of diagrams. Using UML can be an effective way to
illustrate different aspects of software without getting caught up in details;
12 Further Reading
Further Reading
The paper Software Aging by David L. Parnas [12] introduces the term ignorant
surgery and provides a compelling motivation for the benefits of maintaining good
design in software. Parnas is one of the early contributors to the software engineer-
ing discipline. Chapter 1 of the book Clean Code: A Handbook of Agile Software
Craftmanship by Robert C. Martin [7] discusses the various ills of bad or “messy”
code. My short paper titled Sustainable Software Design discusses in more detail
what it means for design decisions to be self-evident [14].
Chapter 1 of the book UML Distilled, 3rd Edition by Martin Fowler [5] provides
a more comprehensive introduction to the UML. Fowler distinguishes between three
modes for using the UML: as sketches for design, as a blueprint for creating an ap-
plication, and as source code that can be executed. Sketching is the mode employed
in this book.
The original book on design patterns is Design Patterns: Elements of Reusable
Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, and
John Vlissides [6]. This book is often referred to as the Gang of Four book. Be-
cause it predates the UML, the notation it uses for capturing software designs may
feel a bit foreign. Nevertheless, it is a timeless reference work.
The book Refactoring: Improving the Design of Existing Code, also by Martin
Fowler [3] is the main reference on the practice of refactoring. It introduces the idea
of design antipatterns (which are called code smells in the book). Robert C. Martin
also includes a list of bad smells in Chapter 17 of Clean Code, cited above.
Chapter 2
Encapsulation
Design Context
tion is not to be confused with the action of depicting the card on a user interface
component.
As our first design task, we define the abstractions that are necessary to represent a
deck of cards. An abstraction is a conceptual building block for a software system.
Examples of common abstractions in computing include data structures (for exam-
ple, stack, list) and operations (sorting, iterating). However, abstractions can also
refer to ideas in the problem domain, such as playing card. With the term defining
an abstraction, I mean deciding what the abstraction represents, and what it will
look like in terms of source code. In the case of a deck of cards, the first part of
the process is straightforward, because the concepts we need to represent in code
2.2 Encoding Abstractions as Types 15
(a playing card, a deck of cards) are well-defined in the real world. This will not
always be the case.
Essentially, a deck of cards is an ordered collection of playing cards. We could
use any standard data structure to represent this collection (an array, a list, etc.).
However, what would such a collection hold? What is a card? In the code, we can
represent a playing card in many different ways. For example, we could use an
integer between 0 and 51 where the value represents a certain card according to a
convention. For example, Clubs could have numbers 0–12 in increasing rank, Hearts
13–25, etc.:
int card = 13; // 13 = The Ace of Hearts
int suit = card / 13 // 1 = Hearts
int rank = card % 13; // 0 = Ace
This approach would also require us to have similar conventions to represent suits
and ranks, as illustrated on the second and third lines.1 To avoid having to contin-
ually divide and multiply numbers that represent cards to switch between suits, we
could also represent a card as a pair of values, the first one encoding the suit, and
the second one encoding the rank (or vice-versa):
int[] card = {1,0}; // The Ace of Hearts
While we are at it, we could even decide to represent a card using a combination
of six Boolean values. Although extremely inconvenient, this design decision is
technically possible to implement: it is an example of a decision that is possible, but
not acceptable (see Section 1.1). As it turns out, all three options above have major
drawbacks.
First, the representation of a card does not map to the corresponding domain
concept. To facilitate code understanding and help avoid programming errors, the
representation of values should ideally be tied to the concept they represent. For
example, the general type int maps to the concept of an integer (a type of number),
not that of a playing card. We could define a variable of type int intended to store
a playing card, and unwittingly put a value that represents a different entity in it
(e.g., the number of cards in the deck). This will not be noticed as an error by the
compiler, yet it is likely to lead to intense confusion when executing the code.
Second, the representation of a card is coupled to its implementation. If our de-
sign decision is that cards should be represented as integers, any location in the code
that must store a value that represents a card will refer to an integer. Changing this
encoding to something else (for example, the two-element array discussed above)
will require discovering and changing every single location where an int variable
is used to store a card, and all the code that works with cards as integers.
Third, it is easy to corrupt a variable that stores a value that represents a card. In
Java a variable of type int can take 232 distinct values. To represent a playing card
we only need a tiny subset of these (52 values). Consequently, the overwhelming
majority of values we can store in an int variable (232 − 52) intended to represent a
playing card does not represent any valid information. This opens the door to errors.
1 The modulo operator (%) returns the remainder of the integer division.
16 2 Encapsulation
The problem would have been even worse had we decided to use a two-element
array of type int, which supports 264 + 1 values.2
We can do better. It is generally a bad idea to try to shoehorn domain concepts
into basic general types like int, String, and so on. Ideally, these types should only
be used to hold values that are proper values of the type. For instance, the int type
should only be used to hold actual integers (and perhaps very similar concepts, such
as currency). Similarly, Strings should be used only to hold sequences of characters
meant to represent text or text-like information, as opposed to being some encoding
of some other concept (e.g., "AceOfClubs"). The tendency to use primitive types to
represent other abstractions is a common problem in software design that has been
captured by the antipattern P RIMITIVE O BSESSION†.
To apply the principle of information hiding, we instead organize our code to
hide the decision of how exactly we represent a card. We hide this decision behind
an interface specifically tied with the concept of a card. In programming languages
with a strong support for types, such as Java, this is typically accomplished through
the use of types. In our case, to properly represent a card in code, we define our own
type Card as a Java class:
class Card {}
As we will see below, the use of a specific type to represent a card will allow us
to hide the decision of how we represent a card internally. However, although we
now have a class Card, we still need to decide how to represent a card within the
class. All options are back on the table. We could do simply:
class Card {
int aCard; // 0-51 encodes the card
}
This class defines a single instance variable aCard of type int. The name of the
instance variable includes the prefix a as part of a coding convention detailed in
Appendix B. Client code can refer to this variable through dereference (see Sec-
tion A.2), for example:3
Card card = new Card();
card.aCard = 28;
Although using a class somewhat links the value a bit better to the domain con-
cept of a card, the other problems are still present. First, it is still possible to corrupt
the representation of a card. Second, the decision to represent this value as an int
is not exactly hidden, given that client code would be accessing the variable di-
rectly through a dereference of the instance variable. Let us then tackle the issue
of representing the card internally. The next section handles the issue of hiding this
decision.
2 The additional value comes from the fact that array-typed variables can also be null.
3 Technically, this code only compiles if placed in a method declared in a class that is in the same
package as class Card. This detail is not important here. Section 2.3 explains where class members
can be accessed in the code.
2.2 Encoding Abstractions as Types 17
Two key observations can help us arrive at a better way to encode a card. First,
the value of a playing card is completely and exactly defined in terms of two sub-
concepts: its suit (e.g., Clubs) and its rank (e.g., Ace). So, we can take the process
of decomposition one step further, and define abstractions for ranks and suits. Fol-
lowing the same logic as above, primitive values are not a good match for encoding
these abstractions, so we prefer to use a dedicated type. However, here the second
important observation comes into play: the rank of a playing card can only be one of
13 distinct values, which are known in advance and can be enumerated. In the case
of suits, the number of values is even smaller (four). The best tool at our disposal to
encode such abstractions is the enumerated type:
enum Suit {
CLUBS, DIAMONDS, SPADES, HEARTS
}
In Java, enumerated types are a special kind of class declaration. The identifiers
listed in the declaration of the enumerated type are globally available constants (see
Section A.3 in the appendix). These constants store a reference to an object of the
class that corresponds to the enumerated value. For example,
Suit suit1 = Suit.CLUBS;
Suit suit2 = Suit.CLUBS;
boolean same = suit1 == suit2; // same == true
Enumerated types are a perfect fit in our situation. They meet all our design
requirements, because variables of type Suit and Rank are directly tied to their
corresponding concept of rank and suit, and variables of these types can only take
values that are meaningful for the type.4 Enumerated types are a simple yet effec-
tive feature for creating or implementing robust designs. They help avoid P RIMITIVE
O BSESSION† and generally make the code clearer and less error-prone.
The code below completes our definition of class Card as a combination of a
rank and a suit value. It assumes that each enumerated type is defined in its own file.
enum Suit {
CLUBS, DIAMONDS, SPADES, HEARTS
}
enum Rank {
ACE, TWO, ..., QUEEN, KING
}
class Card {
Suit aSuit;
Rank aRank;
}
Now that we have a reasonable type to represent a playing card in the code,
we return to the issue of representing a deck of cards, and follow the same line of
thinking. Because a deck is just a collection of cards, we could represent a deck of
cards as a List of Cards:
4 With the unfortunate exception of null. See Section 4.5.
18 2 Encapsulation
However, the disadvantages of this approach are the same as the disadvantages of
representing a playing card as an int value:
• A list of cards is not strongly tied to the concept of a deck. It could represent any
list of cards, e.g., the cards in one of the piles created while playing Solitaire, the
cards discarded as part of the game, etc.
• Using a list of cards ties the representation of a deck in the program with its
implementation. If we decide later to replace the list by, say, an array, we would
have to change all the corresponding source code locations.5
• The structure can easily be corrupted: a simple deck of cards can hold a max-
imum of 52 cards, without duplicates. A list allows one to put any number of
cards in the structure, including duplicates.
A better way to approach the representation of a deck of cards in our code is to
also define a proper type for it:
class Deck {
List<Card> aCards = new ArrayList<>();
}
Although it may seem redundant to define a new class to hold just one instance of an
ArrayList, this decision helps avoid many of the problems discussed above. The
new type Deck specializes the list and ties it directly to its corresponding domain
concept. It also becomes possible to hide the decision of how the cards are stored.
The remainder of this chapter presents the details of how to achieve this hiding in
practice.
5 Disturbingly, replacing the list by a Stack on the right-hand side of the assignment in the listing
would actually work because in Java Stack is a subtype of List. In Chapter 7, I explain why
this is disturbing.
2.3 Scopes and Accessibility 19
Encoding abstractions as types is only the first step in the process of encapsulation.
Once we have types we feel are good abstractions for our design, we need to ensure
that these types are effective in hiding information from client code. At this point we
have determined that four types are necessary to represent a deck of cards in code:
Deck, Card, Rank, and Suit. Each of these types defines a set of possible values
and a set of operations on these values. We now turn to the problem of specifying
the values these types can take and the operations on these types so as to achieve
good encapsulation of both the values and computation.
In Java and most other object-oriented languages, an object is a mechanism to
group variables together and access their values through the process of derefer-
encing (see Section A.2 in the appendix). Without encapsulation, any variable that
forms part of an object can be accessed indiscriminately. For example, given the
following code:
20 2 Encapsulation
class Deck {
public List<Card> aCards = new ArrayList<>();
}
class Card {
public Rank aRank = null;
public Suit aSuit = null;
}
has a compilation error because in the second assignment, the reference to a cannot
be resolved because, according to Java scoping rules, it is not visible in the second
scope. In this tiny example, the scoping restriction may look like a limitation. How-
ever, it is actually a powerful feature. To understand what happens in the second
statement, we only need to track down references to variables that are in scope (as
opposed to every code location). We can do the same with classes.
6 This code is not defined in any method because its exact location does not matter. For example,
the code could be placed in a main method.
2.3 Scopes and Accessibility 21
In Java, it is possible to control the visibility of classes and class members (and
in particular fields) through the use of access modifiers. In this chapter, I only focus
on the distinction between the public and private access modifiers.7 Members
marked public are visible anywhere in the code. In the example above, because
the field aCards of class Deck is public, the variable aCards of any object of type
Deck is visible from any code that has a reference to the object. In contrast, members
marked private are only visible within the scope of the class, namely, between the
left curly brace that begins the declaration of the class body and the last right curly
brace of the class declaration.
A general principle for achieving good encapsulation is to use the narrowest pos-
sible scope for class members. Thus, instance variables should almost always be
private. Also, public methods should reveal as little as possible about imple-
mentation decisions meant to be encapsulated. A revised design for class Card that
respects this guideline is as follows:
public class Card {
now, we will keep things simple and consider that the interface to a class is the
set of its public methods. This is only a starting point, and we will be refining this
definition of interface as we go along, and in particular in Section 3.1. For now, what
is important is that the public methods of a type (a class) represent what client code
can do with objects of the type, and the design of all other (non-public) fields and
methods remains hidden from that client.
An object diagram is a type of UML diagram (see Section 1.3) that represents ob-
jects and how they refer to each other. Whenever a new statement is executed, an
object of a class is created and a reference to this object is returned and can be
passed around. It can often be useful to visually represent the resulting graph of
objects and their inter-dependent references, especially when groups of objects are
structured or organized in sophisticated ways, or when relations between objects are
especially important.
I introduce a slight enhancement to official UML object diagrams, so as to pro-
vide a representation of an object’s fields and values that resembles the kind of
data-structure diagrams often used in introductory computer science classes. In an
object diagram, a rectangle represents an object, with its name and type indicated
as name:type. Both name and type information are optional, but in general it is
useful to have at least one of the two. In UML diagrams in general, the name of
objects (as opposed to classes) are underlined. Objects can contain fields, which are
just like fields in a Java program. Fields can contain a value of a primitive type or
a value of a reference type (see Section A.1 in the Appendix). When the value is a
reference type, it is represented as a directed arrow to the object being referenced.
Let us consider the diagram of Figure 2.1:
Fig. 2.1 Object diagram showing a detailed model of the object graph for a deck of cards
2.4 Object Diagrams 23
This diagram models an instance of the Deck class named deck. It would have been
fine to omit this name and simply indicate :Deck in the rectangle, as in the case of
ArrayList<Card>, which is anonymous. This deck has a field aCards whose cur-
rent value is a reference to an ArrayList<Card> object. The ArrayList<Card> ob-
ject’s elementData field references two Card instances. Here, because ArrayList
is a library type, it is necessary to have knowledge of the source code of the library
to accurately model objects of this class. However, for a design sketch, using the
actual name is not critical. To model internal properties of library types without
looking up all their implementation details, it is often sufficient simply to make up
evocative names. For example, the diagram would be just as informative if the field
had been named data or elements.
Through modeling, we can skip over some details. In reality, in an instance of
ArrayList the elementData field refers to an array of Object-typed cells that
contain the actual data. This information is not useful here, and we link directly
to the contained data. It is also worth noting how the list refers to two cards, and
not three or four or 52. Another important point about object diagrams is that they
represent a snapshot in the execution of a program. Here it was at the point where the
list had two cards. For the purpose of communicating design information, including
only two cards is sufficient to illustrate that a deck is a list of cards, so it would
not be worth it to depict a snapshot of the program when the deck contains more
cards. The two Card instances, however, are modeled in full detail. The values of
enumerated types are distinguished by name, as they should be, and the enumerated
value Suit.CLUBS is shared between two cards. I will cover reference sharing in
more detail in Chapter 4.
Fig. 2.2 Object diagram showing a simplified model of the object graph for a deck of cards
The second example diagram (Figure 2.2) illustrates some of the additional mod-
eling simplifications we can do, when appropriate. First, I added an untyped object
named main. This “object” is actually a trick for representing a method body. Ob-
ject diagrams do not have an explicit notation to represent code statements that form
the body of a method declaration. However, this can be achieved through untyped
objects by observing that, from the point of view of the diagram, an object and a
method body are simply collections of variables (instance variables in the first case,
local variables in the second). A second difference is that the Deck object is now
anonymous, and the name deck is used to represent the variable in which a refer-
ence to an object (any object) is stored, as opposed to a specific object. Third, the
main method contains a name variable that stores a string. In Java, strings are tech-
24 2 Encapsulation
The use of the visibility restrictions for fields using the private keyword provides
a basic level of encapsulation, but it by no means ensures an iron-clad protection of
internal structures. We explore this problem by returning to the issue of storing an
aggregation of Card objects within an instance of a Deck object. Let us assume we
decided to implement a Deck as a list of cards using Java’s ArrayList type.8
public class Deck {
public Deck() {
/* Add all 52 cards to the deck */
/* Shuffle the cards */
}
So far, the only way to use an instance of Deck from code outside the class is
to draw a card from the deck: there are no other members (methods or fields) that
could be referenced outside the class. The class is thus very well encapsulated, but
also very limited it the services it can offer. Let us assume the client code needs to
inspect the content of the deck. We could simply add a getter method to the class:
8 It may appear that Stack could be a better choice, but I prefer to avoid this type because its
implementation is victim of a design flaw discussed in Chapter 7.
2.5 Escaping References 25
Unfortunately, this solution solves the problem of providing access to the content
of the deck at a great cost: it allows a reference to the private internal list of cards to
escape the scope of the class, thus granting access to internal elements of the class
from outside the class. For example:
Deck deck = new Deck();
List<Card> cards = deck.getCards();
cards.add(new Card(Rank.ACE, Suit.HEARTS));
Here, the reference to the list of cards held within an instance of Deck escaped
into the scope of the client code, which can then use it to mess things up, for example
by adding an additional Ace of Hearts.
Clearly, declaring fields private is insufficient to ensure good encapsulation. If a
class is well encapsulated, it should not be possible to change the data stored by an
object without going through one of its methods. In turn, to achieve this encapsula-
tion quality, it is also necessary to prevent references to internal structures to escape
the scope of the class. There are three main ways in which a reference to a private
structure can escape the scope of its class: returning a reference to an internal ob-
ject, storing an external reference internally, or leaking references through shared
structures.9
This problem is demonstrated above through the use of the getter method. It is not
a good idea to automatically supply getters and setters for each field because, as in
this case, it may result in a degradation of encapsulation. Figure 2.3 shows the effect
of this escape.
The problem with returning a reference to an internal object is that this reference
becomes shared by the client code. A similar problem is to introduce this sharing
by using a reference to an external object to initialize the internal state of another
object. For example, if we have a setter method for the content of the deck:
public class Deck {
Here, we can corrupt the state of the deck from the scope of the client code, for
example by adding an Ace of Hearts. From an object graph perspective, the outcome
of this code is similar to the one caused by leaking a reference through a getter
method, as illustrated in Figure 2.3.
A similar version of this problem is to set the content of the deck from a con-
structor, as opposed to a setter method:
public class Deck {
private List<Card> aCards = new ArrayList<>();
Although the leak uses a different type of programming language element (construc-
tor vs. setter method), the result is identical.
2.5 Escaping References 27
The issue of escaping references is complex because references can escape through
any number of shared structures, which may not always be obvious. Although a bit
contrived, the following example shows how this could come about:
public class Deck {
private List<Card> aCards = new ArrayList<>();
2.6 Immutability
One of the major design insights of this chapter is that to ensure good encapsulation,
it should not be possible to modify the internal state of an object without going
through its methods. Section 2.5 discussed the issue of escaping references, and
how they threaten encapsulation. There is, however, one situation where leaking
a reference to an internal object is harmless: when the object is immutable (i.e.,
impossible to change). Let us consider the following code:
class Person {
private String aName;
The implementation of class Person clearly violates the advice given in Section 2.5
(of not returning references held in private fields), given that Person.getName()
returns a reference to the value of an instance variable. We can also represent this
situation with an object diagram (Figure 2.5):
However, a crucial notice in the reference documentation of the String library type
changes things considerably:
Strings are constant; their values cannot be changed after they are created. [...] Because
String objects are immutable they can be shared.
2.6 Immutability 29
In this definition of the class, the only way to set the values of the two instance
variables is through the constructor call which, by definition, is only executed once
for each object. The fields are private, so they cannot be accessed from outside the
10 This is a slight abuse of language because, technically speaking, it makes no sense for a class
to be immutable. However, immutable class is a more convenient term than class that yields im-
mutable objects.
11 Simple enumerated types, which only enumerate values, are immutable. Although it is tech-
nically possible to define enumerated types that are not immutable, this is not a good idea. See
Chapter 4.
30 2 Encapsulation
class. There are only two methods. Although they are public, neither changes (or
mutates) the state of the object. Finally, although the methods return a reference
to the content of a field, the type of these fields is immutable, so it will not be
possible to change the state of the referenced objects in any case. The class is thus
immutable.12
In many cases the objects of the classes we define will need to expose part of the
information they encapsulate to other objects. How can we do this without breaking
encapsulation? As often in software design, there are different options, each with its
strengths and weaknesses. For sake of discussion, let us consider that we want to
design our Deck class so that it is possible to find out what cards are in a deck.
public class Deck {
private List<Card> aCards = new ArrayList<>();
}
12 To make the immutability of the class even more explicit, the keyword final could be placed in
front of the class keyword, as well as before the type of both fields. The use of final instance
variables is introduced in Chapter 4, and the use of final classes is introduced in Chapter 7.
2.7 Exposing Internal Data 31
As discussed above, adding a getter method that simply returns aCards is out
of the question, as this allows code outside the class Deck to modify the internal
representation of a Deck instance.
Extended interface
One solution is to extend the interface of the class to include access methods that
only return references to immutable objects. In our case, we could accomplish this
goal by adding two methods to the Deck class:
public int size() {
return aCards.size();
}
If class Card is immutable, this solution fulfills its mandate. However, it is somewhat
inelegant if client code typically needs to access all the cards in the deck. In such a
situation, the code would become cluttered with calls to size() and for loops going
over all indexes. Code might also need to be written to check that the argument to
getCard is not out of bounds, and so on.
Returning Copies
Another option, which mimics returning a reference to the field aCards without
breaking encapsulation, is to return a copy of the list stored in aCards. Thus, we
could add a new method:
public List<Card> getCards() {
return new ArrayList<>(aCards);
}
We see that it is not possible to change the internal state of Deck from a reference
to its cards. There are other strategies for returning a copy of a data structure or a
wrapper for it. Ultimately, the details of the implementation do not matter as much as
32 2 Encapsulation
the central idea, which is to return a different object that has all the same information
as the internal structure we wish to keep encapsulated.
Although it looks like a simple idea, copying objects is actually a tricky topic,
because it requires deciding how deep to copy the object graph. So far, we assumed
that Card objects were immutable, so it was sufficient to perform a shallow copy.
A shallow copy of a list is a copy of the list with shared references to the ele-
ments in the original list (that is, the elements are not copied). But what if Card
instances were mutable? In this case the above solution would not offer good en-
capsulation, because it would become possible to change the state of a deck without
going through its interface, for example:
public static void main(String[] pArgs) {
Deck deck = new Deck();
deck.getCards().get(0).setSuit(Suit.HEARTS);
}
Other Strategies
Copying objects is only one of many strategies for exposing information internal to
an object while maintaining encapsulation. The Java class library provides another
option through the use of unmodifiable view collections. An unmodifiable view is
an unmodifiable wrapper for an underlying collection of objects. For example, the
library method Collection.unmodifiableList(List) returns an unmodifiable
wrapper around a list. As an alternative to copying a list, we could do:
public List<Card> getCards() {
return Collections.unmodifiableList(aCards);
}
Other strategies will be covered later in the book. These include iterators (see
Section 3.5) and streams (see Section 9.6).
The encapsulation provided by this class is very good, but there remains a crack in
the shell it provides: it is possible to create a new card with a null reference:
Card card = new Card(null, Suit.CLUBS);
For most use cases where a representation of a playing card is required, this would be
incorrect. At least, I am not aware of any card game that involves a “null of Clubs”.
We thus corrupted the variable. Section 4.5 provides an extended discussion of the
issue of null references, but for now we focus on the general problem of avoiding
the creation of an invalid instance of class Card. For this purpose, one strategy is
to modify the code that provides our functionality of interest so that it checks that
the input is valid, and reports an error otherwise. In Java we typically use exception
handling for this purpose (see Section A.8 in the appendix):
/**
* ...
* @throws IllegalArgumentException if pRank or pSuit is null
*/
public Card(Rank pRank, Suit pSuit) {
if( pRank == null || pSuit == null) {
throw new IllegalArgumentException();
}
aRank = pRank;
aSuit = pSuit;
}
With this code, any attempt to create a Card instance with a null reference for
either of the two fields will result in an exception being thrown. When an exception
is thrown, the constructor does not complete normally and no new object is created.
For this reason, it is now impossible to create an invalid Card object as a result of
calling the class’s constructor.
An important consequence of this input validation is that now the null check be-
comes an integral part of the implementation of the constructor. Like any other kind
of functionality, users of the code should be aware of how a method or constructor
behaves in response to its input. For this reason, it is necessary to document this
2.8 Input Validation 35
behavior carefully. In the example above, the information about the exception being
raised is provided using Javadoc’s @throws tag.
It is important to remember that, in object-oriented programming, the object that
is the target of a method call is also an input to the method. As such, this input may
need to be validated as well. Let us consider a slightly more complete version of
the Deck class where we have added an implementation of a draw() method, along
with a method to check whether the deck is empty:
public class Deck {
private List<Card> aCards = new ArrayList<>();
Calling method draw() on an instance of Deck that contains no card will re-
sult in an exception being thrown by method remove, which will propagate out
of method draw(), causing it to terminate abnormally. This situation, however,
is very different from the case above. In our Card constructor, we explicitly de-
signed our code to detect a null reference being passed as argument, and to throw
an exception in response. The code comment reflects this design decision. In
the case of draw(), the exception is raised because we misuse a library method
in our implementation by passing it an invalid input. The resulting exception is
IndexOutOfBoundsException. One sloppy way to deal with the situation would
be to simply document the exception as follows:
/**
* ...
* @throws IndexOutOfBoundsException if isEmpty()
*/
public Card draw() {
return aCards.remove(aCards.size() - 1);
}
This approach, however, has two major drawbacks: it abuses the exception han-
dling mechanism, and it violates the principle of information hiding. A good design
principle for exception handling is that exceptions should only be used for unpre-
dictable situations. In the case of method remove, this means passing it an unknown
value, which turns out to be out of bounds for the underlying list. However, this is
not our situation, because we can always determine with complete certainty whether
aCards.size()-1 will be valid (by calling isEmpty()). As for information hiding,
the reason why propagating IndexOutOfBoundsException violates the principle is
that the fact that cards in the deck are stored in an indexed sequence is no longer a se-
cret. Although the implications are not dramatic, the encapsulation of class Deck can
be improved by avoiding this information leak. A solution that avoids both problems
36 2 Encapsulation
is thus to implement an explicit check, similarly to how we have done with the Card
constructor. In this case, because the illegal argument is the implicit argument (the
object that is the target of the call), it is clearer to use IllegalStateException.
Chapter 4 discusses the concept of object state in more detail.
/**
* ...
* @throws IllegalStateException if the deck is empty
*/
public Card draw() {
if( isEmpty() ) {
throw new IllegalStateException();
}
return aCards.remove(aCards.size() - 1);
}
Input validation is one option for ensuring that we only construct valid objects
and use them in valid ways. As usual, this design decision has both benefits and
drawbacks. The main benefit, as we have seen, is that the class is very robust: client
code can no longer corrupt the internal values in an object. The consequence, how-
ever, is that we have shifted the responsibility of the client code from input valida-
tion to error handling. Presumably, if the client code is written so that it is possible
to raise an exception, it should also catch this exception:
try {
card = deck.draw();
} catch( IllegalStateException exception ) {
// Recover
}
Another important consequence of input validation is that now we have addi-
tional input validation code to test, document, and maintain within our classes. In
some cases, this extra burden may not be justified. For example, if we only create
new cards in one location in the code, where it is clear that no null values are used,
then the error-handling machinery for protecting against the possibility of null in-
puts would be excessive. In the next section, I describe a systematic way to think
about input validity.
In the previous section, I pointed out the need for input validation to ensure that
client code does not misuse an object. However, input validation may not be nec-
essary if the client code is written in a way that precludes erroneous values. For
example, the following code creates all the cards in the Clubs suit:13
List<Card> clubs = new ArrayList<>();
for( Rank rank : Rank.values() ) {
clubs.add( new Card(rank, Suit.CLUBS));
}
With code like this, no null reference can ever be provided as argument to the
Card constructor, and we could consider omitting input validation. Unfortunately,
the fact that the responsibility for ensuring that valid values are used in a program
can rest either on the implementation of a class or on its client code creates a major
source of ambiguity. Let us again consider the interface of the Card constructor:
public Card(Rank pRank, Suit pSuit)
The main idea of design by contract is for method signatures (and related docu-
mentation) to provide a sort of contract between the client (the caller method) and
the server (the method being called). This contract takes the form of a set of precon-
ditions and a set of postconditions. A precondition is a predicate that must be true
when a method starts executing. The predicate typically involves the value of the
method’s arguments, including the state of the target object upon which the method
is called. Similarly, postconditions are predicates that must be true when the ex-
ecution of the method is completed.14 Given preconditions and postconditions, the
contract is basically that the method can only be expected to conform to the postcon-
ditions if the caller conforms to the preconditions. If a client calls a method without
respecting the preconditions, the behavior of the method is undefined. In practice,
design by contract is a great way to force us to think about all possible ways to use
a method.
In the sample applications (see Appendix C) I follow a lightweight version of
design by contract where preconditions are specified using Java statements in the
comments using the Javadoc @pre tag and postconditions are specified using the tag
@post.
/**
* @pre pRank != null && pSuit != null
*/
public Card(Rank pRank, Suit pSuit) {
// ...
}
It is possible to make pre- and postconditions (and any other predicates) checkable
in Java using the assert statement:
public Card(Rank pRank, Suit pSuit) {
assert pRank != null && pSuit != null;
aRank = pRank; aSuit = pSuit;
}
The assert statement evaluates its predicate expression and raises an Assertion-
Error if the result is false.15
Correctly implemented, design by contract helps prevent the tedious idiom of
defensive programming where corner cases (such as null references) are checked for
everywhere in the code. Additionally, the technique supports clear blame assignment
while debugging: If a precondition check fails, the client (caller method) is to blame.
If a postcondition check fails, the actual method being called is to blame. More
generally, assert statements are a simple yet powerful tool to increase code quality
and they can be used anywhere, not just for pre- and postconditions. Whenever an
assertion check fails, we know exactly where the problem is, and we can thus save
on debugging time.
14 The complete approach also involves the concept of invariants. In theory, invariants are pred-
icates that are expected to remain true at all times. In the practice of design by contract, it is
sufficient for invariants to be true at method entry and exit points.
15 Assertion checking is disabled by default in Java, so to use this properly it is necessary to add
Insights
This chapter focused on how to follow the principles of encapsulation and informa-
tion hiding when defining classes.
• Use classes to define how domain concepts are represented in code, as opposed to
encoding instances of these concepts as values of primitive types (an antipattern
called P RIMITIVE O BSESSION†);
• Use enumerated types to represent a value in a collection of a small number of
elements that can be enumerated;
• Hide the internal implementation of an abstraction behind an interface that tightly
controls how an abstraction can be used. Declare fields of a class private, unless
you have a strong reason not to. Similarly, declare any method private if it
should not be explicitly part of the type’s interface;
• Ensure that the design of your classes prevents any code from modifying the data
stored in an object of the class without using a method of the class. In particular,
be careful to avoid leaking references to private fields of the class that refer to
mutable objects;
• To provide information about the internal data in an object without violating
encapsulation, strategies include extending the interface of the class, returning
copies of internal objects, or using unmodifiable views;
• Object diagrams can help explain or clarify the structure of complex object
graphs, or how references are shared;
• Make classes immutable if possible. In Java, it is only possible to ensure that a
class is immutable through careful design and inspection;
• Input validation can be used to ensure that the objects of a class are created
and used properly. However, this extra code comes at a cost as it needs to be
documented, tested, and maintained.
• Use design by contract to avoid ambiguity in method signatures, and thereby help
prevent the possibility that client code will misuse an instance of a class.
Further Reading 41
Further Reading
In the previous chapter we saw how to define well-encapsulated classes, but conve-
niently left out the question of how objects of these classes would interact. We now
start addressing this question. Interactions between objects are mediated through
interfaces. The term interface is overloaded in programming: it can have different
meanings depending on the context.
Design Context
The examples in this chapter concern the design of a class library to allow client
code to instantiate and use a deck and other collections of card objects to support
the development of card games.
An interface to a class consists of the methods of that class that are accessible (or
visible) to another class. What methods are accessible depends on programming
language rules that take into account access modifiers and scopes (see Section 2.3).
For now, we define the interface to a class as the set of its public methods (I will
extend this definition in Chapter 7). Let us consider the following code:
© Springer Nature Switzerland AG 2022 43
M. P. Robillard, Introduction to Software Design with Java,
https://doi.org/10.1007/978-3-030-97899-0_3
44 3 Types and Interfaces
The interface of class Deck consists of three methods. The code in other classes
can interact with objects of class Deck by calling these and only these methods. Here
we would say that the interface of class Deck is fused, or coupled, with the class
definition. In other words, the interface of class Deck is just a consequence of how
we defined class Deck: there is no way to get the three services that correspond to
the three methods of the class, without interacting with an instance of class Deck. In
our example, to shuffle the deck, client code will need to invoke method shuffle()
of a field or local variable of type Deck: there is no other option.
There can be, however, situations in which we may want to decouple the interface
of a class from its implementation. These are situations in which we want to design
the system so that one part of the code can depend on the availability of a service,
without being tied to the exact details of how this service is implemented. Given
that we are designing a library that can be used to build different card games, we
note that many card games require the user to draw cards, but not necessarily from
a standard deck of 52 cards. For example, some games might require drawing cards
from an aggregation of multiple decks of cards, from a set of cards of only one suit,
from ordered sequences of cards, etc. Let us consider the following code that draws
cards from a deck up to a required number.
public static List<Card> drawCards(Deck pDeck, int pNumber) {
List<Card> result = new ArrayList<>();
return result;
}
This method can only be used with sequences of cards that are an instance of class
Deck. This is a pity, because exactly the same code could be useful for any object
that has the two required methods draw() and isEmpty(). Here it would be useful
to specify an abstraction of an interface without tying it to a specific class. This is
where Java interface types come in. In Java, interface types provide a specification of
the methods that it should be possible to invoke on an object. With interface types,
we can define an abstraction CardSource as any object that supports a draw()
method and an isEmpty() method:
3.1 Decoupling Behavior from Implementation 45
/**
* @return True if there is no card in the source.
*/
boolean isEmpty();
}
This interface declaration1 lists two methods, and includes comments that specify
the behavior of each method. The specification of draw() includes the precondi-
tion that the method can only be invoked if isEmpty() is false. This precondition
is provided to support the use of design by contract (see Section 2.9), and takes into
account the existing state of the object. Because interface method declarations are
a specification and not an implementation, details of what the method is expected
to perform are very important. With a method implementation, it could always be
possible to inspect the code (if we have access to it) and infer the specification.
This is not an ideal situation, but it is better than nothing. With interface methods,
though, reverse-engineering what the method does is not possible. In Java terminol-
ogy, methods that do not have an implementation are called abstract methods.2 To
tie a class with an interface, we use the implements keyword.
public class Deck implements CardSource {
...
}
1 There is an important distinction between the general concept of an interface, and the specific
interface construct in Java. When the difference is clear from the context, I simply use the
term interface. When necessary, I use the expression interface type to refer to the Java construct.
2 Prior to Java 8, all interface methods were automatically abstract. With Java 8, this is no longer
true, because interfaces can include default and static methods, which have an implementation.
Nevertheless, it remains a good practice to thoroughly document the expected behavior of interface
methods.
46 3 Types and Interfaces
Taking this idea further, this means we can make our implementation of the draw-
Cards method much more reusable:
public static List<Card> drawCards(CardSource pSource, int pNum){
List<Card> result = new ArrayList<>();
for( int i = 0; i < pNum && !pSource.isEmpty(); i++ ) {
result.add(pSource.draw());
}
return result;
}
The method is now applicable to objects of any class that implements the Card-
Source interface.
Another illustration of the use of polymorphism is the use of concrete vs. abstract
types in the Java Collections Framework.
List<String> list = new ArrayList<>();
List is an interface that specifies the usual services (add, remove, etc.), and
ArrayList is an implementation of this service that uses an array. But we can re-
place ArrayList with LinkedList and the code will still compile. Even though the
details of the list implementation differ between ArrayList and LinkedList, they
both provide exactly the methods required by the List interface, so it is permissible
to swap them. Polymorphism provides two useful benefits in software design:
• Loose coupling, because the code using a set of methods is not tied to a specific
implementation of these methods.
• Extensibility, because we can easily add new implementations of an interface
(new “shapes” in the polymorphic relation).
solve a design problem or realize a particular feature. One good illustration of both
the purpose and usefulness of interfaces in Java is the Comparable interface.
One obvious task to be implemented in the Deck class is to shuffle a deck of
cards. This can be realized trivially with the help of a library method.
public class Deck {
private List<Card> aCards = new ArrayList<>();
As its name implies, the library method shuffle randomly reorders the objects in
the argument collection. This is an example of code reuse because it is possible
to reuse the library method to reorder any kind of collection. Here reuse is easy
because to shuffle a collection, we do not need to know anything about the items
being shuffled.
But what if we want to reuse code to sort the cards in the deck? Sorting, like
many classic computing problems, is supported by many existing quality imple-
mentations. In most software development situations, it would not be acceptable to
hand-craft one’s own sorting algorithm. The Java Collections class conveniently
supplies us with a number of sorting functions. However, if we opportunistically try
the following without further consideration:
List<Card> cards = ...;
Collections.sort(cards);
we are rewarded with a possibly cryptic compilation error.3 This should not be sur-
prising, though, because how exactly is a library method supposed to know how we
want to sort our cards? Not only is it impossible for the designers of library methods
to anticipate all the user-defined types that can be invented, but even for a given type
like Card, different orderings are possible (e.g., by rank, then suit, or vice-versa).
The Comparable<T> interface helps solve this problem by defining a piece of
behavior related specifically to the comparison of objects, in the form of a single
compareTo(T) abstract method. The specification for this method is that it should
return 0 if the implicit argument is equal to the explicit argument, a negative integer
if it should come before, and a positive integer if it should come after. Given the
existence of this interface, the internal implementation of Collections.sort can
now rely on it to compare the objects it should sort. Conceptually, the internal code
of the sort implementation looks a bit like this:
if( object1.compareTo(object2) > 0 ) ...
So, from the point of view of the implementation of sort, it really does not matter
what the object is, as long as it is comparable with another object. This is a great
example of how interfaces and polymorphism support loose coupling: the code of
3 The method sort(List<T>) in the type Collections is not applicable for the arguments
(List<Card>). Results can vary on different compilers.
48 3 Types and Interfaces
sort depends on the minimum possible piece of functionality required from its ar-
gument objects. This is a good general insight on how to define interface types. Ide-
ally, they should capture the smallest cohesive slice of behavior that is expected to
be used by client code. For this reason, many interface types in Java are named with
an adjective that ends in -able, a suffix that means fit to be.... Besides Comparable,
examples include Iterable, Serializable and Cloneable.
To make it possible for us to sort a list of cards, we therefore have to provide this
comparable behavior and declare it with the implements keyword:
public class Card implements Comparable<Card> {
public int compareTo(Card pCard) {
return aRank.ordinal() - pCard.aRank.ordinal();
}
}
This minimal implementation sorts cards by ascending rank, but leaves the order of
suits undefined, which leads to unpredictability. A more useful implementation of
the Comparable interface would provide a well-defined total ordering.
Because Java interfaces are types, the type-checking mechanism that is part of
the compilation process makes it possible to detect that a List<Card> object cannot
be passed to Collections.sort unless the Card class declares to implement the
Comparable<Card> interface. How this happens is outside the scope of this book
because it requires a good understanding of the typing rules for Java generic types
(see Section A.6 in the appendix).
Many other library types that have a so-called natural ordering implement
the Comparable interface. This includes String (with lexicographic order) but
also many other pervasive types. In particular, Java enumerated types implement
Comparable by comparing instances of an enumerated type according to their or-
dinal value. With this knowledge in hand, we observe that our implementation of
Card.compareTo, above, actually re-implements reusable behavior provided by the
enumerated types. We thus have an opportunity to simplify our code:
public class Card implements Comparable<Card> {
public int compareTo(Card pCard) {
return aRank.compareTo(pCard.aRank);
}
}
Designs where the important concerns revolve around the definition of types and
relations between types can become overwhelming to describe in code, and are more
easily captured through a diagram. Class diagrams represent a static, or compile-
time, view of a software system. They are useful to represent how types are defined
and related, but are a poor vehicle for capturing any kind of run-time property of the
code. Class diagrams are the type of UML diagrams that are the closest to the code.
However, it is important to remember that the point of UML diagrams is not to be
an exact translation of the code. As models, they are useful to capture the essence
of one or more design decisions without having to include all the details.
Class diagram have an extensive associated notation. In a class diagram, there
is typically more going on than, say, in an object diagram. I only use a subset of
the notation in this book. The Further Reading section includes references for UML
class diagrams. Figure 3.1 shows the main concepts used in this book. In the fig-
ure, all quotes are taken from The Unified Modeling Language Reference Manual,
2nd edition [15]. The interpretation of the concepts of aggregation, association, and
dependency will become clearer as we progress through the chapters. For now, it
is sufficient to know that these concepts represent that two classes are somehow re-
lated. The concept of navigability, represented with an arrow head, models how code
supports going from objects of one type to objects of another type. Navigability can
be unidirectional (as shown), bidirectional, or unspecified.
Attribute
Aggregation
Dependency: “A relationship
between two elements in which a Association: “The semantic
change to one element may affect relationship between two or more
or supply information needed by classifiers that involves connections
the other element.” among their instances.”
Figure 3.2 shows an example of a class diagram that models some of the key
relations between the design elements for a card game that we have seen so far. We
can observe the following:
Fig. 3.2 Sample class diagram showing key decisions in the design of a Deck class
• The box that represents class Card does not have attributes for aRank and Suit
because these are represented as aggregations to Rank and Suit enumerated
types, respectively. It is a modeling error to have both an attribute and an ag-
gregation to represent a given field.
• The methods of class Card are not represented. Because they are just the con-
structor and accessors, I judged this to not be very insightful information. It
would not be wrong to include them, but it would clutter the diagram.
• In the UML, there is no good way to indicate that a class does not have a certain
member (field or method). To convey the information that Card does not have
setters for the two fields, it would be necessary to include this using a note.
• Representing generic types is a bit problematic, because in some cases it makes
more sense to represent the type parameter (Comparable<T>) and in some other
cases it makes more sense to represent the type instance (Comparable<Card>).
In this diagram I went with the type parameter because I wanted to show how
Collections depends on Comparable in general.
• All UML tools have some sort of limitations one needs to get around. For sim-
plicity, JetUML (see Appendix C) does not have different font decorations (such
as underlines) to distinguish between static and non-static members. To indicate
that a method is static in JetUML, we can prefix the members’s name with the
keyword static.
• The model includes cardinalities to indicate, for example, that a deck instance
will aggregate between zero and 52 instances of Card. Typical values for an
association’s cardinality include a specific number (for example, 1), the wildcard
* (which means zero or more), and ranges such as M..N (which means between
M and N, inclusively).
3.4 Function Objects 51
In practice, an interface type often defines only a subset of the operations of the
classes that implement it. This scenario is exemplified by Comparable: the complete
implementation of Card comprises methods, such as getSuit() and getRank(),
that add to the slice of behavior required by the Comparable interface. There are
other situations, however, where it is convenient to define classes that specialize in
implementing only the behavior required by a small interface with only one method.
Let us continue with the problem of comparing cards. Implementing the Compara-
ble interface allows instances of Card to compare themselves with other instances
of Card using one strategy, for example, by comparing the card’s rank, and using
suits to break ties. What if we are designing a game where we need to sort cards
according to different strategies, and occasionally switch between them? One could
tweak the code of compareTo, for instance by relying on a global variable that stores
the required strategy and switching the comparison strategy based on this flag. How-
ever, harebrained schemes of this nature have many drawbacks. In our case, using
such a flag variable would degrade the separation of concerns between representing
a card and knowledge of how the card should be sorted, and generally make the code
harder to understand.
In fact, the use of this kind of switching is considered a design antipattern called
S WITCH S TATEMENT†.4 A more promising solution is to move the comparison code to
a separate object. This solution is supported by the Comparator<T> interface. The
abstract method in this interface is compare (as opposed to compareTo).5
int compare(T pObject1, T pObject2)
This method can sort a list of objects that do not necessarily implement the
Comparable interface, by taking as argument an object guaranteed to be able to
compare two instances of the items in the list. One can now define a rank first com-
parator:
4 There is evidence of the antipattern whether or not an actual switch statement is used, because
the latter can be trivially emulated through if–else statements.
5 As of Java 8, this interface provides an intimidating list of methods. These are not important here.
In Chapter 9 I revisit this interface to explain how we can leverage some of the additional methods.
52 3 Types and Interfaces
To client code, the impact of this change in design is minimal: the only difference is
the additional qualification of the name of the comparator class:
Collections.sort(aCards, new Card.CompareBySuitFirst());
6 As of Java 8, method sort is also available on the Collection (no ‘s’) interface. However,
to preserve the symmetry with the use of the Comparable<T> interface, I retain this version.
3.4 Function Objects 53
In the two examples above, we have brought back the problem of encapsulation,
because the code in the anonymous class that implements the comparison is defined
outside of the Card class. We can solve this with the help of a static factory method.
The term factory method refers to methods whose primary role is to create and return
an object.
public class Card {
public static Comparator<Card> createByRankComparator() {
return new Comparator<Card>() {
public int compare(Card pCard1, Card pCard2) {
/* Comparison code */
}
};
}
}
A final question is whether a comparator should store data. For example, instead
of having different comparators for sorting cards by rank and suit, we could define a
UniversalComparator that has a field of an enumerated type that stores the desired
type of comparison. Although this solution is workable, it can lead to code that is
harder to understand, for reasons explained in Section 3.7 and further discussed in
Chapter 4.
54 3 Types and Interfaces
3.5 Iterators
code using the Deck may start relying on the operations defined on a list, or make
the assumption that cards are internally stored in a list within a Deck. To achieve
an even higher level of information hiding, it would be better to allow client code
access to the internal objects of another object, without exposing anything about the
internal structure of the encapsulating object. This design feature is supported by
the concept of an iterator. The concept of an iterator is very general, and iterators
are employed in many programming languages.
In Java, iterators are easy to use, but understanding how they work requires being
aware of a careful coordination between at least three types of objects. Iterators also
provide an example of the use of interfaces types and polymorphism.
To support iteration we must first have a specification of what it means to iterate.
As usual, this specification is captured in an interface: in this case the Iterator
interface. This interface defines two abstract methods: hasNext() and next(). So,
according to the rules of subtyping, once a piece of code gains access to a reference
to an object of any subtype of Iterator, the client code can iterate over it, inde-
pendently of what the actual class of the object is. To enable iteration over the cards
of a Deck, let us simply redefine the getCards method to return an iterator instead
of a list:
public Iterator<Card> getCards() { ... }
Although this design achieves our decoupling goal, we can generalize it to great
effect. A first important insight is that in most software systems there will be dif-
ferent types of objects that it would be useful to iterate over. Lists are an obvious
example. In our case we also have a Deck. The issue with the iterator system as
we have it now, though, is that different classes define a different way to obtain an
iterator. For class List, it is through the method iterator(). For our Deck class,
3.5 Iterators 55
One of the main ways to use Iterable objects in Java is in an enhanced for
loop, also know as a foreach loop:
List<String> list = ...;
The way the enhanced for loop works is that, under the covers, it expects the
rightmost part of the loop head to be an instance of a class that is a subtype of
Iterable (or an array type, which is a special case).
The final issue to solve to complete our iterator-based design for Deck is to find
a way to return an instance of Iterator when the iterator() method is called.
Although it would be possible to hand-craft our own user-defined class that im-
plements the Iterator<Card> interface, we can observe that the List contained
within a Deck is also Iterable, and the Iterator it returns does everything that
we want.
public class Deck implements Iterable<Card> {
private List<Card> aCards;
Strictly speaking, this idiom can violate the encapsulation of class Deck because
interface Iterator<T> includes a method remove() that can be optionally imple-
mented (and which is implemented by ArrayList). Consistent with the book’s
goal of focusing on general design concerns with minimum coverage of the li-
braries, I overlook this case. In the context of the book, it can be assumed that
Iterator#remove() is not used. For production code, how to best avoid the encap-
sulation problem would depend on the context. One option is to return the iterator
obtained from an unmodifiable view of the list with a call such as:
return Collections.unmodifiableList(aCards).iterator().
The previous section introduced the use of iterators as a way to provide access to
a collection of objects encapsulated within another object without violating the in-
formation hiding properties of this object. This solution is a common design pattern
called, not surprisingly, the I TERATOR. The context for I TERATOR is to
Provide a way to access the elements of an aggregate object sequentially without exposing
its underlying representation. [6]
The solution template for I TERATOR can be best captured by the class diagram in
Figure 3.5, which is simply an abstraction of the solution presented in Figure 3.4.
This is a very general definition, especially given that there is no agreed-upon def-
inition for what a family of algorithms is. Fortunately, the solution template for
S TRATEGY provides a clarification for object-oriented code: algorithms in the same
family implement the same interface.
3.7 The S TRATEGY Design Pattern 59
The S TRATEGY looks exceedingly simple. In fact in many cases it can be indis-
tinguishable from a basic use of polymorphism. I find it useful to think of a part of
the design as an application of S TRATEGY when that part of the design is focused on
allowing the switch between algorithms. One example, illustrated in Figure 3.7, is
the use of different card comparators for a deck of cards. Another example is the im-
plementation of different automatic playing strategies, as will be further discussed
in the Code Exploration section below.
are reusing the Comparator interface. This strategy is purely functional as it does
not have any side-effect and returns the result of applying the comparison algorithm.
At this point it should become clearer that implementing the Comparator interface
as a UniversalComparator that holds a value to decide what kind of comparison
to do, does not really respect the spirit of the S TRATEGY because the actual strategy
would be selected by changing the state of an object, as opposed to changing the
concrete strategy object.
So far in this chapter we have seen how to use interface types so that classes that
use a service can be decoupled from the actual implementation of the service. Con-
tinuing with our example of the comparator, let us say we are designing a version of
our Deck of cards that can be sorted in various ways. Applying the S TRATEGY pattern
described in the previous section, our code should look like this:
public class Deck {
private List<Card> aCards = new ArrayList<>();
private Comparator<Card> aComparator = /* initialize */;
In the code above, various options are possible for initializing the comparator
(that is, the concrete strategy). One option is to call the constructor of the desired
comparator when initializing the field:
private Comparator<Card> aComparator = new ByRankComparator();
There are two issues with this approach. First, it does not allow the client code to
easily switch the comparison strategy. To switch the comparison strategy, it would
be necessary to modify the source code of the Deck class and recompile it. Second,
this design introduces a dependency between the Deck class and a specific compara-
tor implementation. Figure 3.8 illustrates the problem.
Fig. 3.8 Introducing a dependency between a client class and an implementation class
One variant of this solution could be to use an anonymous class in the definition
of the comparator:
private Comparator<Card> aComparator = new Comparator<Card>() {
public int compare(Card pCard1, Card pCard2) {
return pCard1.getRank().compareTo(pCard2.getRank());
}
};
However, this solution exhibits the same problem as the previous one: it does
not allow us to switch the comparison algorithm easily, and the Deck class is still
coupled to a specific implementation of the comparison. The only difference is that
now this implementation is anonymous.
A solution to both the lack of flexibility of the Deck class and its tight coupling
with the comparison strategy is to decouple the creation of the dependency (here, the
implementation of the comparator) from the creation of the client of the dependency
(here, the Deck class). Instead, we pass in, or inject, the dependency into the client
class.
public class Deck {
private Comparator<Card> aComparator;
This technique is called dependency injection. In this way, class Deck only has a
dependency to the interface type Comparator<Card>, and remains decoupled from
any specific implementation. The trade-off is that client code of the Deck class must
now inject this dependency when creating a Deck:
Deck deck = new Deck(new ByRankComparator());
As is the case for design patterns, dependency injection is a general idea and
there are many different ways to apply it in practice. For example, one could design
various factory methods to instantiate dependency objects, or inject a dependency
using an anonymous class, etc. It is also possible to inject the dependency via a set-
ter method instead of the constructor. This alternative, however, is often inferior be-
cause it creates object state management challenges, discussed in Chapter 4. Finally,
there are also libraries and frameworks available to support advanced dependency
injection scenarios that require a lot of configuration. In this book, however, I stick
to simple applications of dependency injection such as the one illustrated above.
Throughout this chapter we saw the various benefits of defining specialized inter-
faces that specify a small and coherent slice of behavior that clients depend on.
This way, client code is not coupled with the details of an implementation, and
only depends on the methods it actually requires. For example, code that pro-
cesses cards can only depend on a CardSource interface with two methods, and
can therefore be reusable with any class that can provide these methods. Similarly,
the Collections#sort(...) method works because it can rely on just the fact
that the items in the collection are Comparable. This idea is actually an instance of
a general design principle called the Interface Segregation Principle (ISP). Simply
put, the ISP states that client code should not be forced to depend on interfaces it
does not need.
The idea of the ISP is easier to explain by presenting a situation where the princi-
ple is not respected. We can consider again the code in Section 3.1 where drawCards
takes a Deck as argument:
3.9 The Interface Segregation Principle 63
In Section 3.1 I argued that this was a suboptimal design because it tied an interface
with its implementation. Well, let us say we split the two by declaring an IDeck
interface:
public interface IDeck {
void shuffle();
Card draw();
boolean isEmpty();
}
This effectively decouples interface from implementation, and supports the use of
drawCards with non-Deck sources. However, this design also forces drawCards
to statically depend on a method it does not need, namely, shuffle. What if we
might want to draw cards from a source that cannot be shuffled? For this reason, the
CardSource solution initially presented in Section 3.1 did respect the ISP, and only
included methods draw and isEmpty in interface CardSource.
To push on this idea of ISP a bit, let us assume that there might be places in the
code that only shuffle an object. To support this slice of behavior, we would de-
fine an interface Shufflable with a single method shuffle(). Figure 3.9 shows a
maximally flexible separation of concerns for class Deck, with three different inter-
faces that capture three cohesive slices of behavior that are supported by class Deck,
and three client code locations (represented by Client1-3) interested in different
combinations of these services.
This design has loose coupling, which is great. However, this loose coupling
has one major disadvantage for cases where a client might be interested in more
than one slice of behavior. This situation is represented in Figure 3.9 by Client3,
which needs to both iterate over an Iterable<Card> and draw some cards from
the source. How can we express this combination, since in Java it is only possi-
ble to specify a single type for a variable? For example, if we pass an instance of
CardSource to a method of interest and wish to iterate over the cards, we have to
venture into inelegance:
public void displayCards(CardSource pSource) {
if( !pSource.isEmpty() ) {
pSource.draw();
for( Card card : (Iterable<Card>) pSource) {
...
}
}
}
In fact this is not only inelegant, but also unsafe, because it could be possible to
provide an argument to displayCards that is not a subtype of Iterable. A better
solution to this issue is offered directly by the type system, in the form of subtyping.
In Java, interfaces can be declared to extend each other, with the semantics that if
A extends B, types that implement A must provide implementations for all the meth-
ods declared in B as well, transitively. By extending interfaces, we can more easily
support combinations of services while respecting the ISP. In our scenario, if it is
observed that a lot of the code that uses CardSource also uses Iterable<Card>,
but not the other way around, then we can declare CardSource to be a subtype of
Iterable<Card>, as illustrated in Figure 3.10.
In principle, the same reasoning could apply in the reverse situation: if we no-
tice that most code that uses Iterable<Card> also uses CardSource but not the
other way around, it would make sense to declare Iterable<Card> to extend
CardSource. In practice, however, this is not possible because Iterable<T> is a
library type, which it is not possible to modify. For this reason, there are also large
3.9 The Interface Segregation Principle 65
Insights
This chapter focused on how to use interfaces and polymorphism to achieve exten-
sibility and reuse.
• Use interface types to decouple a specification from its implementation if you
plan to have different implementations of that specification as part of your design;
• Define interface types so that each type groups a cohesive set of methods that are
likely to be used together;
• Organize interface types as subtypes of each other to create flexible groupings of
behavior;
• Use library interface types, such as Comparable, to implement commonly ex-
pected behavior;
• Use class diagrams to explore or capture important design decisions that have to
do with how classes relate to each other;
• Consider function objects as a potential way to implement a small piece of re-
quired functionality, such as a comparison algorithm. Function objects can often
be specified as instances of anonymous classes, or as lambda expressions;
• Use iterators to expose a collection of objects encapsulated within another with-
out violating the encapsulation and information hiding properties of this object.
This idea is known as the I TERATOR design pattern;
• Consider using the S TRATEGY pattern if part of your design requires supporting
an interchangeable family of algorithms;
• Use dependency injection to decouple a client class that uses some abstractions
from the creation of these abstractions;
• Make sure that your code does not depend on interfaces it does not need: break
up large interface types into smaller ones if you find that many methods of a type
are not used in certain code locations.
Further Reading
The definitions for the notation of the class diagram shown in Figure 3.1 are adapted
from The Unified Modeling Language Reference Manual [15], which is one of the
most comprehensive resources available on UML. Chapter 3 of UML Distilled [5]
provides a more concise overview of the notation and semantics for this type of dia-
gram. The Gang of Four book [6] has the original, detailed treatment of the I TERATOR
and S TRATEGY patterns.
Chapter 4
Object State
One of the most difficult things to reason about when looking at a program is state
changes. Which operations can have a side-effect? On which path can data flow?
What impacts what? This chapter clarifies what object state is and how we can
manage to keep control over its state in a principled way.
Design Context
There are different ways we can look at a software system. One way is in terms of the
software elements declared in the source code and the relations between them. For
example, a Deck class declares a field aCards that is a list of Card instances. This
is a static (or compile-time) perspective of the system. The static perspective is best
represented by the source code or a class diagram. A different, but complementary,
way to look at a system, is in terms of objects in a running software system. For
© Springer Nature Switzerland AG 2022 67
M. P. Robillard, Introduction to Software Design with Java,
https://doi.org/10.1007/978-3-030-97899-0_4
68 4 Object State
example, at one point a Deck instance contains three cards, then one card is drawn,
which leads to the instance of Deck containing two cards, etc. This is the dynamic (or
run-time) perspective on the software. The dynamic perspective corresponds to the
set of all values and references held by the variables in a program at different points
in time. It is what we see in a debugger while stepping through the execution of the
code. The dynamic perspective cannot easily be represented by any one diagram.
Instead, we rely on object diagrams, state diagrams (introduced in this chapter), and
sequence diagrams (introduced in Chapter 6). The static and dynamic perspectives
are complementary in software design. Sometimes it is best to think of a problem
and solution in static terms, sometimes in dynamic terms, and sometimes we really
need both. This duality between the static and dynamic perspectives on a software
system is akin to the wave-particle duality for representing the phenomenon of light
in physics:
It seems as though we must use sometimes the one theory and sometimes the other, while
at times we may use either. [...] We have two contradictory pictures of reality; separately
neither of them fully explains the phenomena of light, but together they do.
—Albert Einstein and Leopold Infeld, The Evolution of Physics
The cardinality of the set of possible concrete states for Player is 232 , or about 4
billion states.1 We usually refer to the set of possible states for a variable or object
as its state space. As soon as objects have fields of reference types, the cardinality
(or size) of the state space explodes dramatically. For example, the state space of
a Deck instance includes all possible permutations of any number of cards in the
deck, a number in the range of 2.2 × 1068 . This is an enormous number.2 With class
Player, adding an aName field of type String blows up the size of the state space
to something that is only limited by the computing environment. For this reason,
when designing software, it is more practical to think in terms of abstract states.
In principle, an abstract state is an arbitrarily-defined subset of the concrete state
space. For example, considering the simple version of Player without the aName
field, Even Score could be an abstract state for a Player instance that groups the
roughly 231 states that represent a score that is an even number. Likewise, for an in-
stance of the Deck class, the abstract state Three Kings could represent any possible
configuration of the deck where exactly three cards of rank Rank.KING are present.
These two examples illustrate the fact that because abstract states are arbitrary par-
titions of the state space, they can really be defined as anything, no matter how
whimsical. It should however be evident that neither of these two example abstract
states would be particularly useful to design a realistic software system. In practice,
the software design task of state space partitioning is to define abstract states that
correspond to characteristics that will help construct a clean solution. A more use-
ful abstract state for Player would be Non-zero Score, and one for Deck would be
Empty (no cards in the deck), which in the latter case happens to correspond to a
single concrete state. When the distinction is necessary, I use the term meaningful
abstract state to indicate abstract states that capture states that impact how an object
would be used. For example, the abstract state Empty is meaningful because it is
not possible to draw a card from an empty deck. In contrast, the abstract state Three
Kings is not meaningful because, at least in a game of Solitaire, whether a deck
contains three kings or not has no impact on the game play and is not related to any
design or implementation decision. Unless otherwise noted, future references to the
term abstract state assume that we are talking about meaningful abstract states.
A special case when thinking about object state is that some objects do not store
any values. For example, function objects (see Section 3.4), often do not have any
fields besides constants. In this case, we talk about stateless objects. When the con-
trast is important, we can refer to objects that have state as stateful objects. Another
property of objects that is related to their state is mutability (see Section 2.6). This
chapter is concerned with objects that are both mutable and stateful. In the case of
immutable objects, the boundary between statefulness and statelessness becomes
blurry, because in practice they only have a single state.
52!
2 Computed as ∑52
k=0 (52−k)! .
70 4 Object State
UML state diagrams3 are useful to represent how objects can, during their life-
time, transition from one abstract state to another as a reaction to external events
(typically, method calls). They represent a dynamic view of a software system. The
annotated diagram in Figure 4.1 shows all the state diagram notation used in the
book.
Self
Transition
Transition
The example in Figure 4.2 illustrates both the notation and purpose of UML state
diagrams. It models some of the important abstract states of an instance of a class
Deck that represents a deck of 52 playing cards. Even this simple diagram captures
key information about the design of the Deck class.
The abstract state Empty is annotated as the initial state, which allows us to infer
that the constructor returns a reference to a new Deck object with no cards in it.
3 The official name of the diagram is UML state machine diagram. In this book I use the simpler
form state diagram.
4.3 State Diagrams 71
In state diagrams, absence of a transition usually means that the absent transition
is not possible (i.e., invalid) for that state. Here we can see that we cannot draw
cards from an empty deck. The transitions are annotated with names that correspond
to methods of the class Deck. The only legal transition out of the Empty state is
shuffle which brings the object in the Complete state. From this it can easily be
inferred that Complete is a shorthand for Complete and shuffled (in this particular
design).
The shuffle transition out of the Complete state illustrates the idea of self tran-
sitions, namely, events that do not result in a change of abstract state. The only legal
transition out of the Complete state is draw, which inevitably brings the deck object
to an Incomplete state.
It is also possible to attach to a transition an action that describes what happens as
the result of the transition. The action that corresponds to the draw event is remove
card from the deck. The action information is optional and here I chose to leave it out
of the diagram because it seemed redundant with the name of the event (considering
that to draw is a synonym of to remove in the context of card games).
The two transitions out of the Incomplete state illustrate the importance of
guards, because here without the concept of a guard we would not be able to model
the distinction between a draw event that leads to the Empty state, and a draw event
that keeps the object in the Incomplete state. The language I use for modeling guards
does not follow a formal specification, but I nevertheless like to specify guards using
pseudo-code that is very close to what could be reasonably tested on an instance of
the object. Here the guard would assume the presence of a size() method in the
Deck class.
Finally, this diagram does not include any final state. The final state model ele-
ment is used to specify if an object must be in a certain state at the end of its lifetime.
In Java, this means when all references to the object are eliminated, and the object
becomes available for garbage collection. In many designs, objects can end their life
(stop being used) in any state. In this latter case, the final state model element does
not apply.
An important benefit of state diagrams is that they allow us to self-evaluate the
impact of design decisions on the complexity of the abstract state space that must be
considered when working with an object. Here the state space is very simple (three
states) because of the decision to bundle the deck initialization code together with
the shuffling code. Separating this behavior into distinct initialize and shuffle
events, or including a sort event, leads to a much more complicated abstract state
space for the object (see Figure 4.3 for an example).
Another important benefit of state diagrams is that they support systematically
exploring the abstract behavior of an object of a given class. When modeling the
state of an object, a good practice is to visit each state and consider each possible
type of transition. This simple procedure is an excellent way to avoid overlooking
certain paths through the code (e.g., shuffling an incomplete deck).
When getting started with modeling object states with state diagrams, one com-
mon tendency is to use the state diagram notation to model a type of data flow in-
formation, where states represent processing, and arrows represent the flow of data
72 4 Object State
Fig. 4.3 State diagram for an instance of Deck where initialization and shuffling are separate
operations
between processing stages. This is an incorrect use of the notation.4 A good tip for
avoiding this pitfall is to think about the names of the states. If the names assigned to
states include verbs or generally feel like they are describing actions (for example,
draw card), it is probably a sign that the diagram does not represent a good model
of the state space.
Finally, it is worth noting that the concept of state diagram is very similar to that
of deterministic finite automata (DFA). This is not a coincidence, as both notations
are intended to model some sort of stateful phenomenon. However, the purpose
of each notation is different. DFAs are strict theoretical representations meant to
fully and precisely describe computation models, whereas state diagrams involve
communicating more abstract ideas about software to support software development
activities such as software design, testing, and documentation. State diagrams can
omit certain elements, but DFAs cannot.
4 For which a better match is an activity diagram, which is, however, not covered in this book.
4.4 Designing Object Life Cycles 73
As the result of method calls, a stateful and mutable object will transition between
different abstract states. The state diagram model of an object can also be referred
to as its life cycle, because it describes the “life” of an object, from its initialization
to its abandonment and eventual destruction by the garbage collector. The idea of
a life cycle is similar to that of humans, which are created (somehow), then go
through various stages of life (childhood, adolescence, etc.). However, in contrast to
humans and most types of biological organisms, where stages of life occur once in
a given sequence, the life cycle of objects depends on their design, and can get very
complex. Object life cycles can include actual cycles, different paths through the
abstract state space, and dead ends. As the complexity of the class grows (in terms
of number of abstract states), the life cycle of instances of the class can become
impossible to understand, even with the help of state diagrams. The contrast between
Figure 4.2 and Figure 4.3 shows how an object life cycle can gain in complexity,
even as a consequence of a seemingly small design change.
Objects with a complex life cycle are difficult to use, difficult to test (see Chap-
ter 5), and their design and implementation is error-prone. A good design principle
to avoid objects with complex life cycles is thus to minimize the state space of ob-
jects to what is absolutely necessary for the object to conduct its business. In prac-
tice, this means designing the class so that it is both impossible to put the object in
an invalid or useless state, and that there is no unnecessary state information.
Some states in the state space might simply be a consequence of how an object is
designed or implemented, without there being any use for an object in that state
in a given software system. For example, in a game of Solitaire, there is no use
for an unshuffled deck of cards. A design where a fresh instance of Deck must be
first initialized, then shuffled, introduces two abstract states, and all the consequent
complexity visible in Figure 4.3, without any benefit. In some cases, eliminating
some states from the life cycle of an object may seem like reducing the versatility
of a class (what if we need this one day?). This is an argument that can be made in
some situations. However, it is always important to consider the cost (in terms of
software developer time spent understanding code, writing tests, and fixing bugs).
Often, this kind of S PECULATIVE G ENERALITY† is not worth the cost.
Another common bad practice when designing objects is to cache stateful infor-
mation in instance variables, typically for convenience, presumed performance effi-
ciency, or both. This point is best illustrated with an example. Because this example
74 4 Object State
is so simple, the argumentation may seem ridiculous. However, I have seen the prob-
lem it represents countless times in code.
The implementation of the Deck class we have been discussing so far stores cards
in a list and provides a method size() to allow client code to determine the number
of cards in a deck.
public class Deck {
private List<Card> aCards = new ArrayList<>();
A developer with a keen eye for efficiency might observe that in a typical ex-
ecution of the Solitaire application, method size() is called numerous times on
an instance of Deck of the same size. For example, it may be possible to call
deck.size() ten times and receive the same value, before deck.draw() is called
(which changes the size of the deck). Every time, a call to deck.size() results in
a “useless” call to aCards.size()! Instead, would it not be more efficient to store
the size directly in the deck, and return it?
public class Deck {
private List<Card> aCards = new ArrayList<>();
private int aSize = 0;
This design decision is an example of the classic computing trade-off between space
and time: save a bit of time at the cost of a bit of extra memory. However, except
in the case of genuine long-running operations (such as device input/output), the
savings in terms of execution time and the cost in terms of memory tend to be
insignificant, whereas the significant losers are the simplicity, elegance, and under-
standability of the code.
Similarly to a misplaced concern about performance, another common reason for
storing redundant information in fields is convenience (e.g., when a value is difficult
to obtain through other fields of an object). This case is more difficult to illustrate
with simple code examples. The general principle, however, applies widely: to the
extent possible, information should not be stored in an object unless it uniquely con-
tributes to the intrinsic value represented by the object. A violation of this principle
often constitutes an instance of the T EMPORARY F IELD† antipattern.
4.5 Nullability 75
4.5 Nullability
One aspect of most programming languages that gets in the way of designing clean
state spaces and life cycles for objects is the possibility to assign the value null to
a variable of a reference type. In Java (and many similar languages, such as C++),
null is a special value that indicates, in a troublesome way, the absence of value.
For example, if we assign null to a variable:
Card card = null;
we are in effect stating that the variable card is of a reference type Card, but that
it refers to... nothing! This is a big problem because variables of reference types are
intended to be dereferenced:
System.out.println(card.getRank());
If it is possible to design a class to avoid any abstract state where a certain variable
does not have a value, it is greatly desirable to design the class to forbid this even-
76 4 Object State
tuality. For example, normal playing cards must have a rank and a suit, so there is
really no reason to allow null references for either instance variable.
public class Card {
private Rank aRank; // Should not be null
private Suit aSuit; // Should not be null
We can ensure that variables are not assigned a null reference by using either
one of two approaches: input validation (Section 2.8), or design by contract (Sec-
tion 2.9). With input validation, everywhere a variable can be assigned, we check
whether the input value is a null reference and throw an exception if that is the case:
public Card(Rank pRank, Suit pSuit) {
if( pRank == null || pSuit == null) {
throw new IllegalArgumentException();
}
aRank = pRank;
aSuit = pSuit;
}
In either case, if the Card constructor is the only place where aRank and aSuit can
be assigned, we have effectively ensured that the value stored in either variable will
not be a null reference.
In many situations, the domain concept we are trying to model will require that we
make a provision for the fact that there may not be a value. As an example, let us
consider a variant of class Card where an instance can also represent a joker. In
many card games, a joker is a special card that has no rank and no suit. To flag a
card as a joker, a simple approach is to add a aIsJoker field to its declaration:
4.5 Nullability 77
Here the logic to determine whether a card represents a joker is simple enough, but
what should we do with its rank and suit? As usual, many options are possible. We
can review three:
• Null references: We could just ignore the advice offered in this section and assign
null to aRank and aSuit. This means it would be possible to call (for example)
card.getRank().ordinal() on a joker, and get a NullPointerException.
• Bogus values: We could assign an arbitrary, meaningless value for the rank and
suit of a joker (e.g., Ace of Clubs). However, this is both confusing and danger-
ous. A part of the code could erroneously request the rank of a joker, and receive
the value Rank.ACE, which makes no sense. It is easy to imagine how tracking
down this bug could be lengthy and annoying.
• Special values of an enumerated type: We could add an INAPPLICABLE enumer-
ated value to both Rank and Suit, and assign these values to the corresponding
fields for instances of Card that represent jokers. In my opinion, this solution is
slightly better than the two above, but it still has some clear weaknesses. First, it
is a conceptual abuse of the idea of enumerated types, where each value is enu-
merated. Although technically that is what it is, conceptually INAPPLICABLE is
not a valid value in the enumeration, but rather a flag that indicates that we do not
have a value. Second, although we have four ranks and 13 suits, this solution will
yield five and 14 enumerated values for each type, respectively. This discrepancy
will muck up any code that relies on the ordinal values of these types (such as
the initialization of a deck of cards), and introduce opportunities for off-by-one
errors.
Fortunately, there are better solutions for avoiding the use of null references to
represent absent values.
Optional Types
One solution is to use an optional type. In Java, optional types are supported by
the Optional<T> library class. The Optional class is a generic type that acts as a
wrapper for an instance of type T, and which can be empty. To make a value of type
T optional for a variable, we declare this variable to be of type Optional<T>. In our
case:
78 4 Object State
To represent the absence of a value of the variable, we use the value returned by
Optional.empty(). So, to create a constructor that instantiates a Card that repre-
sents a joker, we could have:
public class Card {
private Optional<Rank> aRank;
private Optional<Suit> aSuit;
public Card() {
aRank = Optional.empty();
aSuit = Optional.empty();
}
There exists a second solution for avoiding the use of null references to represent
absent values, which avoids the issue of unpacking wrapper objects. This solution
uses a special object to represent the null value. For this reason, this idea is called
the N ULL O BJECT design pattern. Using a N ULL O BJECT to represent a null value re-
lies on polymorphism, so it is only applicable to situations where a type hierarchy
is available. Because Card objects are not a subtype of any other user-defined type,
we cannot use it to model a joker. To explore the N ULL O BJECT pattern, let us con-
sider a different scenario, where a CardSource in client code could be unavailable.
CardSource is an interface that defines methods draw() and isEmpty() and that it
is implemented by the Deck class (see Section 3.1).
The main idea of N ULL O BJECT is to create one special object to represent the
absent value, and to test for absence using a polymorphic method call. Figure 4.4
captures the solution template as applied to the problem of handling potentially
absent card sources.
Fig. 4.4 Class diagram showing the CardSource interface with support for the N ULL O BJECT
pattern
In this design, one method was added to the original CardSource interface to
determine if we are dealing with the null object, and one class was added to repre-
80 4 Object State
sent the null object. As expected, isNull() returns false for objects of any con-
crete type except NullCardSource, and true for the instance of NullCardSource.
Client code can then use this idiom to handle absent card sources:
CardSource source = new NullCardSource();
...
CardSource source = getSource();
if( source.isNull() ) {...}
With the features of Java 8, a N ULL O BJECT solution can be implemented effi-
ciently by only modifying the interface at the root of the type hierarchy. First, we
use a default method to avoid having to change all card source classes to implement
isNull simply to return true. In Java, default methods are methods that have an
implementation in the interface which is applicable to instances of all implementing
types. To have a minimal impact on the rest of the code, we can also implement the
N ULL O BJECT as a constant in the interface by using an anonymous class.5
public interface CardSource {
Card draw();
boolean isEmpty();
default boolean isNull() { return false; }
}
In Section 4.4, I argued that one useful principle to follow when designing a class
is to keep the abstract state space for objects of the class to the minimum necessary
for the objects of the class to provide the services expected of them. For example,
a well-designed Deck class has three meaningful abstract states, not ten. Because
object state is just an abstraction of the combination of values taken by the fields of
an object, the way to realize the principle in practice is to limit the number of ways
in which the field values can be updated. We already saw, in the previous section,
how avoiding null references whenever possible can help us reach this goal. An even
stricter constraint for keeping the abstract state space of objects nice and small, is to
prevent changing the value of a field after initialization, so that the value of the field
remains constant.
This constraint can be made explicit through the use of the final keyword placed
in front of a variable declaration (which includes the declaration of instance vari-
ables). If we declare the fields aRank and aSuit to be final in class Card:
public class Card {
private final Rank aRank;
private final Suit aSuit;
then the fields can be assigned a value only once, either in the initializer part of
their declaration, or directly in the constructor (as in the example).6 Attempting to
6 In practice, field initialization code (the right-hand side of the equal sign in a field declaration)
gets executed as part of constructor call anyways.
82 4 Object State
reassign the value of the field anywhere else in the code leads to a compilation error.
The final keyword thus goes a long way in limiting the state space of an object,
because any field marked as final can only take a single value. In the case of class
Card, making both aRank and aSuit final effectively renders objects of the class
immutable, because there are no other fields.
An important thing to keep in mind with the use of the final keyword, however,
is that for reference types, the value stored in a variable is a reference to an object.
So, although it is not possible to reassign a final field, it is certainly possible to
change the state of the object referenced (if the object is mutable). Let us illustrate
this point by making field aCards of class Deck final:
public class Deck {
private final List<Card> aCards = new ArrayList<>();
}
Because field aCards is final (something not visible on the diagram), we can be
sure that the reference held in the field will always refer to the one ArrayList
named cardList on the diagram. In other words, it will not be possible for this
arrow to point anywhere else. However, we can (and need to) change the state of
cardList, for example to initialize it with all the cards. Thus, although final fields
can be very helpful in restricting the state space of an object to make it easier to
understand the behavior of the object at run time, they do not make the referenced
objects immutable.
The discussion above was concerned mainly with instance variables. However,
local variables (including method parameters) can also be declared to be final. As
opposed to fields, however, the case for making local variables final is much less
clear because they are not long-lived.7 There is one fairly technical special case
where local variables must not be reassigned (see Section 4.10), but even then the
variable does not need to be explicitly marked with the final keyword.8 I occa-
sionally declare a variable final to make my intent clear that the variable is not and
should not be reassigned. This is only really useful for long and/or complex meth-
ods that may be a bit difficult to understand. Ideally, this should be a rare scenario,
because well-designed methods are short and simple (and an overly L ONG M ETHOD†
is a recognized antipattern).
7 Local variables only exist for the duration of the execution of code in their scope.
8 Since Java 8, local variables that are not reassigned are considered effectively final by the com-
piler.
4.7 Object Identity, Equality, and Uniqueness 83
Three important concepts to keep in mind when designing object life cycles are
those of identity, equality, and uniqueness.
Identity refers to the fact that we are referring to a particular object, even if this
object is not in a variable. In terms of programming environments, the identity of an
object usually refers to its “memory location”, or “reference/pointer to”. However,
in modern programming systems the memory management of objects is heavily
abstracted, and for this reason it is best to think in terms of object identity. Most
integrated development environments supply a convenient handle to represent an
object’s identity. For example, in the Eclipse debugger this is represented by the
object id.
In the small example of Figure 4.6 two Card objects are created, and conse-
quently result in two distinct objects with two distinct identities, represented with
internal object identifiers 49 and 50 (on the right, in the Value column). In the object
diagram of Figure 4.7, the main method is represented as an object with two fields
in place of local variables. The diagram shows how object identity corresponds to
both object model elements and the references to these objects. If, for instance, a
reference to the Card object with id 49 is added to a list, there will be two locations
that refer to a single shared identity.
The last statement in the main method in Figure 4.6, is a reminder that in Java,
the == operator returns true if the two operands evaluate to the same value. In
the case of values of reference types, the same value means referring to the same
object (identity). So here the statement returns false because, although both cards
represent an Ace of Clubs, they are references to different objects.
The situation above, where two different Card objects represent the Ace of Clubs,
illustrates the concept of object equality. In the general case, equality between two
objects must be programmer-defined because the meaning of equality cannot always
be inferred from the design of the object’s class. In very simple cases (like objects of
class Card), one could say that two objects are equal if all their fields have the same
value. However, for many objects of more complex classes, this would be too strict.
For example, if some objects cache values or have non-deterministic or unspecified
internal representations, they could be “equal” in the practical sense, without having
precisely the same value for each field, transitively. For example, two instances of
a set abstract data type (such as Java’s Set) must be equal if they have the same
elements, even if internally the order in which these elements are stored is different.
card1.equals(card2)
will return true.
A crucial constraint when overriding equals is that any class that overrides
equals must also override hashCode so that the following requirement is respected:
If two objects are equal according to the equals(Object) method, then calling the
hashCode method on each of the two objects must produce the same integer result.
—Reference documentation for Object#equals
This constraint is necessary because, among other things, many classes of the
Collections framework rely interchangeably on equality testing and on an object’s
hash code for indexing objects in internal data structures.
A final consideration related to identity and equality is the concept of unique-
ness. In our example code, we could rightfully wonder what is the point of tolerating
duplicate objects that represent exactly the same card (e.g., Ace of Clubs). A some-
times useful property for the objects of a class is uniqueness. Objects of a class are
unique if it is not possible for two distinct objects to be equal. If the objects of a class
can be guaranteed to be unique, then we no longer need to define equality, because
in this specific case, equality become equivalent to identity and we can compare
objects using the == operator. Strict guarantees of uniqueness are almost impossible
to achieve in Java due to mechanisms such as metaprogramming (see Section 5.4)
and serialization.10 However, in practice, the use of two design patterns, presented
below, and the conscious avoidance of metaprogramming and serialization, provide
a good enough guarantee of uniqueness that can help simplify some designs.
10 Serialization involves converting an object into a data structure that can be stored outside a
running program, and then reconstructed later. The reconstruction of a serialized object almost
invariably leads to a copy of the serialized object being created.
86 4 Object State
The context for using F LYWEIGHT is when instances of a class are heavily shared
in a software system. For example, Card objects in the Solitaire application are
referenced in many different classes.
The idea that underlies the pattern’s solution template is to manage the creation
of objects of a certain class, called the flyweight class. Instances of the flyweight
class are called flyweight objects. The crucial aspect of the F LYWEIGHT is to control
the creation of flyweight objects through an access method that ensures that no du-
plicate objects (distinct but equal) ever exist. The three main components necessary
to realize this constraint are:
1. A private constructor for the flyweight class, so clients cannot control the cre-
ation of objects of the class;
2. A static flyweight store that keeps a collection of flyweight objects;
3. A static access method that returns the unique flyweight object that corresponds
to some identification key. The access method typically checks whether the re-
quested flyweight object already exists in the store, creates it if it does not already
exist, and returns the unique object.
For example, we could decide to make the Card class a flyweight. Let us first
consider the non-flyweight version:
public class Card {
private final Rank aRank;
private final Suit aSuit;
Instances of this class are clearly not flyweights, given that it is possible to use the
constructor to create two instances that are distinct but equal:
Card card1 = new Card(Rank.ACE, Suit.CLUBS);
Card card2 = new Card(Rank.ACE, Suit.CLUBS);
System.out.println(String.format("Same?: %b; Equal?: %b",
card1 == card2, card1.equals(card2)));
overview of the main structures. For the F LYWEIGHT in particular, the implementation
of the flyweight store and access method can exhibit much variability, depending on
the details of the flyweight class.
In our specific case, because playing cards can be completely indexed in terms of
two keys (rank and suit), I will store them in a two-dimensional array. As to where
this array should be located, an obvious choice is to hold it as a static field in class
Card so that we can make it private and use methods of class Card to access it. The
following code shows the definition of the flyweight store and an implementation of
its initialization.
public class Card {
private static final Card[][] CARDS =
new Card[Suit.values().length][Rank.values().length];
static {
for( Suit suit : Suit.values() ) {
for( Rank rank : Rank.values() ) {
CARDS[suit.ordinal()][rank.ordinal()] =
new Card(rank, suit);
}
}
}
}
Because objects that represent playing cards are relatively small in number (52)
and completely known in advance, I also chose to pre-initialize the flyweight store
with a static initializer block.11 This implementation is only one example of a F LY-
WEIGHT implementation. Even for the same context (playing cards), many other al-
ternatives are possible. For example, it would be possible to store flyweights in lists
or hash tables.12 With the current solution, accessing the collection with a correct
index is guaranteed to produce the requested card. For example:
CARDS[Suit.CLUBS.ordinal()][Rank.ACE.ordinal()];
will return the (assumed unique) instance of Card that represents the Ace of Clubs.
The code above is only correct if it is placed within the scope of class Card,
because CARDS is private. To grant access to cards to code outside the class, we need
an access method. In our example, the implementation of this method is trivial:
public static Card get(Rank pRank, Suit pSuit) {
assert pRank != null && pSuit != null;
return CARDS[pSuit.ordinal()][pRank.ordinal()];
}
This method is static given that the flyweight store is static. This makes sense, be-
cause it would be bizarre to request a card instance through another card instance.
The combination of the flyweight store and corresponding access method is some-
times referred to as the flyweight factory. In Java, it is common to use static struc-
11A block of code that executes once, when the class is first loaded in the run-time environment.
12The best choice is probably to use the EnumMap library type, but to get the point across with a
minimum of explanation, the array-based solution is more accessible.
88 4 Object State
tures in the flyweight class as the flyweight factory. However, it is not the only op-
tion, as we could easily create a separate class to fulfill this role (called, for example,
CardFactory).
For flyweight objects that represent playing cards, the use of the pair (rank, suit)
as the identification key is intuitive. In other scenarios, it can be less obvious what
the identification key should be. For example, for an object of type Person, the key
could be a name, an identification number, etc. In any case, the identification cannot
be an instance of the flyweight object itself. In our example with cards, such an
approach would look like:
Card card = Card.get(someCard); // INVALID
This would mean that to obtain a flyweight object of class Card, it would be
necessary to already have that object. Because the only way to get a flyweight object
should be through its access method, this scheme leads to an infinite cycle, and is
thus flawed.
An important concern when implementing the F LYWEIGHT pattern is whether to
pre-initialize the flyweight store, or whether to do this lazily, by creating objects as
they are requested through the access method. The answer is context-dependent. In
general, in cases where there exists a small and finite set of flyweights, it may make
sense to pre-initialize them (as in the example). In other cases, additional logic must
be added to the access method to check whether the object exists in the collection
and, if not, create it based on the key. In this latter case, the access method needs
to be able to access all the information it needs to create the flyweight instance that
corresponds to the requested key. The following code shows the F LYWEIGHT-relevant
portion of a version of the Card class where instances are lazily created:
public class Card {
private static final Card[][] CARDS =
new Card[Suit.values().length][Rank.values().length];
The S INGLETON design pattern provides a way to ensure that there is only one in-
stance of a given class at any point in the execution of the code. The context for
this design pattern is the need to manage an instance that holds, in one place, a co-
hesive amount of information that different parts of the code need. An example of
a potential S INGLETON object in a card game would be the instance that represents
the aggregated state of the game. This state could include the deck of cards and the
various piles of cards in the game in progress. The solution template for S INGLETON
involves three elements:
1. A private constructor for the singleton class, so clients cannot create multiple
objects;
2. A global variable for holding a reference to the single instance of the singleton
object.
3. An accessor method, usually called instance(), that returns the singleton in-
stance. The accessor method is optional, because it is also possible to implement
the pattern by declaring the global instance to be a public constant.
In a sample card game, a singleton object that encapsulates the aggregated state
of the game, of class GameModel, could be implemented as follows:
public class GameModel {
private static final GameModel INSTANCE = new GameModel();
This technically works because the compiler will prevent the instantiation of enu-
merated types. Although this approach is presented as preferred in Effective Java,
it is not without detractors. To me, this strategy uses a programming mechanism
(enumerated types) for an intent other than originally designed and, as such, it can
be confusing. Here the type GameModel is not a finite set of values representing
different game models, which is what one would initially expect when seeing an
enum designation. I thus recommend sticking to a private constructor to ensure the
single-instance constraint.
Now that we know about the S INGLETON, I must mention that this pattern is con-
troversial. While all design decisions involve trade-offs (see Section 1.1), in the case
of the S INGLETON, the balance can often tip in favor of the disadvantages. First, a sin-
gleton is essentially a global instance, accessible from anywhere in the code. It is
thus easy to make unprincipled use of this object, leading to numerous dependen-
cies and code that is hard to understand. Second, singleton objects are difficult to
test because they control their own life cycle and live for the entire duration of the
application. Chapter 5 discusses testing in detail but, for now, it suffices to say that
use of the S INGLETON makes a stateful object difficult to replace when necessary.
In many cases, a good alternative for accessing a reference to a unique object is
to use dependency injection (see Section 3.8). However, dependency injection does
not provide a mechanism for preventing the creation of multiple instantiations of a
class. This constraint must be respected with the assistance of methodical program-
ming and documentation. What dependency injection helps achieve, however, is to
propagate that single instance to the code that requires it.
Generally, what is important is to be able to recognize situations where only one
instance of a class must be present in an application, and to be able to evaluate
4.9 The S INGLETON Design Pattern 91
different strategies for creating and managing this unique instance. In cases where
dependency injection or another solution is fit for purpose, it may be preferable to
employ it instead of using a S INGLETON. In some cases, however, the S INGLETON may
turn out to be the preferable solution.
Fig. 4.8 Different categories of Java classes and relations between them. The classifiers in the
diagram represent the categories of classes, not source code classes.
Section 3.4 introduced the use of anonymous classes to implement function ob-
jects. As it turns out, inner classes can sometimes discretely maintain a reference
to other objects. Because this can affect their state space and life cycle in important
ways, it is important to know about this feature of the Java language.
Inner Classes
Let us start with inner classes. Inner classes are declared within another class, and
used to provide additional behavior that involves an instance of the enclosing class,
but which for some reason we do not want to integrate into the enclosing class. As an
example, let us say we want the option to remember how many times a certain Deck
instance was shuffled. As usual, there are different ways of doing this. To illustrate
how inner classes work, we will define a Shuffler class as an inner class of Deck:
4.10 Objects of Nested Classes 93
private Shuffler() {}
In this example, the first part of the declaration of class Shuffler looks pretty
normal: we declare a class Shuffler with a field aNumberOfShuffles and a
method shuffle(). However, things get interesting within the code of Shuffler-
#shuffle(), where we observe the statement Deck.this.shuffle();. Instances
of an inner class automatically get a reference to the corresponding instance of their
enclosing class (called the outer instance). The outer instance for an inner class
is the object that was the implicit parameter of the method where the instance of
the inner class was created. This can be confusing at first, so let us run through an
execution:
Deck deck = new Deck();
Shuffler shuffler = deck.newShuffler();
shuffler.shuffle();
The first line creates a new instance of Deck, as usual. The second line calls method
newShuffler() on the instance of Deck referred to by variable deck. This method
creates a new instance of Shuffler, whose outer instance will be the one referred
to by deck. Within an inner class, the outer instance can be accessed through a
qualified name that consists of the name of the class of the outer instance, followed
by this. So, in our case, Deck.this refers to the outer instance of shuffler. On
the third line, it is the method shuffle() of the Shuffler instance that is called,
but when this method executes, it then calls the shuffle() method of class Deck
on the deck instance using Deck.this. Figure 4.9 illustrates the scenario with an
object diagram.
With this design, the deck can be shuffled through the shuffler instance, which
will remember how many times the method was invoked. It is also possible to shuf-
fle the deck without going through the shuffler instance, in which case the field
aNumberOfShuffles will not be incremented. The important point about this dis-
cussion, however, is that the concrete state space of inner classes adds to that of the
outer instance. Although this can technically lead to increased complexity, it does
94 4 Object State
not have to be so. In good design, the abstract state space of objects of inner classes
should abstract the state of the outer instance. This is the case of the Shuffler
class, for which the state of the outer instance does not influence how we use the
Shuffler instance.
Java also allows the declaration of static nested classes. The main difference
between static nested classes and inner classes is that static nested classes are not
linked to an outer instance. As such, they are mostly used for encapsulation and
code organization.
Anonymous Classes
Just like inner classes, local and anonymous classes also have implicit access to
additional state information through a reference to their outer instance. Because
local classes are rarely used, this section focuses on anonymous classes. However,
local classes work in a very similar way. Let us consider the following code for a
factory method that creates a Comparator<Deck> instance that compares two decks
in terms of the number of cards of a given rank that they have.
public class Deck {
public static Comparator<Deck> createRankComparator(Rank pRank){
return new Comparator<Deck>() {
public int compare(Deck pDeck1, Deck pDeck2) {
return countCards(pDeck1) - countCards(pDeck2);
}
For example, in the code below, to see whether deck1 contains more kings than
deck2, we could do:
Comparator<Deck> comp = Deck.createRankComparator(Rank.KING);
boolean result = comp.compare(deck1,deck2) < 1;
4.10 Objects of Nested Classes 95
In this diagram the factory method is represented as a separate object with field
pRank used to represent its parameter. This method returns a completely new object
of an anonymous class. So that the compare method can still refer to the pRank
parameter, a field pRank is created in the instance of the anonymous comparator,
and the value of pRank is copied to it. A method definition together with references
to its local environment variables is sometimes called a closure.13 As the object
diagram shows, it should be clear that closures can lead to shared references between
object instances. To prevent unexpected behavior, Java prevents referencing external
variables that are reassigned within anonymous classes.14
13 In Java, anonymous classes and lambda expressions are not closures in the strict sense because
they cannot modify the variables they reference. However, because they are as close as we can
get to closures in Java, I employ the term to refer to methods that capture some of the values in
non-local variables, as in this case.
14 Prior to Java 8, local variables referenced within anonymous classes had to be declared final.
With Java 8 the compiler can infer variables to be effectively final without the keyword.
96 Insights
Insights
This chapter defined object state, argued that keeping track of all the different ab-
stract states an object can go through can be difficult, and proposed a number of
techniques for designing classes whose objects have simple and well-structured life
cycles.
• For stateful classes, consider using state diagrams to reason systematically about
the abstract states of the object of the classes and their life cycle;
• Try to minimize the number of meaningful abstract states for objects of a class:
make sure it is not possible to put the object in invalid or useless states, and
avoid storing information just for convenience or non-critical performance im-
provements;
• Avoid using null references to represent legal information in objects and vari-
ables; consider using optional types or the N ULL O BJECT pattern to represent ab-
sent values, if necessary;
• Consider declaring instance variables final whenever possible;
• Be explicit about whether objects of a class should be unique or not;
• If objects are not designed to be unique, override the equals and hashCode
methods; if objects should be unique, consider using the F LYWEIGHT pattern to
enforce uniqueness;
• Consider an explicit structure, such as S INGLETON or dependency injection, for
managing classes that should yield only one instance of a stateful object.
• Remember that additional data can be attached to instances of inner classes, ei-
ther in the form of a reference to an instance of an outer class, or as copies of
local variables bundled in a closure.
Further Reading 97
Further Reading
The Gang of Four book [6] has the original, detailed treatment of the F LYWEIGHT and
S INGLETON patterns. Chapter 3 of Refactoring: Improving the Design of Existing
Code [3] mentions the T EMPORARY F IELD† and L ONG M ETHODS† antipatterns. The
entry Introduce Null Object in Chapter 8 discusses the idea of N ULL O BJECT.
Chapter 10 of Java 8 in Action [16] is entitled Using Optional as a better alter-
native to null, and provides more details and examples on the use of the Optional
type. This is also where I found the anecdote about Tony Hoare. Item 55 in Effective
Java [1] provides useful insights on when and how to use optional types.
The API documentation for Object.equals(Object) and Object.hashCode()
provide additional information on the meaning of equality in Java. The section on
nested classes in the Java Tutorial [10] is a good reference.
Chapter 5
Unit Testing
Design Context
This chapter discusses the testing of code derived from design elements of the Soli-
taire sample application. For simplicity, the examples are slightly adapted from
the actual project code. The first examples focus on the simple library function
Math.abs(int), then a method of the Suit enumerated type. Subsequent examples
revolve around FoundationPile, a class that represents one of the four piles where
finished suits are accumulated in the game. The final design problem concerns the
partial testing of class GameModel, which encapsulates the entire aggregated state of
a game of Solitaire in progress.
Software quality problems are often caused by programmers writing code that does
not do what they expect, and the programmers remaining ignorant of this mismatch
between expectations and reality. As a real example, one bug in JetUML made the
directory structure disappear from the file chooser1 when a user wanted to select the
location where to export a file. The offending code statement was in a method in
charge of deciding whether to accept a file for display in the file chooser or not:
public boolean accept(File pFile) {
return !pFile.isDirectory() &&
(pFile.getName().endsWith("." + pFormat.toLowerCase()) ||
pFile.getName().endsWith("." + pFormat.toUpperCase()));
}
At a quick glance, and with insufficient caffeine, this code looks reasonable: if the
file is not a directory and its name ends with a specified extension, in either lower
or upper case, accept it. The idea of the last clause was to avoid exporting images
to directories named, for example, directory.jpg. Unfortunately, focusing on this
unlikely possibility had obscured the more important requirement that we do need
to show directories in the file chooser. This kind of blind spot is typically how bugs
appear. And they really do like to hide in compound conditional statements. Once
exposed, the fix was trivial: remove the negation operator and replace the first and
operator (&&) with an or operator (||).
One way to detect bugs, and to gain confidence that a part of the code does what
we expect, is to test it. Testing is a software quality assurance technique that can
take many forms. Given that this is an introductory book on software design, I focus
on one specific testing approach called unit testing. The idea of unit testing is to test
a small part of the code in isolation. This way, if a test fails, it is easy to know where
to look for problems.
A unit test consists of one or more executions of a unit under test (UUT) with
some input data and the comparison of the result of the execution against some
oracle. A UUT is whatever piece of the code we wish to test in isolation. UUTs
are often methods, but in some cases they can also be entire classes, initialization
statements, or certain paths through the code. The term oracle designates the correct
or expected result of the execution of a UUT.
For example, the statement: Math.abs(5) == 5; technically qualifies as a test.
Here the UUT is the library method Math.abs(int),2 the input data is the integer
literal 5, and the oracle is, in this case, also the value 5. The comparison of the result
of executing the UUT with the oracle is also called an assertion. The name captures
the idea that the role of the comparison is to assert that the result is what we expect.
When testing instance methods, it is important to remember that the input data
includes the implicit argument (the instance that receives the method call). As a
1 A graphical user interface component that allows the user to select a file by browsing a view of
the file system.
2 Function to compute the absolute value of an int value.
5.1 Introduction to Unit Testing 101
second example that involves an implicit argument, let us consider a version of the
Suit enumerated type that includes an additional method sameColorAs(Suit). In
a standard deck of cards, the Clubs and Spades are black suits and the Diamonds
and Hearts are red suits.
public enum Suit {
CLUBS, DIAMONDS, SPADES, HEARTS;
With this design we are not returning the color of a Suit instance, but rather,
whether the suit is of the same color as some other Suit instance. In the case of
a game where the color of a suit does not matter, only whether it is the same color as
some other suit, this design decision makes a lot of sense. Following the principle
of information hiding, we only provide the information that is absolutely needed.
Returning to our example, something to note is that the code was written by a pro-
grammer who favors compact code over clarity of intent and robustness. To give
ourselves confidence that this actually works despite the hackery, we can write a
unit test for the method:
public static void main(String[] args) {
boolean oracle = false;
System.out.println(oracle == CLUBS.sameColorAs(HEARTS));
}
This example makes it clear that although method sameColorAs takes a single ex-
plicit argument, there are in fact two arguments to the UUT: the explicitly provided
argument (Suit.HEARTS), and the implicit argument: Suit.CLUBS. According to
the definition of a unit test provided above, this main method qualifies as a unit test:
it includes a UUT (Suit#sameColorAs), some input data (CLUBS and HEARTS), an
oracle (false), and an assertion that compares the result with the oracle. Executing
the main method will tell us whether the test passes or not.
If we wanted to increase the number of inputs we test for sameColorAs, we could
add additional pairs of suits. Because there are only four suits, we could actually test
all possible inputs for sameColorAs with a mere 16 tests. This achievement, called
exhaustive testing, is almost never possible (see Section 5.9). Writing an exhaustive
test for sameColorAs will show that the method works correctly for all possible
inputs. This is fabulous, but only ephemerally so. Source code is not set in stone.
To continue with our scenario, let us say that a different programmer comes along
later and, because of some requirement change, reorders the suits as follows with-
out touching the sameColorAs method (perhaps because its implementation looks
fancy):
CLUBS, SPADES, DIAMONDS, HEARTS;
In this case, running the test again will immediately reveal a bug introduced by
the fact that sameColorAs relies on an undocumented and unchecked assumption
102 5 Unit Testing
about the order of enumerated values. This example illustrates the second major
benefit of unit tests: in addition to helping detect bugs in new code, they can also
check that tested behavior that used to meet some specific expectation still does
meet that expectation even after the code changes. Running tests to ensure that what
was tested as correct still is (or for detecting new bugs caused by changes) is called
regression testing.
An important thing to realize about unit testing is what it cannot do. Unit testing
cannot verify code to be correct! When a test passes, it only shows that the one
specific execution of the code that is being tested behaves as expected. There are
software engineering techniques designed to provide certain guarantees about all
possible code executions for a specific code element, but unit testing is not one of
them. Section 5.9 provides further justification for why testing is not a verification
technique.
Although it is possible to test a system manually, unit testing is normally done au-
tomatically. Because in software development the way to automate anything is to
write code to do it, to automate software testing we also write code to test other
code. This task is typically supported by a unit testing framework.3 Unit testing
frameworks automate a lot of the mundane aspects of unit testing, including col-
lecting tests, running them, and reporting the results. In addition to tools to collect
and run tests and display the results, frameworks also include a set of constructs to
allow developers to write tests in a structured way and (if they so choose) efficiently.
The major constructs supported by testing frameworks are test cases, test suites, test
fixtures and assertions. The dominant unit testing framework for Java is called JU-
nit. I will introduce the basics of this framework sufficiently to illustrate all of the
testing techniques covered in this chapter. However, this chapter is not a tutorial on
JUnit, and the Further Reading section provides pointers to additional information
and coaching on how to use JUnit.
In JUnit, a unit test maps to a method. The code below illustrates a series of
simple unit tests with JUnit.
public class AbsTest {
@Test
public void testAbs_Positive() {
assertEquals(5, Math.abs(5));
}
3The example with the main method in the previous section, although it qualifies as automation,
was only to illustrate a point, and should not be considered a reasonable way to test production
code.
5.2 Unit Testing Framework Fundamentals with JUnit 103
@Test
public void testAbs_Negative() {
assertEquals(5, Math.abs(-5));
}
@Test
public void testAbs_Max() {
assertEquals(Integer.MAX_VALUE,Math.abs(Integer.MIN_VALUE));
}
}
The @Test annotation instance indicates that the annotated method should be run
as a unit test. Section 5.4 explains annotations in more detail. For now, it suffices to
say it is a marker we put in the code to mark a method as a unit test. The definition
of the @Test annotation is defined in the JUnit library.4 The code example above
shows a test class (see Section 5.3) that defines three tests, all intended to test the
library method Math.abs(int).
To constitute proper tests, test methods should contain at least one execution of a
unit under test. The way to automatically verify that the execution of a unit under test
has the expected effect is to execute various calls to assert methods. Assert methods
are different from the assert statement in Java. They are declared as static meth-
ods of the class org.junit.jupiter.api.Assertions and all they do is verify
a predicate and, if the predicate is false, report a test failure. The JUnit framework
includes a graphical user interface component called a test runner, which automat-
ically scans some input code, detects all the tests in the input, executes them, and
then reports whether the tests passed or failed. Figure 5.1 shows a screenshot of the
result of executing all the tests in the test class TestAbs (above), using a version of
the test runner available through the Eclipse integrated development environment.
Two tests passed but one test, testAbsMax, failed. Perhaps it seems surprising that
the absolute value of Integer.MIN_VALUE is not, in fact, Integer.MAX_VALUE.
This quirk is documented in the Javadoc header for the method:
Note that if the argument is equal to the value of Integer.MIN VALUE, the most negative
representable int value, the result is that same value, which is negative.
The reason for this design choice is imposed by physical constraints: because
one bit of information is used to encode 0, there is no space left available to encode
the absolute value of Integer.MIN_VALUE in a 32-bit int type. In addition to their
bug detection potential, unit tests are a great way to surface important but obscure
corner cases.
4 The JUnit library must be added to a project’s class path before it can be used. This chapter is
based on JUnit version 5.
104 5 Unit Testing
Fig. 5.1 Result of running JUnit using class AbsTest in the Eclipse IDE
A collection of tests for a project is known as a test suite. By default, a project’s test
suite consists of all the unit tests for the production code in the project. However,
for various reasons, it may be desirable to run only a subset of different tests at
different times (for example, to focus on a specific feature, or save some time). Unit
testing frameworks typically provide mechanisms to define arbitrary tests suites or,
more generally, to run certain subsets of unit tests. As one example, JUnit provides
a @Suite construct that allows a developer to list a number of test classes to be
executed together. As another example, the JUnit plug-in for the Eclipse IDE allows
users to execute the tests for only one package, or even a single test class. Because
executing tests is a concern somewhat independent from the issue of designing them,
the rest of the chapter focuses on writing the tests themselves.
A common question when building a suite of unit tests is how to organize our
tests in a sensible manner. There are different approaches, but in Java a common
idiom is to have one test class per project class, where the test class collects all the
tests that test methods or other usage scenarios that involve the class. Furthermore,
it is common practice to locate all the testing code in a different source folder with
a package structure that mirrors the package structure of the production code. The
rationale for this organization is that in Java classes with the same package name
are in the same package scope independently of their location in a file system. This
means that classes and methods in the test package can refer to non-public (but non-
private) members of classes in the production code, while still being separated from
the production code. The figure below illustrates this idea.
5.4 Metaprogramming 105
5.4 Metaprogramming
In the previous section, we saw that to mark a method as a test, we annotate it with
the string @Test. The unit testing framework can then rely on this annotation to
detect which methods are tests, and then proceed to execute these methods as part
of the execution of the test runner. This general approach, employed by most unit
testing frameworks, is special in that in requires the code to manipulate other code.
Specifically, the testing framework first scans the code to detect tests, and then exe-
cutes the code, without having any explicit code for calls to test methods. This strat-
egy is an illustration of a general programming feature called metaprogramming.
The idea of metaprogramming is to write code that operates on a representation of
a program’s code. Although it may seem confusing at first, metaprogramming is
just a special case of general-purpose programming. When we write code, this code
typically operates on data that represents various things in the world (playing cards,
geometric shapes, bank records, etc.). With metaprogramming, this data happens
to be pieces of software code (classes, methods, fields, etc.). Although metapro-
gramming is a programming feature, it is instrumental for testing, and can be used
to implement many design ideas. In Java, metaprogramming is called reflection,
and library support for metaprogramming features is available through the class
java.lang.Class and the package java.lang.reflect.
Introspection
try {
String fullyQualifiedName = "cards.Card";
Class<Card> cardClass =
(Class<Card>) Class.forName(fullyQualifiedName);
} catch(ClassNotFoundException e) {
e.printStackTrace();
}
As is evident from the code above, calls to forName are brittle: there are many
ways in which they can fail. First, we notice that the call is enclosed in a try-catch
block. Method forName declares the checked exception ClassNotFoundException
and throws it whenever the argument does not correspond to the fully-qualified name
of a class on the class path. This may seem easy to prevent in the example above,
given the use of a string literal to specify the name of the argument to forName.
However, any string can be supplied as argument to Class.forName, so the re-
quested class name may not even be known at compile time, as in the example
below:
public static void main(String[] args) {
try {
Class<?> theClass = Class.forName(args[0]);
} catch(ClassNotFoundException e) {
e.printStackTrace();
}
}
For the same reason, we have to use a type wildcard as the instance of the type
parameter in the type declaration of the variable that receives the reference supplied
by forName. In the last example, the variable is declared as Class<?>. The exact
functioning of the type wildcard is outside the scope of this book, but for now it
suffices to say that it acts as a placeholder for any type. In the previous example,
because we know exactly which type parameter is appropriate for class Card, we
can use a downcast instead.
Besides the forName library method, Java offers two other ways to obtain a ref-
erence to an instance of class Class that are less brittle: class literals, and through
an instance of the class of interest. Both strategies are illustrated in the code below:
Class<Card> cardClass1 = Card.class;
Card card = Card.get(Rank.ACE, Suit.CLUBS);
Class<?> cardClass2 = card.getClass();
System.out.println(cardClass1 == cardClass2);
5.4 Metaprogramming 107
The first line shows the use of class literals. In Java, a class literal is a literal ex-
pression that consists of the name of a class followed by the suffix .class, and that
refers to a reference to an instance of class Class that represents the class named
before the suffix. So, Card.class refers to the instance of class Class that repre-
sents class Card. Because, in the case of class literals, the argument T to the type
parameter of Class<T> is guaranteed to be known at compile time, we can include
it in the variable declaration. Class literals are the least brittle way to obtain a refer-
ence to an instance of class Class, but they require that we know the exact class to
instrospect at compile time.
The final way to obtain an instance of class Class is through an instance of the
class, as illustrated in the second and third lines of the code fragment. As will be
explained in detail in Chapter 7, it is possible to call method getClass() on any
object in a Java program, and the method will return a reference to an instance of
class Class that represents the run-time type of the object. Because of polymor-
phism, this type is not known at compile time, so in this case also we have to use
the type wildcard in the declaration of the variable.5 However, because any call to
getClass() is guaranteed to return a valid reference to an instance of Class, the
method does not declare to throw an exception.
The last line in the code fragment illustrates a very important property of class
Class: its instances are unique (see Section 4.7). If executed, the code should always
print true on the console. Indeed, class Class has no accessible constructor, and its
instances can be considered to be unique flyweight objects (see Section 4.8).
With metaprogramming, we can introspect any class, including class Class. This
may at first seem contrived, but it is actually not a special case: class Class is just
another class. The following code:
Class<Class> classClass = Class.class;
will produce the object graph illustrated in the diagram in Figure 5.4.
Obtaining an instance of class Class is only the first step for instrospection. The
interface to class Class provides numerous methods that can be called to obtain ob-
jects that represent the members of the class, its superclass, etc. As one example of
endless possibilities, the following code fragment prints the name of all the meth-
ods declared in class String. This example makes use of class Method, a library
class intended to represent methods in Java code. Similar classes exist to represent
constructors (Constructor) and fields (Field).
5 In this case it is possible to use a type bound, e.g., Class<? extends Card>.
108 5 Unit Testing
Program Manipulation
Obtaining information about a code base, or code introspection, only constitutes the
most basic form of metaprogramming. However, in many cases we also want to
actually manipulate the code of a program. Unlike more dynamic languages, Java
does not allow adding or removing members from classes (and objects, by exten-
sion). However, in Java it is possible to use metaprogramming features to change
the accessibility of class members, set field values, create new instances of objects,
and invoke methods. I only provide a small overview of the features most relevant
to software testing and design in general. The API documentation of the relevant
library classes will provide the catalog of possibilities.
Assuming that our implementation of class Card is a realization of the F LYWEIGHT
design pattern and has a private constructor, we will use metaprogramming to get
around the pattern and create a duplicate Ace of Clubs.
try {
Card card1 = Card.get(Rank.ACE, Suit.CLUBS);
Constructor<Card> cardConstructor =
Card.class.getDeclaredConstructor(Rank.class, Suit.class);
cardConstructor.setAccessible(true);
Card card2 = cardConstructor.newInstance(Rank.ACE, Suit.CLUBS);
System.out.println(card1 == card2);
} catch( ReflectiveOperationException e ) {
e.printStackTrace();
}
try {
Card card = Card.get(Rank.TWO, Suit.CLUBS);
Field rankField = Card.class.getDeclaredField("aRank");
rankField.setAccessible(true);
rankField.set(card, Rank.ACE);
System.out.println(card);
} catch( ReflectiveOperationException e ) {
e.printStackTrace();
}
This code works similarly to the previous example, but with one notable differ-
ence. Because rankField represents a field of class Card, as opposed to an instance
of the class, the call to set needs to know on which instance the field should be as-
signed a value. Thus, the first argument to set is the instance of Card on which we
want to assign a new value to the field aRank, and the second argument is the actual
value we want to assign.
Program Metadata
With metaprogramming, it is possible for code to operate not only on data that
consists of code elements (e.g., classes, methods, fields), but also on metadata about
these code elements. In Java and similar languages, it is possible to attach additional
information (i.e., meta-information) to code elements in the form of annotations. We
have already seen one type of annotation: the use of @Test to indicate that a method
is a unit test in JUnit.
To flag methods as tests for a unit testing framework, it would also have been
possible to indicate this information in the comments, using some sort of convention.
For example:
public class AbsTest {
/* TEST */
public void testAbsPositive() {
assertEquals(5,Math.abs(5));
}
}
Then, annotation instances can be added to the code, in the form @Test. The main
advantage of annotations in Java is that they are typed and checked by the com-
piler. The @Test annotation used to flag unit tests in JUnit is thus a type annotation
provided by the JUnit library, and its use is checked by the compiler. For example,
110 5 Unit Testing
writing @test (with a lowercase ‘t’) will result in a compilation error. Java an-
notations support many other features (see Further Reading), but their main usage
scenario is to provide a way to add structured, type-checked metadata to some code
elements, that can then be read by the compiler, development environments, unit
testing frameworks, and similar tools. We will see other example applications of an-
notations later in the book. Because they are officially part of the code, information
about annotations can also be accessed through metaprogramming.6
Writing unit tests for non-trivial classes is often a challenging creative process, not
unlike writing production code. For this reason, there is no standard formula or tem-
plate for writing the code of a unit test. In fact, browsing the test suites of different
open-source projects will show that different communities follow different styles
and use different testing techniques. This being said, certain basic principles are
generally agreed upon, including that unit tests should be fast, independent, repeat-
able, focused, and readable [7].
• Fast. Unit tests are intended to be run often, and in many cases within a
programming-compilation-execution cycle. For this reason, whatever test suite
is executed should be able to complete in the order of a few seconds. Otherwise,
developers will be tempted to omit running them, and the tests will stop being
useful. This means that unit tests should avoid long-running operations such as
intensive device I/O and network access, and leave the testing of such function-
ality to tests other than unit tests. These could include, for example, acceptance
tests or integration tests.
• Independent. Each unit test should be able to execute in isolation. This means
that, for example, one test should not depend on the fact that another test executes
before to leave an input object in a certain state. First, it is often desirable to ex-
ecute only a single test. Second, just like code, test suites evolve, with new tests
being added and (to a minimum extent) some tests being removed. Test indepen-
dence facilitates test suite evolution. Finally, JUnit and similarly designed testing
frameworks provide no guarantee that tests will be executed in a predictable or-
der. In practice, this means that each test should start with a fresh initialization
of the state used as part of the test.
• Repeatable. The execution of unit tests should produce the same result in differ-
ent environments (for example, when executed on different operating systems).
This means that test oracles should not depend on environment-specific proper-
ties, such as display size, CPU speed, or system fonts.
6 However, only annotation instances of annotation types marked with the @Retention-
(value=RUNTIME) meta-annotation can be accessed in this way. See Further Reading for a
reference to complementary information on annotations that covers meta-annotations.
5.5 Structuring Tests 111
• Focused. Tests should exercise and verify a slice of code execution behavior that
is as narrow as reasonably possible. The rationale for this principle is that the
point of unit tests is to help developers identify faults. If a unit test comprises 500
lines of code and tests a whole series of complex interactions between objects,
it will not be easy to determine what went wrong if it fails. In contrast, a test
that checks a single input on a single method call will make it easy to home in
on a problem. Some have even argued that unit tests should comprise a single
assertion [7]. My opinion is that in many cases this is too strict and can lead to
inefficiencies. However, tests should ideally focus on testing only one aspect of
one unit under test. If that unit under test is a method, we can refer to it as the
focal method for the test.
• Readable. The structure and coding style of the test should make it easy to iden-
tify all the components of the test (unit under test, input data, oracle), as well as
the rationale for the test. Are we testing the initialization of an object? A special
case? A particular combination of values? Choosing an appropriate name for the
test can often help in clarifying its rationale.
For example, let us write some unit tests for a method canMoveTo of a hypothet-
ical class FoundationPile that could be part of the design of the Solitaire example
application. The method should return true only if it is possible to move the input
pCard to the top of the pile that an instance of the class represents. According to
the rules of the game, this is only possible if the pile is empty and the input card is
an ace, or if the input card is of the same suit as the top of the pile, and of a rank
immediately above the top of the pile (e.g., you can only put a Three of Clubs on
top of a Two of Clubs).
public class FoundationPile {
public boolean isEmpty() { ... }
public Card peek() { ... }
public Card pop() { ... }
public void push(Card pCard) { ... }
As our first test, we will keep things small and only test for the case where the
pile is empty:
112 5 Unit Testing
This test respects our five desired properties. It will execute with lightning speed,
be independent from any other test that could exist, and is not affected by any en-
vironment properties. It is also focused, not only on a single method, but also on a
specific input combination for the method. Finally, many properties of this test add
to its readability. First, the name of the test encodes both the focal method and the
input of interest. Second, the names of the variables describe their content. Finally,
the assertion statements are self-evident. Reading the last line of the test, for ex-
ample, we see that calling canMoveTo with a Three of Clubs on an empty pile will
return false, which is correct.
One issue with this test, however, is its coverage. The test only verifies a sin-
gle path through the method, so many possible executions of the method remain
untested. This issue will be further discussed in Section 5.9. For now, let us try to
fix it by writing an additional test in the same class
@Test
public void testCanMoveTo_NotEmptyAndSameSuit() {
Card aceOfClubs = Card.get(Rank.ACE, Suit.CLUBS);
Card twoOfClubs = Card.get(Rank.TWO, Suit.CLUBS);
Card threeOfClubs = Card.get(Rank.THREE, Suit.CLUBS);
FoundationPile pileWithOneCard = new FoundationPile();
pileWithOneCard.push(aceOfClubs);
assertTrue(pileWithOneCard.canMoveTo(twoOfClubs));
assertFalse(pileWithOneCard.canMoveTo(threeOfClubs));
}
This test improves the test suite by adding to the coverage. However, we already
note a lot of redundant code between the two tests, namely, the code to create an in-
stance of FoundationPile, and the code to create cards. If we had, say, 20 tests, this
would look suboptimal. In test classes that group multiple test methods, it will often
be convenient to define a number of default objects or values to be used as receiver
objects, explicit parameters, and/or oracles. This practice avoids the duplication of
setup code in each test method, which constitutes D UPLICATED C ODE†. Baseline ob-
jects used for testing are often referred to as a test fixture, and declared as fields
of a test class. However, for the reasons discussed above, and in particular because
JUnit provides no ordering guarantee of any test execution, it is crucial to preserve
test independence. This implies that no test method should rely on the fixture being
left in a given state by another test. Conveniently, unit testing frameworks can help
avoid this problem. By default, JUnit 5 will instantiate a fresh version of the test
class before running every test method in the class. For this reason, the values of
5.5 Structuring Tests 113
the fields of the test class will contain their initial value when any test executes.7
Of course, immutable objects do not need to be reinitialized, so they can be stored
as static fields of the class. The code below shows an improved version of our test
class TestFoundationPile, which now uses a test fixture.
public class TestFoundationPile
{
private static final Card ACE_CLUBS =
Card.get(Rank.ACE, Suit.CLUBS);
private static final Card TWO_CLUBS =
Card.get(Rank.TWO, Suit.CLUBS);
private static final Card THREE_CLUBS =
Card.get(Rank.THREE, Suit.CLUBS);
@Test
public void testCanMoveTo_Empty() {
assertTrue(aPile.canMoveTo(ACE_CLUBS));
assertFalse(aPile.canMoveTo(THREE_CLUBS));
}
@Test
public void testCanMoveTo_NotEmptyAndSameSuit() {
aPile.push(ACE_CLUBS);
assertTrue(aPile.canMoveTo(TWO_CLUBS));
assertFalse(aPile.canMoveTo(THREE_CLUBS));
}
}
This code not only avoids duplication, but also increases the readability of the tests
by decluttering them. The first test in particular is very readable, while the second
only needs to add one line of test-specific initialization. The only regression in test
readability is due to the fact that by using a field to refer to the pile, we lose our flex-
ibility to name the pile with a variable name that describes its state. In this context
this is a small price to pay for the benefit of using the fixture. Although it would al-
ways be possible to alias the aPile field into an appropriately named variable (e.g.,
emptyPile), it is not clear that this would necessarily improve readability, because
aliasing can also hinder code comprehension.
7 As an alternative, it is possible to reuse an instance of the test class for multiple tests, but to
nominate a method of the test class to execute before any test method, and initialize all the required
structures afresh. These features are available in JUnit via different annotations.
114 5 Unit Testing
An important point when writing unit tests is that what we are testing is that the
unit under test does what it is expected to. This means that when using design by
contract, it does not make sense to test code with input that does not respect the
method’s preconditions, because the resulting behavior is unspecified. For example,
let us consider a version of method peek of class FoundationPile (introduced in
the previous section) which returns the top of the pile.
class FoundationPile {
boolean isEmpty() { ... }
/*
* @return The card on top of the pile.
* @pre !isEmpty()
*/
Card peek() { ... }
}
The documented precondition implies that the method cannot be expected to fulfill
its contract (to return the top card) if the precondition is not met. Thus, if we call the
method on an empty pile, there is no expectation to test. The situation is different,
however, when raising exceptions is explicitly part of the interface. Let us consider
the following slight variant of method peek():
class FoundationPile {
boolean isEmpty() { ... }
/*
* @return The card on top of the pile.
* @throws EmptyStackException if isEmpty()
*/
Card peek() {
if( isEmpty() ) {
throw new EmptyStackException();
}
...
}
}
5.6 Tests and Exceptional Conditions 115
A design issue that often comes up when testing is that to write the test we want,
we need some functionality that is not part of the interface of the class being tested.
For example, let us consider that the interface to class FoundationPile (see Sec-
tion 5.5) does not include a method to return the number of cards in the pile (e.g.,
size()). Presumably, because FoundationPile does not have a size() method,
and following the principle of information hiding, we can assume that no part of
the production code needs this information. However, it would be very convenient
for testing to check that, for example, the size of the pile changes as we push cards
onto it or pop cards from it. Would it not make sense, then, to add a size() method
to the interface of our target class? Although experts might disagree, my personal
recommendation is to design production code to have the best and tightest encapsu-
lation possible, and let the testing code work around whatever constraints this may
impose. A typical solution when an interface does not include a method that would
be convenient for testing, is to provide the desired functionality in the form of a
helper method in the testing class instead. For example:
public class TestFoundationPile {
private FoundationPile aPile = new FoundationPile();
while( !aPile.isEmpty() ) {
size++;
temp.add(aPile.pop());
}
while( !temp.isEmpty() ) {
aPile.push(temp.remove(temp.size() - 1));
}
return size;
}
}
There are often many ways to obtain the information we need without polluting the
interface of the target class. In cases where the information is difficult to obtain using
methods of the class’s interface, metaprogramming is a recourse (see Section 5.4).
A related question when writing tests is, how can we test private methods? There
are different possible avenues for answering that question, and again experts dis-
agree about which one is best:
• Private methods are internal elements of other, accessible methods, and therefore
are not really units that should be tested. Following this logic, the code in pri-
vate methods should be tested indirectly through the execution of the accessible
methods that call them;
5.7 Encapsulation and Unit Testing 117
• The private access modifier is a tool to help us structure the project code, and
tests can ignore it.
Although I understand the rationale of the first approach, I personally consider
that the second option can also be a reasonable strategy. There are often situations
where a neat little method can be restricted to a class’s scope, but it would still be
valuable to test it in isolation. Situations where private methods should probably
not be tested separately are when their parameters or return type encode detailed
information about the internal structure of the class, or make narrow assumptions
about the internal implementation of the class.
In cases where it is judged desirable to test a private method, we need to bypass
the method’s access restriction. This can be done using metaprogramming. For sake
of discussion, let us assume that class FoundationPile also has a method:
private Optional<Card> getPreviousCard(Card pCard) { ... }
@Test
public void testGetPreviousCard_empty() {
assertFalse(getPreviousCard(Card.get(Rank.ACE, Suit.CLUBS)).
isPresent());
}
}
In the test class, we define a helper method that launches the execution of the unit
under test (getPreviousCard(Card)) on an instance of aPile, which forms part
of the test fixture. With this helper method, the code of the test looks pretty normal.
However, there is one big difference. Here, the call to getPreviousCard is not a
call to the unit under tests. Instead, the call is to a helper method (of the same name)
that uses metaprogramming to call the unit under test while bypassing the access
restriction of the private keyword.
118 5 Unit Testing
The key to unit testing is to test small parts of the code in isolation. In some cases,
however, factors can make it difficult to test a piece of code in isolation, for example,
when the part we want to test:
• triggers the execution of a large chunk of other code;
• includes sections whose behavior depends on the environment (e.g., system
fonts);
• involves non-deterministic behavior (e.g., randomness).
Such a case is illustrated in the following design, which is a simplified version
of the Solitaire example application. The GameModel class has a tryToAutoPlay()
method that triggers the computation of the next move by dynamically delegating
the task to a strategy, which could be any of a number of options (see Figure 5.5).
Here we would like to write a unit test for the GameModel.tryToAutoPlay(...)
method.
well with the concept of unit testing, where we want to test small pieces of code
in isolation.
• The implementation of the strategy may involve some randomness.
• We do not know which strategy would be used by the game engine. Presumably
we need to determine an oracle for the results.
• It is unclear how is this different from testing the strategies individually.
The way out of this conundrum is the realization that the responsibility of
GameModel#tryToAutoPlay(...) is not to compute the next move, but rather to
delegate this to a strategy. So, to write a unit test that tests that the UUT does what
it is expected to do, we only need to verify that it properly relays the request to
compute a move to a strategy. This can be achieved with the writing of a stub.
A stub is a greatly simplified version of an object that mimics its behavior suffi-
ciently to support the testing of a UUT that uses this object. Using stubs is heavily
dependent on types and polymorphism. Continuing with our tryToAutoPlay situ-
ation, we just want to test that the method calls a strategy method, so we will define
a dummy strategy in the test class:
public class TestGameModel {
static class StubStrategy implements PlayingStrategy {
private boolean aExecuted = false;
This strategy does nothing except remember that its computeNextMove method
has been called, and returns a dummy Move object called NullMove (an application
of the N ULL O BJECT pattern). We can then use an instance of this stub instead of a
real strategy in the rest of the test. To inject the stub into the game model, we can
rely on metaprogramming:
@Test
public void testTryToAutoPlay() {
Field strategyField =
GameModel.class.getDeclaredField("aPlayingStrategy");
strategyField.setAccessible(true);
StubStrategy strategy = new StubStrategy();
GameModel model = GameModel.instance();
strategyField.set(model, strategy);
...
}
at which point completing the test is just a matter of calling the UUT tryToAuto-
Play and verifying that it did properly call the strategy:
120 5 Unit Testing
@Test
public void testTryToAutoPlay() {
...
model.tryToAutoPlay();
assertTrue(strategy.hasExecuted());
}
The use of stubs in unit testing can get very sophisticated, and frameworks exist
to support this task if necessary.
Up to now this chapter covered how to define and structure unit tests from a practical
standpoint, but avoided the question of what inputs to provide to the unit under test.
Despite the example of exhaustive testing in Section 5.1, it should be clear that for
the vast majority of UUTs it is not even physically possible to exhaustively test
the input space. For example, as discussed in Section 4.2, the number of different
arrangements of cards that an instance of class Deck can take is astronomical (2.2 ×
1068 ). Even with a cutting-edge CPU, testing any of the methods of class Deck for all
possible inputs (i.e., possible states of the implicit parameter) would take an amount
of time greater than many times the age of the universe. This is quite incompatible
with the requirement that unit tests execute quickly (see Section 5.5).
Clearly we need to select some input out of all the possibilities. This is a problem
known as test case selection, where test case can be considered to be a set of input
values for an assumed UUT. For example, an instance of Deck with a single Ace of
Clubs in the deck is a test case of the method Deck.draw(). The basic challenge of
the test case selection problem is to test efficiently, meaning to find a minimal set of
test cases that provides us a maximal amount of testing for our code. Unfortunately,
while it is fairly intuitive what a minimal number of test cases is, there is no natural
or even agreed-upon definition of what an amount of testing is. However, there is
a large body of research results and practical experience on the topic of test case
selection: enough for many books (see Further Reading for a recommendation). In
this section, I only summarize the key theoretical tenets and practical insights nec-
essary to get started with test case selection. There are two basic ways to approach
the selection of test cases:
• Functional (or black-box) testing tries to cover as much of the specified behav-
ior of a UUT as possible, based on some external specification of what the UUT
should do. For the Deck.draw() method, this specification is that the method
should result in the top card of the deck being removed and returned. There are
many advantages to black-box testing, including that it is not necessary to access
the code of the UUT, that tests can reveal problems with the specification, and
that tests can reveal missing logic.
• Structural (or white-box) testing tries to cover as much of the implemented
behavior of the UUT as possible, based on an analysis of the source code of the
5.9 Test Coverage 121
Here, we can intuitively see that the code structure can be partitioned into differ-
ent pieces that might be good to test. First, there is the case where the pile is empty
(the true part of the if statement), and the case where it is not empty (the else
block). But then, each of these pieces can also be partitioned into different sub-
pieces, for example to cover the case where the cards are in the correct sequence,
but of different suits. In the general case, things can get hairy, and it is easy to get
lost without a systematic way to understand the code.
One common method for determining what to test is based on the concept of cov-
erage. A test coverage metric is a number (typically a percentage) that determines
how much of the code executes when we run our tests. Test coverage metrics can
be computed by code coverage tools that keep track of the code that gets executed
when we run unit tests. This sounds simple, but the catch is that there are different
definitions of what we can mean by code, in the context of testing. Each definition
is a different way to compute how much testing is done. Certain software devel-
opment organizations may have well-defined test adequacy criteria whereby test
suites must meet certain coverage thresholds, but in many other cases, the insights
provided by coverage metrics are used more generally to help determine where to
invest future testing efforts. The following are three well-known coverage metrics
(there are many others, see Further Reading).
Statement Coverage
Let us start with the simplest coverage metric: statement coverage. Statement cov-
erage is the number of statements executed by a test or test suite, divided by the
number of statements in the code of interest. The following test:
122 5 Unit Testing
@Test
public void testCanMoveTo_Empty() {
assertTrue(aPile.canMoveTo(ACE_CLUBS));
assertFalse(aPile.canMoveTo(THREE_CLUBS));
}
achieves 2/3 = 67% coverage, because the conditional statement predicate and the
single statement in the true branch are executed, and the single statement in the
false branch is not. The logic behind statement coverage is that if a fault is present
in a statement that is never executed, the tests are not going to help find it. Although
this logic may seem appealing, statement coverage is actually a poor coverage met-
ric. A first reason is that it depends heavily on the detailed structure of the code. We
could rewrite the canMoveTo method as follows, and achieve 100% test coverage
with exactly the same tests.
boolean canMoveTo(Card pCard) {
boolean result = pCard.getSuit() == peek().getSuit() &&
pCard.getRank().ordinal() ==
peek().getRank().ordinal()+1;
if( isEmpty() ) {
result = pCard.getRank() == Rank.ACE;
}
return result;
}
The second reason is that not all statements are created equally, and there can be
quite a bit that goes on in a statement, especially if this statement involves a com-
pound Boolean expression (as is the case of the first statement in the last example).
Branch Coverage
@Test
public void testCanMoveTo_NotEmptyAndSameSuit() {
aPile.push(ACE_CLUBS);
assertTrue(aPile.canMoveTo(TWO_CLUBS));
assertFalse(aPile.canMoveTo(THREE_CLUBS));
}
we get 7/8 = 87.5%. This is pretty good, but our systematic coverage analysis points
out that we are actually missing one branch, which corresponds to the case where
the pile is not empty, and the card at the top of the pile and the card passed as
argument are of different suits. Branch coverage is one of the most useful test cov-
erage criteria. It is well supported by testing tools and relatively straightforward to
interpret, and also subsumes statement coverage, meaning that achieving complete
branch coverage always implies complete statement coverage.
Path Coverage
There are other coverage metrics stronger than branch coverage. For example, one
could, in principle, compute a path coverage metric as the number of execution
paths actually executed over all possible execution paths in the code of interest.
Path coverage subsumes almost all other coverage metrics, and is a very close ap-
proximation of the entire behavior that is possible to test. Unfortunately, in many
cases, the number of paths through a piece of code will be unbounded, so it will not
be possible to compute this metric. For this reason, path coverage is considered a
theoretical metric, useful for reasoning about test coverage in the abstract, but with-
out any serious hope of general practical applicability. Interestingly, the number of
paths in the code of canMoveTo is actually only five, so, less than the number of
branches! The path coverage of the two-test test suite above can thus be computed,
at 4/5 = 80%. Because the structure of the code is without loops, this is not overly
surprising. However, as soon as loops enter the picture, reasoning about paths be-
comes troublesome.
Insights
This chapter described techniques to structure and implement unit tests for a project,
and argued that unit tests can provide valuable feedback on the design of production
code. The following insights assume you have decided to adopt unit testing and are
using a unit testing framework.
• Every unit test includes the execution of unit under test (UUT), some input data
passed to the UUT, an oracle that describes what the result of executing the UUT
should be, and one or more assertions that compare the result of the execution
with the oracle;
124 Further Reading
• Design your unit tests so that they are focused, that is, that they isolate and test a
small and well-defined amount of behavior;
• Design your unit tests to run fast, be independent from each other, and be repeat-
able in any computing environment;
• Design your unit tests to be readable: consider using the name of the test and local
variables to add clarity about what you are testing and to describe the oracle;
• Organize your test suite cleanly, with a clear mapping between tests and the code
units they test. Consider separate source code directories with a parallel package
structure for production and test code;
• Metaprogramming is a powerful language feature that allows you to write code
to analyze other code. However, it must be used with care in production code,
because it is prone to run-time errors;
• Type annotations can provide metadata about certain program elements, which
can then be accessed through metaprogramming;
• Use test fixtures to structure your testing code cleanly. Remember that any data
used by unit tests must be initialized before every test, since tests are not guaran-
teed to be executed in any specific order;
• Do not test for unspecified behavior, and in particular for input that does not
respect a method’s preconditions;
• Exceptions that can be raised are often an explicit part of a method’s interface, in
which case the raising of exceptions constitutes behavior that can be tested;
• Do not weaken the interface of a class only to provide additional state inspection
methods for your tests. Instead, write helper methods in the test class to obtain
this information. Consider using metaprogramming for the trickier cases;
• To isolate the behavior of stateful objects that refer to many other objects, con-
sider using stubs to abstract the behavior of the component objects;
• Use test coverage metrics to reason about how much of the program’s behavior
you are testing. Favor branch coverage over statement coverage;
• Remember that passing tests do not guarantee that the code is correct.
Further Reading
The book Software Testing and Analysis by Pezzè and Young [13] provides a com-
prehensive treatment of testing, including the definition of many test coverage met-
rics. The Java Tutorial [10] provides a good introduction to annotations and reflec-
tion (Java’s version of metaprogramming). Documentation on how to use JUnit is
available on the JUnit website.
Chapter 6
Composition
Large software systems are assembled from smaller parts. In object-oriented design,
parts are connected through two main mechanisms: composition and inheritance.
Composition means that one object holds a reference to another object and dele-
gates some functionality to it. Although this sounds straightforward, unprincipled
composition can lead to a mess of spaghetti code. In this chapter I give a quick
refresher on the mechanism of polymorphism and how it can be used to elegantly
compose objects together by following some well-known design patterns. The sec-
ond way of assembling systems is through inheritance, which is more complex and
is covered in Chapter 7.
Design Context
This chapter draws its code examples from various problems related to the modeling
of card games. The design problems address requirements at different levels of ab-
straction, from the management of low-level structures to represent a card source, to
high-level structures that can represent the entire state of a card game. To support a
discussion of a variety of potential design alternatives, the examples are not limited
to the context of a Solitaire application, but also consider other usage scenarios. For
the examples that do target the Solitaire application specifically, knowledge of the
game terminology will be useful: see Appendix C for definitions of the main game
concepts and terms and an overview of a game in progress.
where elements can be thought of as either components (i.e., essential parts of the
aggregate object) or delegates (i.e., service providers for the aggregate object).
Let us make this discussion more concrete by studying the GameModel class of
the Solitaire example application. Figure 6.1 shows a simplified class diagram of the
GameModel. The diagram shows how class GameModel is an aggregate of one Deck,
one CardStack (the discard pile), one Foundations, and one Tableau. A first thing
to observe is that in this version of the code, instead of having a Deck class aggregate
Card objects using the List library type, I used composition to define a dedicated
type CardStack that provides a narrow interface dedicated to handling stacks of
cards. The following is a partial implementation:
public class CardStack implements Iterable<Card> {
private final List<Card> aCards = new ArrayList<>();
Fig. 6.1 Class diagram of the GameModel. Composition relations are represented using the white
diamond decoration. The diamond is on the side of the aggregate. Normally, in a class diagram,
model elements that represent a given class are not repeated. In this diagram I took the liberty of
repeating CardStack for clarity. All CardStack elements, however, represent the same class.
/**
* Removes a card from the source and returns it.
*
* @return The card that was removed from the source.
* @pre !isEmpty()
*/
Card draw();
/**
* @return True if there is no card in the source.
*/
boolean isEmpty();
}
The main feature of this design decision is that the set of possible implemen-
tations of CardSource is specified statically (in the source code), as opposed to
dynamically (when the code runs). Three major limitations of this static structure
are:
• The number of possible structures of interest can be very large. As illustrated
by the fifth definition, DeckAndFourAces, supporting all possible configurations
leads to a combinatorial explosion of class definitions.
• Each option requires a class definition, even if it is used very rarely. This clutters
the code unnecessarily, because most implementations would probably look very
similar.
• In running code, it is very difficult to accommodate the situation where a type of
card source configuration is needed that was not anticipated before launching the
application.
The above limitations are derived from the static nature of the design. A gen-
eral solution is to support an open-ended number of configurations by relying on
object composition as opposed to class definition. The fundamental idea to support
this approach is to define a class that represents multiple CardSources while still
behaving like a single one. This core idea is captured as the C OMPOSITE design pat-
tern. Figure 6.2 shows a class diagram of the C OMPOSITE applied to the CardSource
context.
The diagram shows the application of the pattern, and the roles of each element in
the solution template are indicated in notes. In this pattern, the three main roles are
component, composite, and leaf. The composite element has two important features:
• It aggregates a number of different objects of the component type (CardSource
in our case). Using the component interface type is important, as it allows the
6.2 The C OMPOSITE Design Pattern 131
In the case of method draw, the behavior is a bit special. Instead of delegating the
method call to all elements, we only need to iterate until we can find one card to
draw.
132 6 Composition
Here we use the copy constructor to avoid leaking a reference to the private col-
lection structure (see Section 2.5). Another option would be to use Java’s varargs
mechanism to list each card source individually (see Further Reading):
1 This assumes a single-threaded system. Concurrent programming is outside the scope of this
book.
6.2 The C OMPOSITE Design Pattern 133
The main reason for adopting the “add method” strategy is if we need to modify
the state of the composite at run time. However, this comes at a cost in terms of
design structure and code understandability, because we need to deal with a more
complex life-cycle for the composite object and have to manage the difference be-
tween the interface of the component (which does not have the add method) and
the one of the composite (which does). If run-time modification of the composite is
not necessary, then it is likely a better option to initialize the composite once and
leave it as is. In the context of the CardSource example, it would not result in an
immutable composite (we still draw cards), but in other contexts immutability may
be an additional advantage.
Some practical aspects related to using the pattern are independent from the
structure of the pattern itself. These include:
• The location of the creation of the composite in client code;
• The logic required to preserve the integrity of the object graph induced by this
design.
Because these concerns are context-dependent, their solution will depend on the
specific design problem at hand. However, it is important to be aware that simply
creating a well-designed composite class is not sufficient to have a correct applica-
tion of the C OMPOSITE. For example, with the design of Figure 6.2, it could be possi-
ble to write code that results in the object graph of Figure 6.4. However, this outcome
is very likely undesirable, because the shared deck instance between source1 and
source2 and the self-reference in source2 would lead to unmanageable behavior.
Fig. 6.4 Object diagram showing an abused design for a composite CardSource
134 6 Composition
The use of composition in software design implies design decisions that have to
do with how objects collaborate with each other. This means that the impact of
composition-related design decisions is reflected on how objects end up calling each
other.2 We can contrast this to more static design decisions, which have to do with
how classes depend on each other. For example, an important consequence of the use
of the C OMPOSITE for CardSource is that to determine if a CompositeCardSource
is empty, we need to call isEmpty() on some, and possibly all, of its elements.
It can sometimes be helpful to model certain design decisions related to object
call sequences. With the UML, this is accomplished through sequence diagrams.
Just like object diagrams and state diagrams, sequence diagrams model the dynamic
perspective on a software system. Like object diagrams and as opposed to state
diagrams, sequence diagrams represent a specific execution of the code. They are the
closest representation to what one would see when stepping through the execution
of the code in a debugger, for example.
To introduce sequence diagrams, and bring home the point that the C OMPOSITE
pattern is really a way to organize how objects interact, Figure 6.5 shows a sequence
diagram that models a call to isEmpty() on an instance of CompositeCardSource.
Each rectangle at the top of the diagram represents an object. An object in a se-
quence diagram is also referred to as implicit parameter, because it is the object
upon which a method is called. Consistently with other UML diagrams that repre-
2 Objects calling each other is a linguistic shortcut. The precise, but more cumbersome, phrasing
would be code of a method with a given implicit argument calling methods with other objects as
implicit arguments.
6.3 Sequence Diagrams 135
sent the system at run time, the object names are underlined and follow the conven-
tion name:type as necessary. Here I did not specify a type for the client because it
does not matter, and did not specify a name for any of the other objects because it
does not matter either.
In the diagram, we observe the recursive descent through an instance of Composi-
teCardSource. This information cannot be captured in a class diagram, because the
notation does not support the specification of the behavior of the different methods,
even at an abstract level. This diagram complements the class diagram of Figure 6.2
by showing a dynamic aspect of the design that is invisible on the class diagram.
The dashed vertical line emanating from an object represents the object’s life
line. The life line represents the time (running from top to bottom) when the object
exists, that is, between its creation and the time it is ready to be garbage-collected.
When objects are placed at the top of the diagram, they are assumed to exist at the
beginning of the scenario being modeled. The diagram thus shows an interaction
between a client object and an instance of CompositeCardSource and all its com-
ponent objects, all of which were created before the modeled interaction began. How
these objects were created is an example of details left unspecified by a particular
diagram.
When representing the type of an object in a sequence diagram, there is some
flexibility in terms of what type to represent in the object’s type hierarchy. We can
use the concrete type of the object or one of its supertypes. As usual when modeling,
we use what is the most informative. Here the CompositeCardSource and Deck
objects are represented using their concrete type because the only other option is
CardSource, which makes the information in the diagram less self-explanatory.
Messages between objects typically correspond to method calls. Messages are
represented using a directed arrow from the caller object to the called object. By
called object I mean the object that is the implicit parameter of the method call.
Messages are typically labeled with the method that is called, optionally with some
label representing arguments, when useful. When creating a sequence diagram that
represents an execution of Java code, it is likely to be a modeling error if a message
incoming on an object does not correspond to a method of the object’s interface.
Constructor calls are modeled as special messages with the label <<create>>.
Messages between objects induce an activation box, which is the thicker white
box overlaid on the life line. The activation box represents the time when a method
of the corresponding object is on the execution stack (but not necessarily at the top
of the execution stack).
It is also possible to model the return of control out of a method back to the
caller. This is represented with a dashed directed arrow. Return edges are optional.
I personally only use them to aid understanding when there are complex sequences
of messages, or to give a name to the value that is returned to make the rest of
the diagram more self-explanatory. Here, for example, I included return edges to
provide the rationale for subsequent calls in the sequence (given that the execution
terminates as soon as isEmpty() returns false).
To explore some of the additional modeling features of sequence diagrams and
their potential, let us model the use of an iterator in the I TERATOR pattern (see Sec-
136 6 Composition
tion 3.6). Figure 6.6 shows the class diagram of the specific application of I TERATOR
I model with a sequence diagram.
This diagram shows a version of the Deck class that relies on a collection type
Stack to store cards. Both the Deck and the Stack are iterable. The client code,
represented as class Client, can refer to instances of class Deck as well as the
iterators they return.
Let us look at what happens when the client code makes a call to Deck.itera-
tor(). Figure 6.7 is the sequence diagram that models a specific execution of
Deck.iterator() within client code. The names of model elements are provided
as notes on the diagram.
The iterator() message to a Deck instance leads to the call being delegated to
the Stack object. The Stack object is responsible for creating the iterator. It is also
possible to show the creation of an instance by placing it lower in the diagram, as in
the case here for the Iterator object. The label iterator is used on the return edge
from both iterator() calls to show (indirectly) that it is the same object being
propagated back to the client. In this diagram I also included a return edge from the
next() method and labeled it nextCard to show that the returned object is the one
being supplied to the subsequent self-call (a method called on an object from within
a method already executing with this object as implicit parameter).
In terms of representing types, here the Deck object is represented using its con-
crete type, but the label deck:Iterable<Card> would have been a valid option as
well. For the Iterator object I used the interface supertype because in practice the
concrete type of this object is anonymous and does not really matter.
The distinction between models and complete source code applies to sequence
diagrams as well. First, a sequence diagram models a specific execution, not all exe-
cutions. In the above example, a different execution could have received false from
hasNext() and not called next(), or called next() twice, etc. These options are
not represented, because they are different scenarios. Second, sequence diagrams
will naturally omit some details of the execution of the code. We use sequence di-
agrams to show how objects interact to convey a specific idea. Although the UML
supports the specification of looping and conditional statements within a method,
6.4 The D ECORATOR Design Pattern 137
these are typically not included in UML sketches and I do not use this notation in
the book. Asynchronous calls (which are shown using a half arrow head), are also
not covered. Insignificant calls (e.g., to library methods) are typically omitted from
sequence diagrams in sketches.
In some cases we would like to optionally have objects of a given type to exhibit
special behavior, or have certain extra features. In the example of a CardSource,
we could imagine that in some cases we might want to print a description of each
card drawn on the console or in a file (a process called logging). As another exam-
ple, we might want to keep a reference to every card drawn from a certain source
(i.e., memorizing the drawn cards). One strategy for meeting this requirement is to
enhance the static structure of the design to accommodate the new features. In other
words, we can provide additional functionality by writing more classes that have
that functionality. Let us consider two possible design solutions for doing this.
Our first solution, which I will call the specialized class solution, will be to de-
sign one class for each type of feature we want to support. For example, to have
a CardSource that logs cards drawn, we could define our own special version of
Deck:
138 6 Composition
Similarly, to have a version of Deck that remembers the cards drawn, we could
create a new class MemorizingDeck that stores a reference to every drawn card in a
separate structure. Although a strategy that relies on the static structure could work
in simple cases, it has several drawbacks.
The main drawback of the specialized class idea is that it offers no flexibility for
toggling features on and off at run time. In other words, it is not easily possible to
turn a normal deck into a “memorizing” deck, or to start logging the cards drawn at
some arbitrary point in the execution of the code. In Java, it is impossible to change
the type of an object at run time, so the only option would be to initialize a new
object and copy the state of the old object into a new object which has the desired
features. Such a scheme is not very elegant. However, turning features on or off
might be necessary if the user interface allows the player to turn these features on
or off during game play.
We can consider a second solution that can accommodate run-time adjustments
in the features of an object. I will call this solution the multi-mode class solution.
With this solution, we provide all possible features within one class, and include
a flag value to represent the mode the object of the class is in. The resulting code
would look like this:
public class MultiModeDeck implements CardSource {
enum Mode {
SIMPLE, LOGGING, MEMORIZING, LOGGING_MEMORIZING
}
private Mode aMode = Mode.SIMPLE;
Although the multi-mode class solution does allow one to toggle features on and
off at run time, it contravenes the important principles presented in Chapter 4 by
inducing elaborate state spaces for objects that should otherwise be fairly simple. It
6.4 The D ECORATOR Design Pattern 139
also violates the principle of separation of concerns by tangling the behavior of dif-
ferent features within one class, or even a single method. In the extreme, it can turn
a class intended to represent a simple concept into a G OD C LASS†. As a consequence
of its complexity, the multi-mode class solution also suffers from a lack of extensi-
bility. To add a new feature, we need to add yet more code and branching behavior
to account for new modes. With, say, ten features, it is easy to imagine how the code
would become a nightmare of case switches and an instance of S WITCH S TATEMENT†.
As is often the case in the presence of a potential combinatorial explosion, the key
is to move from a solution that relies on defining new classes to a solution that relies
on combining objects together.
The D ECORATOR design pattern offers just that solution. The context for using the
pattern is a design problem where we want to decorate some objects with additional
features, while being able to treat the decorated objects like any other object of
the undecorated type. Figure 6.8 shows an application of the solution template of
D ECORATOR to the CardSource scenario. The diagram shows the roles played by
different elements as notes.
In terms of solution template, the D ECORATOR looks very much like the C OMPOS -
ITE,except that instead of a composite class we have some decorator classes. Indeed,
the design constraints of the decorator class are similar as those of the composite
class:
• A decorator aggregates one object of the component interface type (CardSource
in the example). Using the component interface type is important, as it allows the
decorator to decorate any other kind of components, including other decorators
(and composites).
• It implements the component interface. This is what allows decorator objects to
be treated by the rest of the code in exactly the same way as leaf elements.
The main question to resolve when applying the D ECORATOR is what the methods
of the decorator class should do. In a classic use of the D ECORATOR, the implemen-
tation of the interface’s methods that implement the decoration involves two steps,
illustrated in the code below:
140 6 Composition
One step is to delegate the execution of the original behavior to the element being
decorated. In our case, we call draw() on the original card source (the one being
decorated). The other step is to implement the “decoration”, which in our case is to
add the card to some internal structure. There is no prescribed order for these two
steps, although in some case the problem domain may impose an order. In our case,
it is necessary to draw a card before we can add it to the internal storage. Finally,
although only some methods may involve a behavioral decoration, it is necessary
to re-route all methods declared in the component interface to respect the subtyping
contract. In our case, this means that we have to implement a method isEmpty()
that simply returns whether the decorated element is empty.
With the D ECORATOR, we can easily combine decorations. Because a decora-
tor aggregates a component, combining features becomes as simple as decorat-
ing a decorated object. The sequence diagram of Figure 6.9 illustrates the del-
egation sequence when using a D ECORATOR where we decorated a Deck with a
MemorizingDecorator, and then again with a LoggingDecorator, so that the fi-
nal behavior of draw() will be to memorize, log, and return the next card in the card
source.
An important constraint when using the D ECORATOR is that for the design to work,
decorations must be independent and strictly additive. The main benefit of the D EC -
ORATOR is to support attaching features in a flexible way, sometimes in unanticipated
configurations. For this reason, use of the pattern should not require client code to
respect elaborate combination rules. As for being additive, this means that the D EC -
ORATOR pattern should not be used to remove features from objects. The main reason
for this constraint is that it would violate a fundamental principle of object-oriented
design introduced in Chapter 7.
When implementing the D ECORATOR design pattern in Java, it is a good idea to
specify as final the field that stores a reference to the decorated object, and to
6.4 The D ECORATOR Design Pattern 141
Although the D ECORATOR and C OMPOSITE patterns are distinct and often presented
separately, decorator and composite classes can easily co-exist in a type hierarchy.
If they implement the same component interface, they can work hand-in-hand in
supporting composition-based solutions to design problems. The class diagram of
Figure 6.11 shows a type hierarchy with one leaf, one composite, and two decora-
tors.
Fig. 6.11 Combining the C OMPOSITE and D ECORATOR in the same class hierarchy
The object diagram of Figure 6.12 shows a sample object graph that can be in-
duced by this type hierarchy. The diagram shows examples of both a decorated
composite and a composite of a decorated object.
Fig. 6.12 Object diagram showing a combination of composite and decorator objects
Figure with a draw() method. Leaf classes are concrete figures, such as rectangles,
ellipses, text boxes, etc. Figure 6.13 shows a class diagram of the domain elements
and corresponding design structures.
Fig. 6.13 Class diagram of the C OMPOSITE and D ECORATOR patterns applied to the context of a
drawing editor
In this design, the CompositeFigure very naturally supports the end-user feature
of grouping figures into an aggregate figure. A group can then be considered a single
figure element, which can then be grouped with other figures and groups, etc. As for
the D ECORATOR, it allows decorating figures, literally. The example provided on the
diagram is that of a decorator that adds a border to the decorated figure, whatever
it is. This classic application of the C OMPOSITE and D ECORATOR patterns is good to
know about, because they also provide the conceptually cleanest illustration of the
behavior that must be realized by their implementation of the component interface.
Specifically, the draw() method of the composite is simply an invocation of the
draw() method of all the figures it contains:
public void draw() {
for( Figure figure : aFigures ) {
figure.draw();
}
}
For the D ECORATOR, the implementation of the draw method would be a sequence
of one delegation followed by a decoration.
public void draw() {
aFigure.draw();
// Additional code to draw the border
}
144 6 Composition
We are now starting to work with designs that involve various combinations of ob-
jects in elaborate object graphs. The use of such dynamic structures has various
implications for other aspects of the design. One implication is for object identity
(see Section 6.4). Another implication is for designs that rely on object copying.
In Section 2.7, I discussed situations where it is useful to copy some objects,
and introduced copy constructors, which allow a client to make a copy of an object
passed as argument:
Deck deckCopy = new Deck(deck);
Copy constructors work fine in many situations, but their main limitation is that
a constructor call requires a static reference to a specific class (here, class Deck).
In designs that make use of polymorphism, this can turn out to be a problem. Let
us consider a situation where we are managing a list of CardSource objects. If we
want to make a deep copy of the list, we would have to make a copy of every card
source in the list:
List<CardSource> sources = ...;
List<CardSource> copy = new ArrayList<>();
for( CardSource source : sources ) {
copy.add(/* ??? */);
}
Solutions of this nature are not recommended because they essentially void the ben-
efits of polymorphism, namely, to be able to work with instances of CardSource
no matter what their actual concrete type is. Moreover, this code is also an example
of S WITCH S TATEMENT† which completely destroys the extensibility of the design,
as it would break as soon as a new subtype of CardSource is introduced. Finally,
it would be a mess to implement because some CardSource classes are wrappers
around other card sources. Specifically, because CompositeCardSource can aggre-
gate any kind of card source, a copy constructor for this class would also need a
branching statement like the above. In the presence of polymorphism, the use of
copy constructors is essentially unworkable.
6.6 Polymorphic Copying 145
/**
* @return An object that is an exact deep copy
* (distinct object graph) of this card source.
*/
CardSource copy();
}
The impact of this addition is that all concrete subtypes of CardSource are now
required to supply a copy() operation. Figure 6.14 presents some of the different
cases we have seen so far in previous sections, with their elements that are relevant
to copying. In this design, we will assume that the Deck class is implemented using
the class CardStack introduced in Section 6.1. In this scenario, CardStack also has
a copy constructor.
Fig. 6.14 Polymorphic copy requires all implementing classes of a type to supply a copy()
operation
In the case of a decorator, our copy consists of a new decorator of a copy of the
original decorated element. However, because decorators can decorate polymorphi-
cally any subtype of CardSource, we must copy the decorated element polymor-
phically. Fortunately, the support we need to do this is precisely the one we are
implementing throughout the CardSource type hierarchy: method copy(). The im-
plementation of copy() for MemorizingDecorator is very similar, except that we
also have to copy the additional state (aDrawnCards):
public MemorizingDecorator copy() {
MemorizingDecorator copy =
new MemorizingDecorator(aElement.copy());
copy.aDrawnCards = new ArrayList<>(aDrawnCards);
return copy;
}
problems are very similar to the ones discussed in the previous section (that the use
of a S WITCH S TATEMENT† structure destroys the benefits of polymorphism, etc.).
To create a card source without hard-coding its type, one option better than a
S WITCH S TATEMENT† would be to use metaprogramming (see Section 5.4). For ex-
ample, we could add a parameter to newGame() of type Class<T>, which specifies
the type of the card source to add. Although workable, solutions of this nature tend
to be fragile and require a lot of error handling.
Another option is to rely on a polymorphic copying mechanism and create new
instances of an object of interest by copying a prototype object. This idea is captured
as the P ROTOTYPE design pattern. The context for using the P ROTOTYPE is the need to
create objects whose type may not be known at compile time. The solution template
involves storing a reference to the prototype object and polymorphically copying
this object whenever new instances are required.
For the GameModel scenario, the application of the P ROTOTYPE would look like
this:
public class GameModel {
private final CardSource aCardSourcePrototype;
private CardSource aCardSource;
Fig. 6.15 Sample application of the P ROTOTYPE, with the name of roles indicated in notes
In this solution, we use dependency injection (see Section 3.8) to inject a card
source prototype object into the GameModel via its constructor. Then, whenever a
fresh CardSource object is required, we make a copy of the prototype and assign
the result to aCardSource. If need be, it would also be possible to add a setter
method to change the prototype at run time.
Figure 6.15 shows a class diagram that summarizes the key aspects of the solu-
tion template, and indicates the role various elements play in the application of the
6.8 The C OMMAND Design Pattern 149
pattern. The client is a generic mention to represent any code that needs to perform
polymorphic instantiation. The prototype is the abstract element (typically an inter-
face) whose concrete prototype must be instantiated at run time. The products are
the objects that can be created by copying the prototype.
One added benefit of the P ROTOTYPE pattern is that normally the option to create
objects of different types does not increase the amount of control flow (branching
statements) in the client class. In a traditional, “mode-based” design, the newGame()
method would have to check whether the object is in a specific state to create, say,
Deck card sources as opposed to other card sources, using a control statement such
as an if statement. With the P ROTOTYPE, this branching is done through polymor-
phism. As the code of the newGame() method shows, there is no such control state-
ment: the method just makes a copy of whatever object is the current prototype.
Now that we are studying designs that make principled use of objects, we con-
sider an alternative idea for representing commands, namely for objects to serve as
manageable units of functionality. In sophisticated applications, there are many dif-
ferent contexts in which we might want to exercise a functionality such as drawing
a card from the deck. For example, we might want to store a history of completed
commands, so that we can undo them or replay them later. Or, we might want to
accumulate commands and execute them all at once, in batch mode. Or, we might
150 6 Composition
want to parameterize other objects, such as graphical user interface menus, with
commands. Requirements such as these point to the additional need to manage func-
tionality in a principled way. The C OMMAND design pattern provides a recognizable
way to manage abstractions that represent commands.
The class diagram of Figure 6.17 shows a sample application of the pattern. The
Command interface defines an execute method and other methods to specify the ser-
vices required by the clients to manage the commands. In the example, this includes
an additional undo() method, but other designs may leave it out or have other re-
quired services (such as getDescription(), to get a description of the command).
The C OMMAND pattern has a simple solution template. The template involves
defining commands as objects, with an interface for commands that includes a
method to execute the command. Another important part of the solution template
6.8 The C OMMAND Design Pattern 151
is for the client to refer to commands through the interface. Despite the apparent
simplicity of the solution template, the C OMMAND pattern is not necessarily an easy
one to apply, because many important design choices induced by the pattern are
implementation-dependent. Let us look at some examples from our scenario.
• Access to command target: Command execution can modify the state of one of
more objects. For example, drawing a card from a deck changes the state of the
deck. The design must specify how the command gains access to the objects it
must act on. Typically this is done by storing a reference to the target within the
command object, but other alternatives are possible, including passing arguments
to the execute method or using closures;
• Data flow: In the typical solution template for C OMMAND, the interface methods
have return type void. The design must thus include a provision for returning the
result of commands that produce an output, such as drawing a card from a deck;
• Command execution correctness: The code responsible for executing com-
mands must ensure that the sequence of execution is correct. For example, the
design needs to specify whether commands can be executed more than once. The
use of design by contract also leads to interesting implications. If commands call
code with specified preconditions, the responsibility of respecting the precondi-
tions is transferred to the code executing the command.
• Encapsulation of target objects: In some cases, a command object might re-
quire operations that are not available in the target object’s public interface. For
example, to undo the effect of calling Deck.draw(), it is necessary to push a
card back onto the deck. In our running example class Deck does not have a
push method. The design must include a solution to this issue. One possibility
is to have a command factory method located in the class of the object the com-
mands operate on. In our case, this would mean to add a createDrawCommand()
method in class Deck.
• Storing data: Some operations supported by commands require storing some
data, something that also needs to be designed as part of the pattern’s application.
For example, in a design context where the undoing of commands is required, the
effect of executing a command may have to be cached so that it can be undone. In
our case, to undo the drawing of a card from a deck, it is necessary to remember
which card was drawn. This information could be stored in the command object
directly, or in an external structure accessible by the command object.
To illustrate one point in the design space for each of the concerns above, the
code below shows an example of how to support a command to draw cards from
a deck. The key idea for this application of the pattern is to use a factory method
to create commands that are instances of an anonymous class with access to fields
of its outer instance (see Section 4.10). In this design, commands to operate on a
Deck instance are obtained directly from the Deck instance of interest. To keep the
example simple, I slightly modify the Command interface so that execute() returns
an Optional<Card>, which allows some commands to return an instance of Card
if applicable. The code also assumes commands are executed only once and undone
in the inverse order of that in which they are executed.
152 6 Composition
does not refer to the state of a GameModel, field NULL_MOVE is a constant and
declared as static. The null move represents the situation where it is not pos-
sible to make a move in the game. This is an application of N ULL O BJECT (see
Section 4.5). Class CompositeMove realizes the role of the composite in the
C OMPOSITE pattern. In the game it is used to combine atomic moves, such as
taking a card from a tableau pile and flipping the card underneath it to re-
veal it. Finally, one implementation of Move is a stub, used for testing (see
Section 5.8).
When designing a piece of software using aggregation, one can often end up with
long delegation chains between objects. For example, Figure 6.18 models the ag-
gregation for card piles in the Solitaire application.
Fig. 6.19 Sample data structure access scenario for the Solitaire game design
This design violates the principle of information hiding by requiring the code of
the GameModel class to know about the precise navigation structure required to add
a card to the system. Although this might be obvious in the case of a CardStack
returning its List<Card>, exactly the same argument can be made for Foundations
returning one of its CardStack. However, the encapsulation quality of intermediate
classes in aggregation chains is easier to overlook. The intuition that designs such as
this one tend to be suboptimal is captured by the M ESSAGE C HAIN† antipattern. The
Law of Demeter is a design guideline intended to help avoid the consequences of
M ESSAGE C HAIN†. This “law” is actually a design guideline that states that the code
of a method should only access:
• The instance variables of its implicit parameter;
• The arguments passed to the method;
• Any new object created within the method;
• (If need be) globally available objects.
To respect this guideline, it becomes necessary to provide additional services in
classes that occupy an intermediate position in an aggregation/delegation chain so
that the clients do not need to manipulate the internal objects encapsulated by these
objects. The solution in our example would be illustrated by Figure 6.20.
In this solution, objects do not return references to their internal structure, but
instead provide the complete service required by the client at each step in the dele-
gation chain.
Insights 155
Fig. 6.20 Sample data structure access scenario for the Solitaire game design, which respects the
Law of Demeter
However, this would require the client to know about the interface of Tableau,
and violate the Law of Demeter.
Insights
This chapter presented various techniques for solving design problems by compos-
ing objects according to specific patterns.
• Large classes can be simplified by introducing classes whose objects will provide
services to the initial class;
156 Further Reading
• If a design problem requires structures that change at run time or can be com-
bined, consider building the structures by combining objects, as opposed to defin-
ing new classes for each possible structure;
• Use the C OMPOSITE when you need to manipulate collections of objects the same
way as single (leaf ) objects;
• Use the D ECORATOR when you need to add functionality to certain objects, while
being able to use them in place of regular objects;
• The C OMPOSITE and D ECORATOR can be combined easily, especially if they share
the same component type;
• Applying well-known object composition patterns is not sufficient to ensure the
code is correct: typically, client code remains responsible for ensuring that the
use of the pattern does not result in defective object graphs;
• Sequence diagrams can help communicate important arrangements of method
calls between objects in a design;
• Use polymorphic copying to make copies of objects whose concrete type is not
known at compile time. If the type is known at compile time, favor the simpler
technique of copy constructors;
• Polymorphic copying can also be used as a way to create fresh instances of ob-
jects whose type is not known at compile time, a technique captured by the P RO -
TOTYPE pattern;
• For designs where function objects need to be explicitly managed by client code,
for example to store them or share them between code locations, the C OMMAND
design pattern provides a recognizable solution template;
• When applying the C OMMAND pattern, be careful not to break the encapsulation
of classes simply to allow command objects to operate on target objects;
• Unless there is an explicit reason not to, respect the Law of Demeter and avoid
long message chains.
Further Reading
The Gang of Four book [6] has the original, detailed treatment of the C OMPOSITE,
D ECORATOR, P ROTOTYPE, and C OMMAND patterns. Their descriptions of the patterns
include useful complementary discussions of the implications of using the pattern.
For example, the presentation of the C OMPOSITE pattern includes an extended discus-
sion of the trade-off between transparency and safety (in the type-checking sense)
involved around the decision of whether to declare the child management operations
in the component interface or not.
Information on variable arguments (varargs) can be found on the Oracle website
in the list of enhancements for Java SE 5.0.
A web page with information on the Law of Demeter can be found at
http://www.ccs.neu.edu/home/lieber/LoD.html.
Chapter 7
Inheritance
Design Context
The examples in this chapter discuss the design of two type hierarchies: card sources
and moves. The card source hierarchy follows the examples of the previous chapters
where instances of objects that are subtypes of a CardSource interface are used to
provide card instances to be used in card games. The second context is the design of
a hierarchy of subtypes of an interface Move which, together, realize an application
of the C OMMAND design pattern as seen in Section 6.8.
So far we have seen many situations where we can leverage polymorphism to re-
alize various design features. Polymorphism helps make a design extensible by de-
coupling client code from the concrete implementation of a required functionality.
The class diagram of Figure 7.1 exemplifies this benefit by showing a GameModel
that depends only on a general CardSource service whose concrete realization can
be one of at least three options: a typical Deck of cards, a MemorizingDeck that
remembers each card drawn, and a CircularDeck that places drawn cards back at
the bottom of the deck.
This design is extensible because in principle the GameModel can work with any
card source. As discussed in Chapter 3, in Java polymorphism relies intrinsically
on the language’s subtyping mechanism. The key to supporting various options for
a CardSource is the fact that the different concrete implementations of the service
are subtypes of the CardSouce interface type.
Although the design illustrated is clean from the point of view of polymorphism,
it has one major weakness from the point of view of the implementation of the var-
ious card source alternatives. This weakness would become apparent as soon as we
would start to implement the class hierarchy of Figure 7.1. The issue is that the
services defined by the CardSource interface are similar, and likely to be imple-
mented in similar ways.1 Figure 7.2 shows a slightly different variant of the class
diagram that emphasizes the implementation of the concrete CardSources instead
of the polymorphism. As is now more evident from the diagram, all three imple-
mentations of CardSource hold a reference to a CardStack delegate. Moreover:
• In all cases, the implementation of method isEmpty() is a delegation to aCards-
.isEmpty()
• In all cases, the implementation of method draw() pops a card from aCards: the
only difference between the three options is small variants for the remainder of
1 This assumes a standard implementation, and not an application of the D ECORATOR, which
would be challenging in the case of CircularDeck because of the requirement to use a service
(adding cards to the source) that is not defined on the component interface.
7.1 The Case for Inheritance 159
the implementation of draw (e.g., to insert the card in the deck in the CardStack
of CircularDeck).
So here we can say that the design induces D UPLICATED C ODE†, also known as
code clones. There is an extensive literature on the topic of duplicated code, but the
bottom line is that it is a good idea to avoid it.
Problems of redundancies such as the one illustrated here can be improved by re-
organizing the design. One mechanism of object-oriented programming languages
that is especially effective for supporting code reuse (and thus avoiding D UPLICATED
C ODE†) is inheritance. Inheritance directly supports code reuse and extensibility be-
cause it allows us to define some classes in terms of other classes. The key idea
of inheritance is to define a new class (the subclass) in terms of how it adds to (or
extends) an existing base class (also called the superclass). Inheritance avoids re-
peating declarations of class members because the declarations of the base class will
automatically be taken into account when creating instances of the subclass.
In class diagrams, inheritance is denoted by a solid line with a white triangle
pointing from the subclass to the superclass. Figure 7.3 illustrates a variant of our
design where MemorizingDeck and CircularDeck are defined as subclasses of the
Deck base class.
In the code above, a new object of run-time type MemorizingDeck is created and
assigned to a variable named deck of compile-time type Deck. This is legal because
MemorizingDeck is a subtype of Deck. The second line of the code example shows
another relation between variables and values of different, yet related, types. The
code declares a variable of type CardSource and assigns the value deck to it. The
compile-time type of deck is Deck, which is a subtype of CardSource. For this
reason, the compiler allows the assignment. At run time, it will turn out that the
concrete type of deck is MemorizingDeck. However, because MemorizingDeck is
a subtype of both Deck and CardSource, there is no problem.
In this chapter, the distinction between compile-time type and run-time type will
become increasingly important. In our case, when an instance of MemorizingDeck
is assigned to a variable of type Deck, it does not become a simple deck or lose any
of its subclass-specific fields. In Java, once an object is created, its run-time type
remains unchanged. All the variable reassignments accomplish in the code above is
to change the type of the variable that holds a reference to the object. The run-time
type of an object is the most specific type of an object when it is instantiated. It is the
type mentioned in the new operation, and the one that is represented by the object
returned by method getClass() (see Section 5.4). The run-time type of an object
never changes for the duration of the object’s lifetime. In contrast, the compile-time
(or static) type of an object is the type of the variable in which a reference to the
object is stored at a particular point in the code. In a correct program the static
type of an object can correspond to its run-time type, or to any supertype of its
run-time type. The static type of an object can be different at different points in the
code, depending on the variables in which an object is stored. Let us consider the
following example:
7.2 Inheritance and Typing 161
At the first line of the main method an object is created that is of run-time
type MemorizingDeck and assigned to a variable of type Deck. As stated above,
the run-time type of this object remains MemorizingDeck throughout the execu-
tion of the code. However, at the following line the static type of the variable
that stores the original object is MemorizingDeck, and within the body of method
isMemorizingDeck it is Deck (a formal parameter is a kind of variable, so the type
of a parameter acts like a type of variable). Because the run-time type of the ob-
ject never changes, the value stored in both isMemorizing1 and isMemorizing2
is true.
Downcasting
To make the code above compile, it is necessary to use a cast operation (Memoriz-
ingDeck). In brief, a cast operation is necessary to enable unsafe type conversion
operations. An example of an unsafe conversion between primitive types is to con-
vert a value of type long into a value of type int (which may cause an overflow).
Similarly, because a reference to a Deck is not guaranteed to refer to an instance
of MemorizingDeck at run time, it is necessary to flag the risky conversion using a
cast operator, a process known as downcasting.2 When using inheritance, subclasses
typically provide services in addition to what is available in the base class. For ex-
ample, a class MemorizingDeck would probably include the definition of a service
to obtain the list of cards drawn from the deck:
public class MemorizingDeck extends Deck {
public Iterator<Card> getDrawnCards() { ... }
}
2 The direction implied in the term is a consequence of the convention that in type hierarchies, the
top of the hierarchy is usually considered to be the root of the hierarchy
162 7 Inheritance
This makes a lot of sense. The code above would be type unsafe. Because refer-
ences to an instance of any subtype of Deck can be stored in variable Deck, there
is no guarantee that, at run time, the object in the variable will actually define a
getDrawnCards() method. If, based on our knowledge of the code, we are sure
that the object will always be of type MemorizingDeck, we can downcast the vari-
able from a supertype down to a subtype:
MemorizingDeck memorizingDeck = (MemorizingDeck) deck;
Iterator<Card> drawnCards = memorizingDeck.getDrawnCards();
If the assumption is wrong, most likely due to a programmer error, then the
execution of the code cannot proceed, and the downcast will raise a ClassCast-
Exception. For this reason, downcasting code will often be protected by control
structures to assert the run-time type of an object, such as:
if( deck instanceof MemorizingDeck ) {
return ((MemorizingDeck)deck).getDrawnCards();
}
Java supports single inheritance, which means that a given class can only declare
to inherit from a single class. This is in contrast to languages such as C++, which
support multiple inheritance. However, because the superclass of a class can also
be defined to inherit from a superclass, classes can have, in effect, more than one
superclass. In fact, classes in Java are organized into a single-rooted class hierarchy.
If a class does not declare to extend any class, by default it extends the library
class Object. Class Object constitutes the root of any class hierarchy in Java code.
The complete class hierarchy for variants of Deck thus includes class Object, as
illustrated in Figure 7.4. Because the subtyping relation is transitive, objects of class
MemorizingDeck can be stored in variables of type Object.
With inheritance, the subclass inherits the declarations of the superclass. The con-
sequences of inheriting field declarations are quite different from those of method
declarations, so I discuss them separately.
7.3 Inheriting Fields 163
objects created with the statement new MemorizingDeck(); will have two fields:
aCards and aDrawnCards. It does not matter that the fields are private. Accessibility
is a static concept, meaning that it is only relevant for the source code. The fact that
the code in class MemorizingDeck cannot access (or see) the field declared in its
superclass does not change anything about the fact that this field is part of the object.
For the fields to be accessible to subclasses, it is possible to set their access modifier
to protected instead of private, or to access their value through a getter method.
Type members declared to be protected are only accessible within methods of the
same class, classes in the same package, and subclasses in any package. To respect
the principles of encapsulation presented in Chapter 2, the accessibility of fields
should however be minimized. This means that, unless widening a field’s visibility
to protected provides a clear advantage, a field should be declared private, even
if its value is required by subclasses. I revisit this point in Section 7.4.3
The inheritance of fields creates an interesting problem of data initialization.
When an object can be initialized with default values, the process is simple. In
3 The Java Language Specification (JLS) considers that private fields are not “inherited”. This is
a matter of terminology, because objects of subclasses do include the private fields declared in
their parent classes. When learning object-oriented design, mixing the concepts of visibility and
inheritance can be confusing, so I do not retain the terminology of the JLS. In this book, the
concepts of field inheritance and visibility are kept consistently distinct.
164 7 Inheritance
our case, if we assign the default values using the field initialization statement as
in the above statements (i.e., = new CardStack();), and rely on the default (i.e.,
parameterless) constructors, we can expect that creating a new instance of class
MemorizingDeck will result in an instance with two fields of type CardStack, each
referring to an empty instance of CardStack.4
However, it is often the case that object initialization requires input data. For
example, what happens if we want to make it possible to initialize a deck with a set
of cards supplied by the client code? For example:
Card[] cards = {Card.get(Rank.ACE, Suit.CLUBS),
Card.get(Rank.ACE, Suit.SPADES)};
MemorizingDeck deck = new MemorizingDeck(cards);
In such situations it becomes important to pay attention to the order in which the
fields of an object are initialized. The general principle in Java is that the fields of an
object are initialized top down, from the field declarations of the most general su-
perclass down to the most specific class (the one named in the new operation). In our
example, aCards would be initialized, then aDrawnCards. This order is achieved by
the fact that the first instruction of any constructor is to call a constructor, generally
of its superclass, and so on.5 For this reason, the order of constructor calls is bottom
up. In our running example, declaring:
public class MemorizingDeck extends Deck {
private final CardStack aDrawnCards = new CardStack();
means that the default constructor of Deck is called and terminates before the code
of the MemorizingDeck constructor executes. It is also possible to invoke the con-
structor of the superclass explicitly, using the super(...) call. However, if used,
this call must be the first statement of a constructor. Although it illustrates how
constructor calls are chained, the example above does not quite do what we want,
because it ignores the input cards. With the initialization mechanism we have seen
so far, however, it becomes possible to pass input values up to initialize fields de-
clared in a superclass. In our case we want to store the input cards into the aCards
field defined by the Deck superclass. We would accomplish this as follows:
4 If no constructor is declared for a class, a default constructor with no parameter is invisibly made
available to client code. Declaring any non-default constructor in a class disables the automatic
generation of a default constructor.
5 If the superclass declares a constructor with no parameter, this call does not need to be explicit.
It is also possible for the first instruction of a constructor to be a call to another constructor of the
same class, using the statement this(...). Eventually, however, construction has to execute the
constructor of the superclass.
7.3 Inheriting Fields 165
Fig. 7.5 Order of constructor call (a) and object construction (b). The calls to the constructors of a
superclass are self-calls
calls the constructor of Deck, which creates an additional Deck instance, different
from the instance under construction, immediately discards the reference to this
instance, and then completes the initialization of the object. This code would not
serve many useful purposes.
In the first case the function is invoked by specifying the target object before the
call: memorizingDeck.shuffle(). In this case we refer to the memorizingDeck
parameter as the implicit parameter. A reference to this parameter is accessible
7.4 Inheriting Methods 167
through the this keyword within the method.6 In the second case, the function
is invoked by specifying the target object as an explicit parameter, so, after the
call: shuffle(memorizingDeck). In this case to clear any ambiguity it is usu-
ally necessary to specify the type of the class where the method is located, so
Deck.shuffle(memorizingDeck). What this example illustrates is that methods
of a superclass are automatically applicable to instances of a subclass because in-
stances of a subclass can be assigned to a variable of any supertype. In our example,
because it is legal to assign a reference to a MemorizingDeck to a parameter of type
Deck, the shuffle() method is applicable to instances of any subclass of Deck.
In some cases, a method inherited from a superclass does not do quite what we
want. In our running example, this would be the case for method draw(), which in
the Deck base class just draws a card from the deck:
public class Deck implements CardSource {
private CardStack aCards = new CardStack();
Unfortunately, this code will not compile because the code of method draw()
in MemorizingDeck refers to private field aCards of class Deck. Because private
fields are only accessible within the class where they are declared, this field is
not visible in other classes, including subclasses. One possible workaround is to
define Deck.aCards as protected instead. A protected access modifier for a
field allows subclasses to manipulate some structure of the superclass when over-
riding methods. Unfortunately, increasing the visibility of aCards from private to
protected has a corresponding negative impact on encapsulation, because now it
is possible to refer to the field, and thus mutate the object it refers to, from many
different classes instead of just one. To circumvent this issue, we can resort to other
alternatives, including the use of super calls, introduced below.
aCards. Unfortunately this does not work precisely because of the dynamic binding
mechanism described above. Because the call to draw() within MemorizingDeck#-
draw() will be dispatched on the same object, the same method implementation will
be selected, endlessly. The result will be a stack overflow error, because the method
will recursively call itself without a termination condition.
What we really want, instead, is to refer specifically to Deck#draw() within
MemorizingDeck#draw(). In other words, we want to statically bind the method
call draw() to the implementation located in Deck. In Java, to refer to the specific
implementation of a method located in the superclass from within a subclass, we
use the keyword super followed by the method call.
public class MemorizingDeck extends Deck {
public Card draw() {
Card card = super.draw();
aDrawCards.push(card);
return card;
}
}
This mechanism is referred to as a super call. Its effect is to statically bind the
method call to the first overridden implementation of the method found by going
up the class hierarchy. The implementation does not need to be in the immediate
superclass, but there needs to be at least one inherited method that can be selected
in this way.
For a method to effectively override another one, it needs to have the same signature
as the one it overrides.7 This requirement for matching method signatures opens the
door to errors with mystifying consequences.
For example, let us say we want to override the equals and hashCode methods
for class Deck, as discussed in Section 4.7, and we proceed as follows:
public class Deck implements CardSource {
public boolean equals(Object) { ... }
public int hashcode() { ... }
}
With these definitions we would expect that instances of Deck could be stored in
collections such as a HashSet without problem, given that we are properly overrid-
ing hashCode() to ensure equal instances of Deck have the same hash code. Except
that we are not. Actually the name of the method declared in Deck is hashcode()
and not hashCode(). Although we expect Object#hashCode() to be overridden,
the hard-to-see, one-character difference in the name means that the method is, in
7 Technically, it could have a subsignature as defined in Section 8.4.2 of the Java Language Speci-
fication. However, this subtlety is outside the scope of this book, so for simplicity we can consider
that the match in terms of method names, parameter types, and declared exceptions, must be exact.
170 7 Inheritance
fact, not overridden. Unless we notice the name difference, the bugs this problem
would cause could be very hard to explain.
To avoid situations like these, where we expect a method to be overridden when
it is not, we can use Java’s @Override annotation (see Section 5.4 for a review
of annotation types). The goal of this annotation is to allow programmers to for-
mally state their intent to override a method. The compiler can then check this intent
against reality and warn of any mismatches. In practice, if a method annotated with
@Override does not actually override anything, a compilation error is raised. The
case for using @Override annotations is very compelling, and personally I use them
systematically.8
public MemorizingDeck() {
/* Version 1: Does nothing besides the initialization */
}
8 For conciseness, overriding annotations are, however, not included in the code examples in the
chapters.
7.6 Polymorphic Copying with Inheritance 171
Here the constructor of MemorizingDeck is invoked three times. In the first call,
the parameterless constructor is selected. In the second call, the constructor used
is Version 3, which might be intuitive in this example because MemorizingDeck
is both the run-time type of the argument object and the static type of the variable
holding a reference to it. However, it can appear surprising that for newDeck2, it is
Version 2 of the constructor that is used. That is because in this case the static type
of the argument passed to the constructor is Deck. Because Deck is a subtype of
CardSource but not a subtype of MemorizingDeck, the only applicable overload
is Version 2. If we change the type of deck from Deck to MemorizingDeck, then
Version 3 is the one that will be selected. Note that the types of variables newDeck1
and newDeck2 play no role whatsoever in the selection algorithm for overloaded
methods and constructors.
Although overloading provides a convenient way to organize related alterna-
tives of a given specification, the use of this mechanism can also lead to hard-to-
understand code. This is especially the case when the types of the parameters of
overloaded versions of a method or constructor are related within a type hierarchy,
as illustrated above. For this reason I recommend avoiding overloading methods ex-
cept for widely used idioms (such as constructor overloading or library methods that
support different primitive types). In many designs, the same properties can be ob-
tained without overloading (namely, by giving different names to the methods that
take different types of arguments).
In the presence of inheritance, the guideline to minimize the visibility of fields can
conflict with our ability to implement polymorphic copying (see Section 6.6). To
make an exact copy of an object, it is necessary to have detailed information about
172 7 Inheritance
the complete state of the object so as to be able to replicate it faithfully. For sake
of illustration, let us assume that we are implementing polymorphic copying for the
class hierarchy shown in Figure 7.6.
This code will not compile because aCards is a private field of class Deck, and thus
not visible within class MemorizingDeck. One option is to change the visibility of
the field to protected. However, widening the accessibility of a field in a superclass
7.6 Polymorphic Copying with Inheritance 173
is not a general solution, because in many design contexts we may be inheriting from
a class that we cannot change (for example, a library class). Second, by widening the
scope of a field in a superclass, we are weakening the encapsulation in the overall
design, just to support copying.
Java provides a mechanism, called cloning, to get around this limitation. This
cloning mechanism revolves around the overriding of the protected clone() method
of class Object. Unfortunately, the Java cloning mechanism suffers from a variety
of design flaws which render it “fragile, dangerous”, and complex to use [1]. For
this reason, it should only be used to support polymorphic copying with inheritance
when no better alternative is available. Because the Java cloning mechanism is de-
scribed at length in existing references (see Further Reading), I only summarize its
main underpinnings here.
To clone an object, it is necessary to override Object#clone() as a public
method, and make a super call to clone() from within the method. For example, to
support cloning for class Deck, we would write (within class Deck):
public Deck clone() {
// NOT Deck clone = new Deck();
Deck clone = (Deck) super.clone();
...
}
The statement super.clone() calls the clone() method in the superclass, which
here means method Object#clone(). This method is special: it uses metaprogram-
ming features (see Section 5.4) to make a field-by-field shallow copy of the current
object and returns the copy. This is unusual because, although the method is im-
plemented in the library class Object, it still returns a new instance of class Deck.
Whenever an object aggregates other mutable objects, the shallow copy per-
formed via Object#clone() will likely be insufficient. For example, in the code
above, the execution of the clone() method results in a shared reference to the
value of the field aCards, as illustrated in Figure 7.7. Because this outcome would
break encapsulation and most likely be incorrect, the clone() method must also
make a new copy of the CardStack, this time using a copy constructor:
174 7 Inheritance
In contrast, with inheritance, the cards in the deck are not stored in a separate
deck, but rather referred to from a field inherited from the superclass. In terms of
methods, shuffle(), isEmpty(), and draw() are also inherited from the super-
class, so they do not all need to be redefined to delegate the call, as in composition.
In our example we only need to override shuffle() and draw() to account for
the memorization. Method isEmpty() can be directly inherited and still do what
we want. In the code of the overridden methods, the delegation to another object is
replaced by a super call, which executes on the same object.
public class MemorizingDeck extends Deck {
private final CardStack aDrawCards = new CardStack();
This last implementation, however, illustrates how designing with inheritance can
be tricky. With the code above, attempting to create a new MemorizingDeck() will
throw a NullPointerException from within method shuffle(). How is this pos-
sible, given that the field is immediately initialized with a reference to a CardStack
object? The explanation has to do with the order of field initialization, as described
in Section 7.3. When the constructor of MemorizingDeck is called, the first instruc-
176 7 Inheritance
Fig. 7.9 Two implementations for MemorizingDeck: composition-based (top), and inheritance-
based (bottom)
Overall, the main difference between the composition- and inheritance-based so-
lutions is the number of Deck objects involved (see Figure 7.9). The composition-
based approach provides a solution that requires coordinating the work of two Deck
objects: a basic Deck object and a wrapper (or decorator) object MemorizingDeck.
Thus, as discussed in Section 6.4, the identity of the object that provides the full
MemorizingDeck set of features is different from that of the other object that
provides the basic card-handling services of the deck. In contrast, the use of a
MemorizingDeck subclass creates a single MemorizingDeck object that contains
all the required fields.
In many situations, it will be possible to realize a design solution using either
inheritance or composition. Which option to choose will ultimately depend on the
context. Composition-based reuse generally provides more run-time flexibility. This
option should therefore be favored in design contexts that require many possible
configurations, or the opportunity to change configurations at run time. At the same
time, composition-based solutions provide fewer options for detailed access to the
internal state of a well-encapsulated object. In contrast, inheritance-based reuse so-
lutions tend to be better in design contexts that require a lot of compile-time con-
9Because the declaration of the constructor is left out, this is not visible in the code. However, a
default (parameterless) constructor gets generated which calls the default constructor of Deck.
7.8 Abstract Classes 177
There are often situations where locating common class members into a single su-
perclass leads to a class declaration that it would not make sense to instantiate. As
a running example for this section and the next, I continue to develop the concept
of command objects as introduced in Section 6.8. Let us assume that for a card
game application we decide to apply the C OMMAND pattern and use the following
definition of the command interface.
public interface Move {
void perform();
void undo();
}
A move represents a possible action in the game. Calling perform() on any sub-
type of Move performs the move, and calling undo() undoes the move. The class
diagram of Figure 7.10 shows a hypothetical application of the C OMMAND pattern.
Following a common naming convention, classes that implement the interface in-
clude the name of the interface as a suffix (for example, DiscardMove represents
the move that discards a card from the deck).
Fig. 7.11 Abuse of inheritance: the members of the base class end up being completely redefined
instead of specialized
Although this could work, it is not good design. An important principle of inher-
itance is that a subclass should be a natural subtype of the base class that extends
the behavior of the base class. In our case, a DiscardMove is not really a special-
ized version of a CardMove, they are just two different moves. First, CardMove
may define non-interface methods that make no sense for users of its subclass
(e.g., getDestination() to get the destination when moving a card). Second, this
idea is risky, because DiscardMove and RevealTopMove automatically inherit the
perform() and undo() methods of class CardMove, which need to be overridden
to implement the actual move we want. If we forget to implement one (undo() for
example), then calling perform() will do one thing, and calling undo() will undo
something else! These types of bugs can be hard to catch. I return to the issue of de-
sign ideas that abuse inheritance in Section 7.11. To use inheritance properly, here
we need to create an entirely new base class, and have all actual commands inherit
it, as shows in Figure 7.12.
Now we avoid the hack of having subclasses that morph their superclass into
something entirely different. However, at the same time we have a problematic situa-
tion: what is a DefaultMove, really? What would the implementation of perform()
and undo() do? Here, even using some sort of default behavior seems questionable,
because that would bring us back to the idea of using a base class that is not con-
ceptually a base for anything. A key realization to move forward is that our new
base class represents a purely abstract concept that needs to be refined to gain con-
creteness. This design situation is directly supported by the abstract class feature
7.8 Abstract Classes 179
• The class can declare new abstract methods using the same abstract keyword,
this time placed in front of a method signature. In practice, this means adding
methods to the interface of the abstract class, and thereby forcing the subclasses
to implement these methods. The usage scenario for this is somewhat specialized,
and I will cover it in detail in Section 7.10. However, for now, we can just say that
abstract methods are typically called from within the class hierarchy: by methods
of the base class, by methods of the subclasses, or both.
Because abstract classes cannot be instantiated, their constructor can only be
called from within the constructors of subclasses. For this reason it makes sense to
declare the constructors of abstract classes protected. In our running example, the
constructor of AbstractMove would be called by the constructor of subclasses to
pass the required reference to the GameModel up into the base class:
public class CardMove extends AbstractMove {
public CardMove(GameModel pModel) {
super(pModel);
}
}
In Section 6.4, we saw how we can use the D ECORATOR pattern to add features, or
decorations, to an object at run time. The key idea of the D ECORATOR is to define
these decorations using wrapper classes and composition as opposed to subclasses.
Figure 7.13 reproduces Figure 6.8, which shows a class diagram of the sample ap-
plication of D ECORATOR to the CardSource design context.
When a design involves multiple decorator types, as in this example, each deco-
rator class will need to aggregate an object to be decorated. This introduces the kind
of redundancy that inheritance was designed to avoid. Thus, we can use inheritance
to pull up the aElement field into an abstract decorator base class, and define con-
crete decorator subclasses that then only need to deal with the specific decoration.
This solution, shown in Figure 7.14, is a good illustration of a design that combines
ideas of composition (as seen in Chapter 6) and inheritance. Specifically, a deco-
rator object is of a subtype that inherits the aElement field, which is then used to
aggregate the instance of CardSource that is being decorated.
Fig. 7.14 Class diagram of a sample application of D ECORATOR that uses inheritance
With this design, the AbstractDecorator includes default delegation to the dec-
orated element.
182 7 Inheritance
It is worth noting that the aElement field is private. This means that concrete
decorator classes will not have access to it. This level of encapsulation is workable
because normally in the D ECORATOR, decorated elements are only accessed through
the methods of the component interface. In this case, subclasses can simply use the
implementation of the interface methods they inherit from AbstractDecorator to
interact with the decorated object. As an example, the following is a basic imple-
mentation of a LoggingDecorator that outputs a description of the cards drawn to
the console.
public class LoggingDecorator extends AbstractDecorator {
public LoggingDecorator(CardSource pElement) {
super(pElement);
}
One potential situation we may face with inheritance is when some common algo-
rithm applies to objects of a certain base type, but a part of the algorithm varies
from subclass to subclass. To illustrate this situation, let us go back to the design
context of creating and managing moves in the Solitaire application, as discussed
above in Section 7.8 and illustrated in Figure 7.12 (with DefaultMove renamed to
7.10 The T EMPLATE M ETHOD Design Pattern 183
In this code, the first statement of method perform() pushes the current move ob-
ject onto a command stack located in the game model. The block comment corre-
sponds to the actual implementation of the move, which would vary from move to
move. The final statement implements some logging of the move, for example by
printing the name of the command class to the console. Let us assume the same
approach is used for undo(), with moves being popped instead of pushed. Be-
cause parts of the code are in common, it will benefit from being pulled up to the
AbstractMove superclass for two main reasons:
• So that it can be reused by all concrete Move subclasses, thereby avoiding D UPLI -
CATED C ODE†;
• So that the design is robust to errors caused by inconsistently re-implementing
common behavior. Specifically, we want to prevent the possibility that a devel-
oper could later declare a new concrete subclass of Move and supply it with an
implementation of method perform() that does not do steps 1 and 3, for exam-
ple.
Because the implementation of perform() needs information from subclasses to
actually perform the move, it cannot be completely implemented in the superclass.11
The solution to this problem is to put all the common code in the superclass, and
to define some hooks to allow subclasses to provide specialized functionality where
needed. This technique is called the T EMPLATE M ETHOD design pattern. The name
relates to the fact that the common method in the superclass is a template, that gets
instantiated differently for each subclass. The steps in the algorithm are defined
10 This is not necessarily the best idea from a separation of concerns standpoint, but I use this
example for simplicity.
11 Although, technically, it would be possible to have a S WITCH STATEMENT † in perform()
that checks the concrete type of the object using instanceof or getClass() and executes
the appropriate code for all commands, this would introduce a dependency cycle between the base
class and its subclasses, and destroy the benefits of polymorphism. A bad idea of epic proportions.
184 7 Inheritance
as non-private12 methods in the superclass. The code below further illustrates the
solution template for the T EMPLATE M ETHOD:
public abstract class AbstractMove implements Move {
protected final GameModel aModel;
In Java, declaring a method to be final means that the method cannot be overridden
by subclasses. The main purpose for declaring a method to be final is to clarify
our intent that a method is not meant to be overridden. One important reason for
preventing overriding is to ensure that a given constraint is respected. Final methods
are exactly what is needed for the T EMPLATE M ETHOD, because we want to ensure that
the template is respected for all subclasses. By declaring the perform() method to
be final, subclasses cannot override it with an implementation that would omit the
call to pushMove or log().
The use of the final keyword with methods has an effect that is different from
the use of the same keyword with fields and local variables (see Section 4.6). The
use of final with fields limits how we can assign values to variables, and does not
involve inheritance, dynamic dispatch, or overriding.
The final keyword can also be used with classes. In this case, the behavior is
consistent with the meaning it has for methods: classes declared to be final can-
not be inherited. Inheritance in effect broadens the interface of a class by allowing
12 The step methods can have default, public, or protected visibility depending on the design con-
text. However, they cannot be private because private methods cannot be overridden. This con-
straint makes sense because private methods are technically not visible outside of their class, and
overriding requires method signature matching across classes.
7.10 The T EMPLATE M ETHOD Design Pattern 185
Abstract Methods
The declaration of class AbstractMove, above, illustrates the key ideas of the so-
lution for T EMPLATE M ETHOD. The following points are also important to remember
about the use of the pattern:
• The method with the common algorithm in the abstract superclass is the template
method; it calls the concrete and abstract step methods;
• If, in a given context, it is important that the algorithm embodied by the template
method be fixed, it could be a good idea to declare the template method final,
so it cannot be overridden (and thus changed) in subclasses;
• It is important that the abstract step method has a different signature from the
template method for this design to work. Otherwise, the template method would
recursively call itself, quite possibly leading to a stack overflow; following the
advice of Section 7.5 about avoiding unnecessary overloading, I would recom-
mend actually using a different name in all cases.
• The most likely access modifier for the abstract step methods is protected,
because in general there will likely not be any reason for client code to call indi-
vidual steps that are intended to be internal parts of a complete algorithm. Client
code would normally be calling the template method;
• The steps that need to be customized by subclasses do not necessarily need to be
abstract. In some cases, it will make sense to have a reasonable default behavior
that could be implemented in the superclass. In this case it might not be necessary
to make the superclass abstract. In our example, there is a default implementation
of log() that can be overridden by subclasses. In a different context, it might
make more sense to declare this method abstract as well.
When first learning to use inheritance, the calling protocol between code in the
super- and subclasses can be confusing because, although it is distributed over mul-
tiple classes, the method calls are actually dispatched to the same target object. The
sequence diagram in Figure 7.15 illustrates a call to perform() on a DiscardMove
instance. As can be seen, although it is implemented in subclasses, the call to the
abstract step method is a self-call.
Inheritance is both a code reuse and an extensibility mechanism. This means that a
subclass inherits the declarations of its superclass, but also becomes a subtype of its
superclass (and its superclass’s superclass, and so on). To avoid major design flaws,
inheritance should only be used for extending the behavior of a superclass. As such,
it is bad design to use inheritance to restrict the behavior of the superclass, or to use
inheritance when the subclass is not a proper subtype of the superclass.
7.11 Proper Use of Inheritance 187
As an example of a design idea to limit what a superclass can do using our running
example of a deck of cards, let us say that in some design context we need to have
decks of cards that cannot be shuffled. Given that we already have a class (Deck)
that defines everything we need to instantiate a deck, can we simply subclass Deck
to “deactivate” the shuffling? We could try this in different ways. For example:
public class UnshufflableDeck extends Deck {
public void shuffle() {
/* Do nothing */
}
}
or,
public class UnshufflableDeck extends Deck {
public void shuffle() {
throw new OperationNotSupportedException();
}
}
Actually, both versions are a bad design decision because they conflict directly
with the use of polymorphism, which supports calling operations on an object inde-
pendently of the concrete type of the object. Let us consider the following hypothet-
ical calling context:
private Optional<Card> shuffleAndDraw(Deck pDeck) {
pDeck.shuffle();
if( !pDeck.isEmpty() ) {
return Optional.of(pDeck.draw());
}
else {
return Optional.empty();
}
}
188 7 Inheritance
This code will compile and, given the interface documentation of Deck, should
do exactly what we want. However if, when the code executes, the run-time type
of the instance passed into shuffleAndDraw happens to be an UnshufflableDeck,
the code will either not work as expected (first variant, the deck silently does not get
shuffled), or raise an exception (second variant). There is clearly something amiss
here.
The intuition that inheritance should only be used for extension is captured by
the Liskov Substitution Principle (LSP). The LSP essentially states that subclasses
should not restrict what clients of the superclass can do with an instance. Specifi-
cally, this means that methods of the subclass:
• Cannot have stricter preconditions;
• Cannot have less strict postconditions;
• Cannot take more specific types as parameters;
• Cannot make the method less accessible (e.g., from public to protected);
• Cannot throw more checked exceptions;
• Cannot have a less specific return type.
This list seems like a lot of things to remember when designing object-oriented
software, but the whole point of the principle is that once we have assimilated its
logic, we no longer need to remember specific elements in the list. In addition, all
situations except for the first two points are prevented by the compiler. Nevertheless,
at first some of these points can seem counter-intuitive, so let us consider concrete
scenarios.
Remembering our interface CardSource, which has method isEmpty() and a
method draw() with the precondition !isEmpty(), we can design a new subclass
of Deck that draws the highest of the two top cards on the deck, and replaces the
lowest one back on top of the deck.
public class Deck implements CardSource {
protected final CardStack aCards = new CardStack();
This code looks relatively simple, which can be a symptom of good design. There
is a catch, however: for this solution to work, the deck needs to have at least two
7.11 Proper Use of Inheritance 189
cards in it. How can we deal with this? One solution is to rework the code of the
draw override to handle both cases:
public Card draw() {
Card card1 = aCards.pop();
if( isEmpty() ) {
return card1;
}
Card card2 = aCards.pop();
...
}
However, this code is not as elegant as the first version, it is more onerous to test,
etc. Given that we know about design by contract (see Section 2.9), why not simply
declare the precondition that, to call draw() on an instance of DrawBestDeck, there
needs to be at least two cards in the deck?
public class DrawBestDeck extends Deck {
public int size() {
return aCards.size();
}
This makes the precondition stricter (i.e., less tolerant), and thus violates the LSP.
This is because the mere presence of the subclass makes code like this unsafe for
cases where a deck has only one card.
if( deck.size() >=1 ) {
return deck.draw();
}
Actually, in our specific example, the idea is bad for an additional, if less funda-
mental, reason. Because the inteface CardSource does not have a size() method,
it is not possible to check the precondition polymorphically. To check that a call to
draw() respects all preconditions, it would thus be necessary to do:
Deck deck = ...
Optional<Card> result = Optional.empty();
if( deck instanceof DrawBestDeck &&
((DrawBestDeck)deck).size() >= 2 ||
!deck.isEmpty() ) {
result = Optional.of(deck.draw());
}
types. Let us say we add a method init(Deck) to the interface of Deck that re-
initializes the target instance to contain exactly the cards in the argument. If we have
a MemorizingDeck in our class hierarchy, we might be tempted to override init in
MemorizingDeck to initialize both the cards and the drawn cards, as illustrated in
Figure 7.16.
Although this code will compile, it will actually create an overloaded version of
init as opposed to an overridden version, as perhaps expected. This is extremely
confusing. The systematic use of the @Override annotation (see Section 7.4) would
help flag this as a problem, but otherwise, the code would lead to very mysterious
executions, given that the result of both calls to init would be different in the code
below:
MemorizingDeck deck = new MemorizingDeck();
MemorizingDeck memorizingDeck = new MemorizingDeck();
Deck mDeck = memorizingDeck;
deck.init(memorizingDeck); // Calls MemorizingDeck.init
deck.init(mDeck); // Calls Deck.init
The reason for this seemingly strange behavior is again to ensure the design
respects the LSP. If clients of the base class Deck can call init and pass in instances
of any subtype of Deck, then it would be limiting what they can do to require the
clients to only pass certain subtypes to init (like MemorizingDeck).
The case of return types inverses the logic: if a method returns an object of a
certain type, it should be possible to assign it to a variable of that type. If subclasses
can redefine methods to return more general types, then it would no longer be pos-
sible to complete the assignment. For example, let us say that in a (bad) design, a
developer adds a version of Deck that contains a joker card, and that objects that
represent jokers are not subtypes of Card, but merely subtypes of the Object root
type. Then calls like this:
Card card = deck.draw();
would become problematic if some versions of method draw() can return objects
that are not subtypes of Card.
The classic example of a violation of the LSP is the so-called Circle–Ellipse prob-
lem, wherein a class to represent a circle is defined by inheriting from an Ellipse
class and preventing clients from creating any ellipse instance that does not have
equal proportions. This violates the LSP because clients that use an Ellipse base
7.11 Proper Use of Inheritance 191
class can set the height to be different from the width, and introducing a Circle
subclass would eliminate this possibility:
Ellipse ellipse = getEllipse();
// Not possible if ellipse is an instance of Circle
ellipse.setWidthAndHeight(100, 200);
How to avoid the Circle–Ellipse problem in practice will, as usual, depend on the
context. In some cases, it may not be necessary to have a type Circle in the first
place. For example, in a drawing editor, user interface features could be responsible
for assisting users in creating ellipses that happen to be circles, while still storing
these as instances of Ellipse internally. In cases where a type Circle can be useful,
it might make sense to have different Circle and Ellipse classes that are siblings
in the type hierarchy, etc.
Let us start our study of the NodeViewer hierarchy at the top, with a look
at AbstractNodeViewer. One thing to note already is that the class does not
provide an implementation for the interface methods draw or getBounds().
This is consistent with the concept of abstract classes. Here it would be mean-
ingless to draw something that is not concrete, so we leave it out.
The implementation of method contains(), however, involves an inter-
esting quirk: it calls the object’s own getBounds() method, for which no
implementation is provided in the AbstractNodeViewer class. This is an ex-
ample where we can provide a complete implementation for one service (to
see if a point is contained in a node) by relying on the existence of another
service (getting the bounds for a node) whose implementation is delegated to
the subclasses.
The design of classes TypeNodeViewer and InterfaceNodeViewer illus-
trates the extent to which inheritance supports code reuse. The viewer classes
are used to depict class and interface nodes in a class diagram. The code is not
simple as it must handle different geometries depending on whether the nodes
have attributes or methods. At the same time, the only visual difference be-
tween types and interface nodes is that interface nodes include the interface
UML stereotype in their name. To support code reuse, TypeNodeViewer de-
fines a protected placeholder method getNameText to get the name of the
node, with a default implementation to use the node’s name as text. The sub-
class InterfaceNodeViewer then overrides this method to add the interface
stereotype to the text, and inherits the complete viewing machinery from its
superclass.
Insights
This chapter introduced inheritance as a mechanism to support code reuse and ex-
tensibility.
• Use inheritance to factor out implementation that is common among subtypes of
a given root type and avoid D UPLICATED C ODE†;
• UML class diagrams can describe inheritance-related design decisions effec-
tively;
• To the extent possible, use the services provided by a subclass through polymor-
phism, to avoid the error-prone practice of downcasting;
• Even in the presence of inheritance, consider keeping your field declarations pri-
vate to the extent possible, as this ensures tighter encapsulation;
• Subclasses should be designed to complement, or specialize, the functionality
provided by a base class, as opposed to redefining completely different behavior;
• Use the @Override annotation to avoid hard-to-find errors when defining over-
riding relations between methods;
Further Reading 193
• Because it can easily lead to code that is difficult to understand, keep overloading
to a minimum. Overloading is best avoided altogether when the parameter types
of the different versions of a method are in a subtyping relation with each other;
• Inheritance- and composition-based approaches are often viable alternative when
looking for a design solution. When exploring inheritance-based solutions, con-
sider whether composition might not be better;
• You can use Java’s cloning mechanism to implement polymorphic copying when
the fields of the superclass are not accessible. However, cloning is a complex and
error-prone mechanism that must be used very carefully;
• Ensure that subclasses that extend a base class can also be considered meaningful
subtypes of the base class, namely that instances of the subclass are in a “is-a”
relation with the base class;
• Ensure that any inheritance-based design respects the Liskov Substitution Princi-
ple. In particular, do not use inheritance to restrict the features of the base class;
• If some of the fields and methods that can be isolated through inheritance do not
add up to a data structure that it makes sense to instantiate, encapsulate them in
an abstract class;
• Remember that abstract classes can define abstract methods, and that methods of
the abstract class can call its own abstract methods. This way you can use abstract
classes to define abstract implementations of algorithms;
• Consider using the T EMPLATE M ETHOD pattern in cases where an algorithm applies
to all subclasses of a certain base class, except for some steps of the algorithm
that must vary from subclass to subclass;
• If there is no scenario for overriding a method, consider declaring it final. Sim-
ilarly, if there is no specific reason for a class to be extensible using inheritance,
consider declaring it final.
Further Reading
The Java Tutorial [10] has a section on interfaces and inheritance that provides com-
plementary material on inheritance, with a focus on the programming language as-
pects.
In terms of design guidelines, Chapter 4 of Effective Java [1], titled Classes and
Interfaces provides many items of guidance relevant to this chapter. Examples in-
clude Item 15, Minimize the accessibility of classes and members, Item 18, Favor
composition over inheritance and Item 19 Design and document for inheritance or
else prohibit it. Additional items relevant to this chapter include Item 13, Override
clone judiciously and Item 52, Use overloading judiciously.
Chapter 8
Inversion of Control
Inversion of control involves reversing the usual flow of control from caller code
to called code to achieve separation of concerns and loose coupling. It allows us to
build sophisticated applications while keeping the overall design complexity down
to a manageable level. One of the main realizations of the principle takes the form
of the O BSERVER pattern. This pattern is pervasive in software development, and it
is realized by most graphical user interface toolkits on most software development
platforms, from desktop to web to mobile applications.
Design Context
One concrete situation that motivates inversion of control in software design is when
a number of stateful objects need to be kept consistent. An example from the pro-
gramming domain itself is an integrated development environment such as Eclipse
or IntelliJ IDEA, which presents different views of the source code. In Eclipse, for
example, the Package Explorer and Outline views shows the structure of a class that
can also be viewed in the source code editor (see Figure 8.1). If a user changes the
class declaration, for example by adding a field in the source code editor, this change
is immediately reflected in all the different views. Likewise, if the user reorders the
method declarations in the Outline view, the new order of method declarations is
reflected in the source code editor. Hence, we could say that the problem we are try-
ing to solve is one of synchronization,1 where we are trying to keep different objects
consistent with each other.
To isolate the issue of view synchronization, I distilled the design problem into
a toy application called LuckyNumber. The application allows a user to select a
number (presumed to be lucky) between 1 and 10 inclusively. The interesting part
of the application, however, is that users can select their lucky number in different
ways, for example by entering the digit(s) that represent the number, typing out the
name of the number in English, or selecting it from a slider (see Figure 8.2).
In the application, each horizontal panel allows the user to view the number in a
specific way, but also to change the number. If the number is changed in one panel,
the change is immediately reflected in all other panels. In addition to its current
features, one requirement for this application is that it can be extended to accommo-
date any additional type of view. For example, old-fashioned users may request the
option to select their number using Roman numerals, geeky users may want to use
binary notation, etc.
1The term synchronization is also used in the context of concurrent programming, a topic that is
outside the scope of this book.
8.1 Motivating Inversion of Control 197
This design suffers from at least the following two related limitations:
• High coupling: Each panel explicitly depends on many other panels. Panels
could be of different types and require different types of interactions. For ex-
ample, to update the number it may be necessary to call setDigit on one panel
and setSliderValue on a different panel.
• Low Extensibility: To add or remove a panel, it is necessary to modify all other
panels. For example, to remove the slider panel, it would be necessary to modify
all other panels to remove the statements that update the slider panel. Similarly,
to add a Roman numeral panel, it would be necessary to change every panel to
add some statements to manage the new panel, etc.
What is even worse, is that the impact of these issues increases quadratically with
the number of panels, given that there are n · (n − 1) directed edges in a complete
graph with n vertices. In the initial application with three panels, we need six call
dependencies to keep all panels synchronized. This may not seem like much, but
if we throw in a Roman numeral panel and a binary notation panel, for a total of
five, then we need 20 dependencies scattered over five components, just to keep a
single number consistent. This is poor separation of concerns, because a significant
198 8 Inversion of Control
amount of code will be required to manage the dependencies that is likely to end
up tangled with code that more directly supports the required logic (e.g., adjusting
a slider). The code will also be less understandable, harder to test, etc.
One way out of using complete pairwise dependencies to synchronize multiple rep-
resentations of the same data is to separate abstractions responsible for storing
data from abstractions responsible for viewing data, from abstractions responsible
for changing data. This key insight is generally known as Model–View–Controller
(MVC) from the name of the three abstractions. The Model is the abstraction that
keeps the unique copy of the data of interest. In our simple context, that would be
the lucky number. The View is, not surprisingly, the abstraction that represents one
view of the data. Generally in a MVC decomposition there can be more than one
view of the same model. This is illustrated in the LuckyNumber application by the
presence of different views for something as simple as a single integer. Finally, the
Controller is the abstraction of the functionality necessary to change the data stored
in the model.
The origin of the MVC is somewhat obscure. The idea can be traced back to the
late 1970s and Xerox PARC researchers working on Smalltalk software, but there is
little besides a few memos in terms of written reports on the original development of
the concept. Currently, the term MVC is used fairly loosely. Some software devel-
opers refer to it as a design pattern. Others refer to it as something slightly different
called an architectural pattern or architectural style (somewhat like a design pat-
tern, but at a higher level of design abstraction). Some refer to it simply as a general
concept. Finally, some web technology platforms use the terms model, view, and
controller to refer to specific software components. Because I see the main benefit
of the Model–View–Controller as a guideline to separate concerns, I like to think of
it simply as a decomposition (of concerns). In this sense, it is more general than a
design pattern, because it does not include a solution template that is specific enough
to apply directly.
The lack of a well-defined solution template for the MVC means that there is
little guidance on how to realize the idea in practice. This also means that there
are innumerable ways to go about separating the model, view, and controller in a
design context. For example, the model could be a single object, or a collection of
objects. The view and controller could be different objects, or fused together. In the
latter case, the separation of concerns would be organized along the interfaces of
objects rather than the objects themselves (see Section 3.9 on the idea of interface
segregation).
Such a vague concept as the MVC is not easy to grasp in itself when learning
software design. Fortunately, there exists a related idea that is much more concrete,
namely the O BSERVER pattern.
8.3 The O BSERVER Design Pattern 199
The central idea of the O BSERVER pattern is to store data of interest in a specialized
object, and to allow other objects to observe this data. The object that stores the
data of interest is called, alternatively, the subject, model, or observable, and it cor-
responds to the Model abstraction in the Model–View–Controller decomposition.
For this reason, the context for the O BSERVER pattern corresponds to the motivation
discussed in Section 8.1: we want a simple way to manage multiple objects that
must be aware of state changes in the same data. The class diagram of Figure 8.4
illustrates how this is realized for the LuckyNumber application.
In this situation, the object in charge of keeping the data is an instance of Model,
which keeps track of a single integer and allows clients to query and change this
integer.
Where things become interesting is that the Model class also includes an aggregation
to an Observer interface, with methods to add and remove Observer instances
from its collection. This process is called registering and deregistering observers.
The mechanism for managing observers can be trivially implemented, for example:
public class Model {
private int aNumber = 5;
private List<Observer> aObservers = new ArrayList<>();
Classes that define objects that need to observe the model must then declare to
implement the Observer interface:
class IntegerPanel implements Observer { ... }
Through polymorphism, we thus achieve loose coupling between the model and
its observers. Specifically:
• The model can be used without any observer;
• The model is aware that it can be observed, but its implementation does not
depend on any concrete observer class;
• It is possible to register and deregister observers at run time.
A first key question about the relation between a model an its observers is, how do
the observers learn that there is a change in the state of the model that they need
to know about? The answer is that whenever there is a change in the model’s state
worth reporting to observers, the model should let the observers know by cycling
through the list of observers and calling a certain method on them. This method has
to be defined on the Observer interface and is usually called a callback (method)
because of the inversion of control that it implies. We talk of inversion of control
because, to find out information from the model, the observers do not call a method
on the model, they instead wait for the model to call them (back). This procedure is
often referred to as the Hollywood Principle (“don’t call us, we’ll call you”). That is
also why the method that is called by the model on the observer is called a callback.
Continuing with the movie industry metaphor, the name of the method to call back
is like the phone number of the prospective actor. If the casting director determines
that the actor should be auditioned, they will call the number. Likewise, if the model
determines that the observers should be notified, it will call their callback method.
In the case of the LuckyNumber application, an appropriate name for the call-
back method would be newNumber, given that this is the method that will be called
whenever the model needs to inform its observers that it has changed the number it
is storing. We thus define this method in the Observer interface:
public interface Observer {
void newNumber(int pNumber);
}
When first learning about callbacks, their logic can be a bit puzzling, especially
if the name of the callback is ambiguous. In the case above, it may look like the
method is intended to set a new number on an observer, because it would be called
like this:
someObserver.newNumber(5);
However, the method name should not be mentally read as “set number to this new
value”, but rather as “the model has a new number, here it is”. In other words, a
callback is not to tell observers what to do, but rather to inform observers about
8.3 The O BSERVER Design Pattern 201
some change in the model, and let them deal with it as they see fit (through the
logic provided in the callback). The analogy with the movie studio still works. If
an actor (the observer) gets an audition, the studio might call them and say “you
have an audition”, but not specify the details of how to react to this information (for
example, by preparing, arranging transportation, etc.). The lesson here is that to help
others understand a design, it is a good practice to name callback methods with a
name that describes a state-change situation, as opposed to a command. In our case,
other explicit names for the callback method would include numberChanged and
newNumberAvailable.
Once we have a callback defined, within class Model, we can create a helper
method, called a notification method2 that will notify all observers and provide them
with the number they should know about:
public class Model {
private void notifyObservers() {
for(Observer observer : aObservers) {
observer.newNumber(aNumber);
}
}
}
To ensure that the model dutifully notifies observers whenever a state change
occurs, two strategies are possible:
• A call to the notification method must be inserted in every state-changing
method; in this case the method can be declared private;
• Clear documentation has to be provided to direct users of the model class to call
the notification method whenever the model should inform observers. In this case
the notification methods needs to be non-private.
As usual, which strategy is better depends on the context. In cases where notifi-
cations can be issued for every model change, the first method provides a simpler
life cycle for the state of the model. However, in certain cases, notifying observers
with every state change may lead to some performance problems. For example, if
we had a model that could be initialized with a large collection of data items by
adding each item one at a time, notifying observers after each individual addition
may dramatically degrade the performance while providing no benefit. In situations
such as this one, it may be better to change the model silently (without notifying the
observers), and then trigger a notification once the batch operation is done. In cases
where such flexibility is needed, the second strategy can provide it.
The sequence diagram of Figure 8.5 illustrates what happens when we change
the number on the LuckyNumber application, using the first strategy.
Inside the state-changing method setNumber(int), we added a call to notify-
Observers to loop through each observer and call the method newNumber on each.
The implementation of the newNumber callback dictates how each observer reacts
to the change in state. In the case of the LuckyNumber application, each observer
2In Java the notification method cannot be called simply notify(), because a legacy method
with this name is already defined in class Object.
202 8 Inversion of Control
deals with the callback in a different way. For example, the IntegerPanel sets
the number of the integer in a text field; the TextPanel looks up the name of the
integer in an array, and sets the value of the text field to that string; the SliderPanel
positions the slider to correspond to the value, etc.
The second key question about the relation between a model and its observers is,
how do the observers access the information that they need to know about from the
model? Two main strategies are available. The first strategy is to make the infor-
mation of interest available through one or more parameters of the callback. This
strategy is also known as the push data-flow strategy because the model is explicitly
pushing data of a pre-determined structure to the observers.
Applying this strategy to our context, we could define the callback method to
include a parameter that represents the number most recently stored in the model.
This is the strategy that I illustrated above with the newNumber(int) callback.
public interface Observer {
void newNumber(int pNumber);
}
...
public void newNumber(int pNumber) {
aText.setText(Integer.toString(pNumber));
}
}
This strategy makes one major assumption: that we know in advance what type of
data from the model the observers will require. In our case, this strategy is a good fit
because there is nothing but a single integer that observers could require. However,
this is not the general case. For example, we could enhance the model to remember
each lucky number ever selected, and the timestamp of its selection. Observers now
have more data to choose from. Given the context, we could still assume that the
most common case for an observer will be to show the most recent number, but more
sophisticated observers might want to show the last three numbers, for example, or
the amount of time a certain number remained selected.
As another example, we now assume that we want to make the Deck class dis-
cussed in previous chapters into an observable object. What would observers be
interested in? Again, one usage scenario stands out: to show the card drawn. So we
could fix this expectation with the callback:
public interface DeckObserver {
void cardDrawn(Card pCard);
}
However, in some cases this might be too strict. Some observers might be interested
in the number of cards left in the deck, or they may want to know about the top card,
etc.
A more flexible strategy is instead to let observers pull the data they want from
the model using query methods defined on the model. Appropriately, this approach
is known as the pull data-flow strategy. To convert the design of the LuckyNumber
application to use the pull strategy, we could exchange the pNumber parameter with
one that would refer to the entire model:
public interface Observer {
void newNumber(Model pModel);
}
That way, the data to put in the text field must be obtained from the model:
public class IntegerView implements Observer {
// User interface element that represents a text field
private TextField aText = new TextField();
...
public void newNumber(Model pModel) {
aText.setText(Integer.toString(pModel.getNumber()));
}
}
204 8 Inversion of Control
Now, any data available through the methods of class Model also becomes avail-
able to the observers. To implement the pull data flow strategy, observers must have
a reference to the model, but this reference must not necessarily be provided as an
argument to the callback method. Another option is to initialize observer objects
with a reference to the model (stored as a field), and refer to that field as necessary.
This design is illustrated in the class diagram of Figure 8.6. That design makes it
clear that the reference to the model is obtained through the constructor.
Fig. 8.6 Class diagram of LuckyNumber as a model using the pull data-flow strategy for observers
At first glance, it may look like the pull data-flow strategy introduces a circular
dependency between a model and its observers, given that both depend on each
other. However, the crucial difference is that, in this design, the model does not
know the concrete type of its observers. Through interface segregation, the only
slice of behavior that the model needs from observers is specified through their
callback method. This being said, one of the main drawbacks of the pull data-flow
strategy is that it does, indeed, increase the coupling between observers and model.
In the design of Figure 8.6, observers can not only call getNumber(), they can
also call setNumber(int)! In other words, by holding a reference to the model,
observers have access to much more of the interface of the model than they need.
Fortunately, we saw how to deal with this situation with the Interface Segregation
Principle (ISP, see Section 3.2). To apply ISP to our design, we could create a new
interface ModelData that only includes the getter methods for the model, and only
refer to this type in the observers. Figure 8.7 illustrates this solution.
Although I presented them here separately, the push and pull strategies can be
combined. For example, it is possible to specify a callback that includes a param-
eter for both data from the model and a reference back to the model. This design
would not be very useful in our scenario, but I include its implementation for sake
of illustration:
public interface Observer {
void newNumber(int pNumber, ModelData pModel);
}
In general, supporting both strategies can help increase the reusability of the
Observer interface at the cost of a more complex design that may include situations
8.3 The O BSERVER Design Pattern 205
where one parameter is not used.3 At the other extreme, for simple design contexts it
may be the case that the only information that needs to flow between the model and
the observers is the fact that a given callback method was invoked. In such cases,
neither the push nor the pull strategy is required: receiving the callback invocation
is enough information for the observers to do their job. An observer that serves as a
counter of a type of event would be one example.
As a final remark regarding the flow of data between the model and its observers,
it is worth noting that in the examples above, none of the callbacks return any value,
and therefore have return type void. This is not a design decision, but a constraint
of the pattern. Because the model is supposed to ignore how many observers it has,
it can be tricky for observers to attempt to manage the model by returning some
value. Technically, it is possible to declare the return type of callbacks to be non-
void, and to aggregate the results across many invocations. For example, one could
design the callback to return true if it somehow succeeded in responding to the
callback (and false otherwise), and have the model apply a logical operator to
the results. Such schemes represent uncommon and possibly fragile applications of
the pattern. When starting out with the O BSERVER, my recommendation is to have
callbacks return void.
Event-Based Programming
One way to think about callback methods is as events, with the model being the event
source and the observers being the event handlers. Within this paradigm, the model
generates a series of events that correspond to different state changes of interest,
and other objects are in charge of reacting to these events. What events correspond
to in practice are simply method calls. Thinking about observers as event handlers
helps realize that we actually have a lot of flexibility when designing callbacks. In
the LuckyNumber application, the design to date has involved a single callback,
3 The Java library includes a pair of types, Observable and Observer, where Observer de-
fines the single callback void update(Observable, Object), which supports both data-
flow strategies. These types are, however, deprecated since Java 9.
206 8 Inversion of Control
With this design, observers do not need to store a copy of the old number, and
they can be notified of precisely the event they are interested in.4 In cases where an
observer does not need to react to an event, the unused callbacks can be implemented
as empty (do-nothing) methods. In the class below, it is assumed that the events are
mutually exclusive, namely that the event increased means increased but not to
the maximum value, and similarly for decreased.
public class IncreaseDetector implements Observer {
public void increased(int pNumber) {
System.out.println("Increased to " + pNumber);
}
public void decreased(int pNumber) {}
public void changedToMax(int pNumber) {}
public void changedToMin(int pNumber) {}
}
With an adapter, the do-nothing behavior becomes inherited, and observers can
override only the subset of callbacks that correspond to the events they need to
respond to:
4Whether we need the parameter for the changeToMax and changeToMin methods depends
on the context, that is, whether the minimum and maximum values are known globally.
8.3 The O BSERVER Design Pattern 207
The context for using the O BSERVER is fairly rich: it involves situations where many
objects should be able to observe some data, and become aware of changes to the
state of this data, while minimizing the coupling between the data and the observers
of that data. Given a class that represents the data (the model), the solution template
for the pattern involves making objects of this class observable by aggregating a
number of abstract observers (usually defined with an interface). The following are
important variation points when applying the O BSERVER:
• What callbacks methods to define on an abstract observer. An abstract observer
can have any number of callbacks that can correspond to different types of events;
208 8 Inversion of Control
• What data flow strategy to use to move data between the model and observers
(push, pull, none, or both);
• Whether to use a single abstract observer or multiple ones. Multiple abstract ob-
servers with different combinations of callbacks give observers more flexibility
to respond to certain events or not;
• How to connect observers with the model if observers need to query or control
the model. Here the use of the Interface Segregation Principle is recommended;
• Whether to include a notification helper method and, if so, whether to make this
method public or private. If public, clients with references to the model get to
control when notifications are issued. If private, it is assumed that the method is
called at appropriate places in the state-changing methods of the model.
The listing below shows the complete code of the Model and Observer type
declarations for the variant that uses the push data-flow strategy.
public interface Observer {
void newNumber(int pNumber);
}
The design space for applying the O BSERVER is extensive. Even in a small, well-
defined context, many different alternatives are possible for designing the observer
and observable types. To illustrate some of the options available and their corre-
sponding trade-offs, let us now explore different designs for an observable version
of the CardStack class introduced in Section 6.1.
The CardStack class provides an implementation of the stack abstract data type
specialized for Card objects. Figure 8.8 summarizes the definition of the class. With
this design, it is only possible to find out about the state of a CardStack in the
traditional way, by querying it via methods of its interface: peek(), isEmpty, and
via its iterator. Let us now assume that other objects may want to observe instances
of this class. When applying the O BSERVER, we usually wish to make the design
general enough to accommodate an open-ended variety of observers. However, to
make the discussion more concrete I will consider two specific observers:
• A counter, which reports the number of cards in the stack at any point, and detects
when the last card has been popped;
• An ace detector, which detects whether an ace is added to the stack at any point.
The simplest design I can think of for making the CardStack observable is to in-
troduce one abstract observer with three callbacks, one per state-changing method.
210 8 Inversion of Control
Figure 8.9 shows the relevant design elements, including classes for the two required
concrete observers.
As for the observers, their implementation reveals some of the limitations of this
design. For the AceDetector, we are only really interested in one event, and must
therefore provide two empty callback implementations:
public class AceDetector implements CardStackObserver {
public void pushed(Card pCard) {
if( pCard.getRank() == Rank.ACE ) {
System.out.println("Ace detected!");
}
}
The solution sketched above fuses the application of the observer pattern to the im-
plementation of CardStack, thereby coupling client code with the observer machin-
212 8 Inversion of Control
ery (the attach method and observer notification) even when it is not needed. An
approach that yields more flexibility is to use inheritance to provide an observable
extension to CardStack. The left side of Figure 8.10 captures this decomposition.
With this design, client code that only requires the plain CardStack can refer
to the original version, and clients in contexts that require an observable one can
instantiate the subclass instead. The ObservableCardStack subclass reuses all the
original state-changing methods, but also overrides them to add the observer notifi-
cation. For example, for pop():
public class ObservableCardStack extends CardStack {
...
public Card pop() {
Card popped = super.pop();
for( CardStackObserver observer : aObservers ) {
observer.popped(popped);
}
return popped;
}
}
While we are at it, we can also leverage inheritance to solve the problem that we
may need to provide empty callback implementations in some observers (such as
AceDetector). As illustrated on the right side of Figure 8.10, we can provide an
adapter class for the Observer interface. The class CardStackObserverAdapter
8.4 Applying the O BSERVER Design Pattern 213
provides empty implementations for all callbacks. By inheriting from the adapter,
observers only need to provide an implementation for the relevant callbacks.5
Let us now try out an implementation with a pull data-flow strategy. With this al-
ternative, we want to allow observers to pull (fetch) the data they need from the
observable card stack, while maintaining a minimal amount of coupling between
the observers and their subject. For this purpose, we will use the Interface Segre-
gation Principle (see Section 3.9) and define a new interface that declares only the
state-querying methods of CardStack. The new interface CardStackView will al-
low us to have objects that can query the state of a card stack, without being coupled
to state-changing methods such as push or pop, or the observer registration method
(attach). Figure 8.11 illustrates the new design variant.
5 I used an explicit adapter class to emphasize the inheritance relation. In practice, it would be
preferable to inherit from default methods declared in the interface as described in Event-Based
Programming, Section 8.3.
214 8 Inversion of Control
As for the Counter observer, we have an interesting situation. Because the entire
state of the card stack is now accessible, we no longer need to replicate the size of
the stack in a field within Counter. However, the CardStackView does not provide
a method size() that would allow us to retrieve this size conveniently. Instead, we
would have to iterate every time through all the cards in the stack to get the size.
Two alternatives are to either modify the CardStack and CardStackView types to
include a size() method, or to implement a helper method within Counter. The
trade-off is that in the first case, we widen the interface of the class, possibly for
a rare usage scenario, whereas the second option may prove overly inefficient if
we are dealing with large stacks. For now, I will chose to use a helper method in
Counter:
public class Counter implements CardStackObserver {
private static int size(CardStackView pView) {
int size = 0;
for( Card card : pView ) {
size++;
}
return size;
}
It is worth noticing that with this solution, the implementation of the Counter is
no longer brittle, as the callbacks will return the correct card count independently of
when the object is attached to its subject ObservableCardStack.
8.4 Applying the O BSERVER Design Pattern 215
As our final variant, we will look at a design with only a single callback that sup-
ports both push and pull data-flow strategies. Figure 8.12 shows the changed el-
ements in the solution. With only one callback for multiple kinds of events, the
nature of the event is no longer represented by the name of the method, so I changed
it to the general actionPerformed. We still need a way to distinguish between the
kinds of events being reported, however. For this purpose, we can introduce a value
that represents the event kind. This role is served by the parameter of enumerated
type Action. To support both push and pull data-flow strategies, I combined the
structures we defined above: a parameter to represent the card involved in the state
change, and a parameter to refer back to the observable structure. While it makes
sense to include a reference to the observable for all kinds of event, the same is not
true for the value we push. For the CLEAR event, there is no card involved in the
event. One solution to this problem is to use an Optional wrapper to avoid passing
null (see Section 4.5).
Unfortunately, for observers that must handle multiple events (such as Counter),
the routing of all events through a single callback is likely to lead to a S WITCH
S TATEMENT† as one method needs to handle separate computations:
216 8 Inversion of Control
One alternative solution is to create three different observers, one for each type.
If we go down this route, we could also get rid of the Action type parameter and
maintain three lists in the observable, one for each type of event. In this case, the
registration context would be necessary to determine the type of event.
With just a simple design context, we have already explored many different ways
to apply the O BSERVER. Even then, many implementation variants remain possible.
For example, in some contexts it may make sense to have at most one observer
per event type. When inversion of control is needed, it is thus more important to
carefully consider the requirements of the design context and apply the pattern ac-
cordingly, than to try to employ a predetermined solution template.
In JetUML, UserPreferences is the class that stores and manages the var-
ious preferences that users can select via the application menus (for ex-
ample, whether to show the grid or not). The class is both a S INGLETON
and a subject in the O BSERVER pattern. The instance of the class manages
two types of preferences, depending on whether they are Boolean or inte-
ger values. In this design, preferences are represented as values of enumer-
ated types. Let us take BooleanPreference as an example. The method
setBoolean stores the preference value, then notifies all the registered
BooleanPreferenceChangeHandler objects. This design makes it possi-
ble to have completely different parts of the application react to changes in
user preferences without complex chains of method calls. For example, class
DiagramCanvas is a concrete observer of Boolean preference changes. In its
callback, it checks whether the preference that changed is showGrid and, if
so, it repaints the canvas.
8.5 Introduction to Graphical User Interface Development 217
In many technologies, the code that implements the Graphical User Interface (GUI)
portion of an application makes heavy use of the O BSERVER. This section and the
next two are an introduction to GUI development that serves the dual purpose of in-
troducing the concept of an application framework and reinforcing knowledge of the
O BSERVER pattern through its application in a new context. This part of the chapter
is based on JavaFX, an extensive GUI framework for the Java language. However,
the general concepts presented here apply to other GUI development frameworks.
Conceptually, the code that makes up a GUI application is split into two parts:
• The framework code consists of a component library and an application skele-
ton. The component library is a collection of reusable types and interfaces that
implement typical GUI functionality: buttons, windows, etc. The application
skeleton is a GUI application that takes care of all the inevitable low-level as-
pects of GUI applications, and in particular monitoring events triggered by input
devices and displaying objects on the screen. By itself, the application skeleton
does not do anything visible: it must be extended and customized with applica-
tion code.
• Application code consists of the code written by GUI developers to extend and
customize the application skeleton so that it provides the required user interface
functionality.
A GUI application does not execute the same way as the script-like applications
we write when learning to program. In such programs, the code executes sequen-
tially from the first statement of the application entry point (the main method in
Java) and the flow of control is entirely dictated by the application code. With GUI
frameworks, the application must be started by launching the framework using a
special library method. The framework then starts an event loop that continually
monitors the system for input from user interface devices. Throughout the execution
of the GUI application, the framework remains in control of calling the application
code. The application code, written by the GUI developers, only get executed at
specific points, in response to calls by the framework. This process is thus a clear
example of inversion of control. Application code does not tell the framework what
to do: it waits for the framework to call it.
Figure 8.13 illustrates the essence of the relation between the LuckyNumber ap-
plication and the JavaFX framework. The class diagram shows how the application
code defines a LuckyNumber class that inherits from the framework’s Application
class. To launch the framework, the following code is used:
public class LuckyNumber extends Application {
public static void main(String[] pArgs) { launch(pArgs); }
@Override
public void start(Stage pPrimaryStage) {
...
}
}
218 8 Inversion of Control
This code calls the static method Application#launch, which launches the
GUI framework, instantiates class LuckyNumber and then executes method start()
on this instance.6 With this setup, class LuckyNumber is effectively used as the con-
nection point between the application code used to extend the GUI and the frame-
work code in charge of running the show.
Conceptually, the application code for a GUI application can be split into two
categories: the component graph,7 and the event handling code.
The component graph is the actual user interface and is comprised of a number
of objects that represent both visible (e.g., buttons) and invisible (e.g., regions) el-
ements of the application. These objects are organized as a tree, with the root of
the tree being the main window or area of the GUI. In modern GUI frameworks,
constructing a component graph can be done by writing code, but also through con-
figuration files that can be generated by GUI building tools. Ultimately, the two
approaches are equivalent because, once the code runs, the outcome is the same:
a tree of plain Java objects that form the user interface. The design of the library
classes that support the construction of a component graph makes heavy use of poly-
morphism and the C OMPOSITE and D ECORATOR patterns. In JavaFX, the component
graph for a user interface is typically instantiated in the application’s start(Stage)
method.
Once the framework is launched and displaying the desired component graph, its
event loop will automatically map low-level system events to specific interactions
with components in the graph (for example, placing the mouse over a text box, or
clicking a button). In common GUI programming terminology, such interactions
are called events. Unless specific application code is provided to react to an event,
nothing will happen as a result of the framework detecting this event. For example,
clicking on a button will graphically show the button to be clicked using some user
interface cue, but then the code will simply continue executing without having any
impact on the application logic. To build interactive GUI applications, it is necessary
to handle events like button clicks and other user interactions. Event handling in
GUI frameworks is an application of the O BSERVER pattern, where the model is a
6 Method launch uses metaprogramming to discover which application class to instantiate.
7 In the documentation for JavaFX, the component graph is called the scene graph.
8.6 Graphical User Interface Component Graphs 219
GUI component (such as a button). Handling a button click, or any similar event,
then becomes a matter of defining an observer and registering it with the button. The
next two sections detail how to design component graphs and handle events on GUI
components.
The component graph is the collection of objects that forms what we usually think
of as the user interface: windows, textboxes, buttons, etc. At different stages in the
development of a graphical user interface, it can be useful to think about this user
interface from three different point of views, or perspectives: user experience, source
code, and run time.
The user experience perspective corresponds to what the user experiences when in-
teracting with the component graph. Figure 8.2, shown earlier in this chapter, shows
the user experience perspective on the component graph for the LuckyNumber ap-
plication. Because not every object in the component graph is necessarily visible,
it is important to remember that the user experience perspective does not show the
complete picture of the application. This picture is complemented by the other two
perspectives.
The source code perspective shows the kind of information about the component
graph that is readily available from the declarations of the classes of the objects
that form the component graph. This information is best summarized by a class
diagram. Figure 8.14 models the source code perspective on the component graph
of the LuckyNumber application. Despite the application being tiny, the diagram
shows that a lot of code is required to instantiate its component graph. Let us walk
through this diagram.
The Scene holds a reference to the root node of the component graph, something
we can deduce from the fact that it is not a subtype of Node, and no class in the dia-
gram aggregates it. The Scene class aggregates class Parent. This may be puzzling
at first, because in my logical model of the component graph, I indicated that the
Scene contains a GridPane. This is an example of polymorphism in use. To allow
users to build any kind of application, the Scene library class accepts any subtype
of type Parent as its child object. In turn, Parent is a subtype of the general Node
type that adds functionality to handle children nodes. In JavaFX, all objects that can
be part of a component graph need to be a subtype of Node, either directly or, more
220 8 Inversion of Control
generally, indirectly by inheriting from other subtypes of Node. The fact that Parent
nodes, which can contain children nodes, are themselves of type Node shows that the
design of the GUI component hierarchy is an application of the C OMPOSITE pattern.
By continuing our investigation of the diagram, we find class GridPane as a
subtype of Parent. This is the reason it is possible to add a GridPane to a scene. A
GridPane is a type of user interface Node that specializes in organizing its children
into a grid. I used it for LuckyNumber to lay out the number views vertically on top
of each other.
In the LuckyNumber application, a GridPane contains a set of Parent compo-
nents. In the general case, a GridPane can contain any subtype of Node. However,
in my design of the application, I created three classes that inherit from Parent:
TextPanel, IntegerPanel, and SliderPanel. These classes represent the three
views of the number in the Model–View–Controller decomposition. By defining
these classes as subclasses of Parent, I achieve two useful properties:
The diagram of Figure 8.14, already somewhat involved, actually omits, for clar-
ity, many intermediate types in the inheritance hierarchy for nodes. For example, the
diagram shows GridPane to be a direct subclass of Parent. In reality, GridPane
is a subclass of Pane, which itself is a subclass of Region, which is a subclass of
Parent. Figure 8.15, while still an incomplete model, shows a bigger picture of the
class hierarchy that can be leveraged to define component graphs in JavaFX.
The run-time perspective is the instantiated component graph for a graphical inter-
face. This perspective can best be represented as an object diagram. Figure 8.16
shows the instantiated component graph for LuckyNumber.
In Section 8.5 I mentioned how after the framework starts it calls the start method
of the main application class (LuckyNumber in our case). This start method is the
natural integration point for extending the framework, and this is where we put the
code that builds the component graph. The code below is the minimum required to
get the application to create the LuckyNumber component graph. In practice, this
kind of code would typically be extended with additional configuration code and
organized using helper methods. The additional configuration code can be used to
beautify the application, for example by adding margins around components, a title
to the window, etc. The JavaFX functionality to generate component graphs from
configuration files is outside the scope of this book.
public class LuckyNumber extends Application {
public void start(Stage pStage) {
Model model = new Model();
pStage.setScene(new Scene(root));
pStage.show();
}
}
The first statement of method start is to create an instance of Model. This in-
stance will play the role of the model in the O BSERVER pattern. It is related to the
construction of the component graph because, as detailed later, some of the com-
ponents in the graph need access to the model. The second statement creates a
GridPane, which is an invisible component used for assisting with the layout of
children components. The local variable that holds a reference to this component is
222 8 Inversion of Control
helpfully named root to indicate that it is the root of the component graph. Then,
three application-defined components are added to the grid. The parameters to the
add method indicate the column and row index and span. For example, the state-
ment:
root.add(new SliderPanel(model), 0, 0, 1, 1);
specifies to add an instance of the SliderPanel in the top-left cell in the grid, and
span only one column and one row. Because SliderPanel is a subtype of Parent,
and thus a subtype of Node, it can be added to the grid. Another important thing to
note about the instantiation of the panel components is that their constructor takes
as argument a reference to the model.
The last two statements of the method are not really related to the construction of
the component graph, but are nevertheless crucial steps in the creation of the GUI.
The statement with the call to setScene creates a Scene from the component graph
and assigns it to the framework’s Stage. Finally, the last statement requests that the
framework display the Stage onto the user’s display.
For additional insights on the creation of the component graph, the code below
shows the relevant part of the constructor of the IntegerPanel (the other panels
are very similar).
public class IntegerPanel extends Parent implements Observer {
private TextField aText = new TextField();
private Model aModel;
This code illustrates a number of insights about the design of the component
graph. First, as already mentioned, the application-defined IntegerPanel class
extends the framework-defined Parent class so that it can become part of the
component graph. Second, an instance of IntegerPanel aggregates a framework-
defined TextField component. However, the mere fact of defining an instance
variable of type TextField inside the class does not add the TextField to the
component graph. To do this, it is necessary for the IntegerPanel to add the in-
stance of TextField to itself, something that is done with the call getChildren()
.add(aText). Method getChildren() is inherited from class Parent, and used to
obtain the list of children of the parent user interface Node, to which the TextField
instance can then be added.
The IntegerPanel instance also maintains a reference to the Model. The reason
for this is that the IntegerPanel needs to act as a controller for the Model, some-
thing that will be explained in more detail in the next section. Also, it is worth notic-
ing how the IntegerPanel is an observer of the Model instance: it declares to im-
plement Observer, it registers itself as an observer upon construction (second state-
ment of the constructor), and it supplies an implementation for the newNumber call-
back. As expected, the behavior of the callback is to set the value of the TextField
user interface component with the most recent value in the model, obtained from
the callback parameter.
As a final insight on the design of the component graph, we can note how the
instance of Model created in method start (see preceding code fragment) is stored
in a local variable. In other words, the application class LuckyNumber does not
manage an instance of the model: this is only done within each panel. This design
decision is to respect the guideline provided in Chapter 4, to keep the number of
fields to a minimum. Without care, an application-defined user interface component
can become a G OD C LASS† bloated with numerous references to stateful objects,
which makes a design much harder to understand.
In GUI frameworks, objects in the component graph act as models in the O BSERVER.
Once the framework is launched, it continually goes through a loop that monitors
input events and checks whether they map to events that can be observed by the
application code. This process is illustrated in Figure 8.17.
Typically, events are defined by the component library supplied by the frame-
work. For example, the TextField user interface component defines an action
event. According to its class documentation “The action handler is normally called
8.7 Event Handling 225
Did the
Yes
Is a user interface application
event detected? register interest
in this event?
Yes
when the user types the ENTER key”. This means that an instance of TextField
can play the role of the model in the O BSERVER. Figure 8.18 shows the correspon-
dence between the code elements and the roles in the O BSERVER pattern.
Fig. 8.18 Correspondence between TextField and the roles in the O BSERVER
Handling the action event on a text field is thus pretty straightforward. All we
need to do is to:
• Define a handler for the event. This means defining a class that is a subtype of
EventHandler<ActionEvent>. The class will be our event handler class.
• Instantiate a handler. This means creating an instance of the class we defined
in the previous step. The instance will be our event handler instance, also called
event handler, or even just handler.
• Register the handler. This means calling the registration method on the model
and passing the handler as an argument. In the case of TextField, we need to
call setOnAction(handler). It is worth noticing an interesting design choice
for this application of O BSERVER: it is only possible to have a single observer for
a TextField.
Although the basic mechanism for specifying and registering event handlers is
always the same, one design choice that must be resolved is where to place the
definition of the handling code. For this, two main strategies are possible:
• To define the handler as a function object using an anonymous class or a
lambda expression (see Section 3.4). This is a good choice if the code of the
handler is simple and does not require storing data that is specific to the handler;
226 8 Inversion of Control
There are two main implications of this choice on the code. First, the handle
method needs to be declared directly in class IntegerPanel. Second, the argument
passed to aText.setOnAction is now this, because it is the IntegerPanel in-
stance itself that is now responsible for handling the event.
Although both design options for locating the handler code are workable, for the
LuckyNumber application I prefer the function object alternative. The handler code
is just a few lines long, and with the function object the behavior of the handler is
located with other code that initializes the text field, so everything is in one place.
228 8 Inversion of Control
Inversion of control can be useful to create loosely coupled design solutions in con-
texts other than applications of the O BSERVER pattern and event handling mecha-
nisms. An additional recognized use for inversion of control is the V ISITOR design
8.8 The V ISITOR Design Pattern 229
pattern. The context for applying the V ISITOR pattern is when we want to make it
possible to support an open-ended number of operations that can be applied to an
object graph, but without impacting the interface of the objects in the graph. To
illustrate such a context, we will use yet another variant of the CardSource type hi-
erarchy. Figure 8.20 shows a design where different types of concrete card sources
have the CardSource interface in common, but then different individual interfaces
for services other than draw() and isEmpty();
Fig. 8.20 Design context for a sample application of the V ISITOR. The constructor uses the vararg
construct to accept an unspecified number of cards as argument. The mechanism is also employed
by the constructor of CompositeCardSource.
In this design, there are three different types of card sources. Although they all
implement the CardSource interface, their commonality ends there. Class Deck
can be shuffled and is iterable. A CardSequence can be initialized with a pre-
determined list of cards, but cannot be shuffled and is not iterable. Instead, elements
in a CardSequence can be accessed through an integer index. For this reason, the
class also includes a size() method. Finally, CompositeCardSource is an appli-
cation of the C OMPOSITE with a narrow interface, as it offers no services besides
those of CardSource. In this example as well as in general, the reason classes that
implement an interface have methods other than the ones in the interface is simply
that each class is intended to work in a specific context where its additional methods
are necessary and, to respect the Interface Segregation Principle, the only methods
in the common type are those used by all contexts (see Section 3.9).
The above design will fulfill its mandate as long as the client code only requires
the limited functionality it currently provides. Problems will arise, however, when
we start needing additional functionality from the card sources. Examples of opera-
tions that may be necessary at some point include:
• Printing to the console a description of each of the cards in the source;
• Obtaining the number of cards in the card source;
• Removing a certain card from a card source;
230 8 Inversion of Control
The cornerstone of the V ISITOR pattern is an interface that describes objects that can
visit objects of all classes of interest in an object graph. This interface is appropri-
ately called the abstract visitor. An abstract visitor follows a prescribed structure:
it contains one method with signature visitElementX(ElementX pElementX) for
each different type of concrete class ElementX in the object structure.8 In our case,
the abstract visitor would be defined as follows:
public interface CardSourceVisitor {
void visitCompositeCardSource(CompositeCardSource pSource);
void visitDeck(Deck pDeck);
void visitCardSequence(CardSequence pCardSequence);
}
8 Technically, the methods can be overloaded, which leads to the more compact form
visit(ElementX pElementX). For the reasons discussed in Section 7.5, I recommend
avoiding overloading by using the longer form.
8.8 The V ISITOR Design Pattern 231
Although a concrete visitor separates a well-defined operation into its own class, it
still needs to be integrated with the class hierarchy that defines the object graph on
which the operation will be applied (henceforth referred to as the class hierarchy).
This integration is accomplished by way of a method, usually called accept, that
acts as a gateway into the object graph for visitor objects. An accept method takes
as single argument an object of the abstract visitor type (CardSourceVisitor in our
232 8 Inversion of Control
case). Unless there is a good reason not to, we normally define the accept method
on the common supertype of the class hierarchy:
public interface CardSource {
Card draw();
boolean isEmpty();
void accept(CardSourceVisitor pVisitor);
}
The implementation of accept by concrete types is where the integration re-
ally happens. This implementation follows a prescribed formula: to call the visit
method for the type of the class that defines the accept method. For example, the
implementation of accept for class Deck is:
public void accept(CardSourceVisitor pVisitor) {
pVisitor.visitDeck(this);
}
and the one for class CardSequence is:
public void accept(CardSourceVisitor pVisitor) {
pVisitor.visitCardSequence(this);
}
The only difference between the two implementations of accept is the specific
visitElementX method that is being called. The version of accept for the Compos-
iteCardSource class is more involved, and is discussed further below, in the sec-
tion Traversing the Object Graph.
Figure 8.21 shows the result of applying the V ISITOR to our context. The figure
includes two concrete visitors to emphasize that the goal of the pattern is to support
adding multiple operations to a class hierarchy.
With the accept method in place, executing an operation on the object graph is
now a matter of creating the concrete visitor object that represents the operation,
and passing this object as argument to method accept on the target element:
PrintingVisitor visitor = new PrintingVisitor();
Deck deck = new Deck();
deck.accept(visitor);
Figure 8.22 shows the result of calling accept on an instance of Deck. The client
code, which holds the reference to the concrete visitor, calls accept on an instance
of Deck with the visitor as argument. The accept method then calls back the appro-
priate method on the visitor. In this sequence, the visitDeck method qualifies as a
callback method. With complex object structures, it may not always be possible to
determine when a visit method will be called. Just like in the O BSERVER pattern,
the model calls its observers back at the appropriate time, in the V ISITOR, concrete
elements call the visitors at the appropriate time.
So far in our application of the V ISITOR we have left out a critical aspect of the pat-
tern: the traversal of the object graph. Any object graph with more than one element
8.8 The V ISITOR Design Pattern 233
Fig. 8.21 Sample application of the V ISITOR pattern with the name of the roles in notes
will have an aggregate node as its root. In our case this is CompositeCardSource,
so let us look at what happens when we apply an operation to such a node. Let us
say we implement accept for this class similarly as for Deck and CardSequence:
public class CompositeCardSource implements CardSource {
public void accept(CardSourceVisitor pVisitor) {
pVisitor.visitCompositeCardSource(this);
}
}
often a recursive one. For the traversal aspect of the pattern to be applicable, at least
one element type in the target hierarchy needs to serve as an aggregate for other
types. In our case this role is played by CompositeCardSource. Let us consider
the object graph illustrated in Figure 8.23. If we want to print the cards reachable
through the root card source, we would need to traverse the entire graph to find all
the cards. We would also need to do such a traversal to count the total number of
cards in a source, remove all instances of a specific card, etc.
There are two main ways to implement the traversal of the object graph in the
V ISITOR. One option is to place the traversal code in the accept method of aggregate
types. The other option is to place this code in the visit methods that serve as
callbacks for aggregate types.
In our case, placing the traversal code in the accept method is relatively straight-
forward:
public class CompositeCardSource implements CardSource {
private final List<CardSource> aElements;
Because the traversal code is implemented within the class of the aggregate, it can
refer to the private field that stores the aggregation (aElements). This access to pri-
vate structures is one major motivation for implementing the traversal code within
the accept method. I discuss additional advantages and disadvantages of this choice
below. Figure 8.24 shows the beginning of the call sequence that results from calling
accept on the root target node of Figure 8.23. From this figure it becomes easier
to visualize the concrete visitor as an implementation of callback methods: some
independent code traverses an object structure and calls the visitElementX call-
backs as appropriate, and methods of the visitor object respond to these visitation
notifications. The fact that the traversal code is implemented in accept is visible
by the fact that some calls to accept originate from the activation bar of a different
accept invocation.
8.8 The V ISITOR Design Pattern 235
Fig. 8.24 Partial call sequence resulting from a call to root.accept on the object graph of
Figure 8.23 when the traversal code is implemented in the accept method
The second option for implementing the traversal code is to put it in the visit
method that corresponds to the element types that are aggregates. In our case this
means, as before, CompositeCardSource. Unfortunately, in our context it is not
possible to implement this option directly, because the aggregate class offers no
public access to the CardSource objects it aggregates. Because the code of the
visit methods is in a separate class, we need a way to access the objects stored by
the private field aElements. To make this work we make CompositeCardSource
iterable over the CardSource instances it aggregates. However, this requirement
to decrease the level of encapsulation of the class is a disadvantage of this design
decision.
public class CompositeCardSource implements CardSource,
Iterable<CardSource> {
private final List<CardSource> aElements;
Figure 8.25 shows the corresponding call sequence on the root of the object graph
illustrated in Figure 8.23.
Fig. 8.25 Partial call sequence resulting from a call to root.accept on the object graph of
Figure 8.23 when the traversal code is implemented in the visit method
As we have seen above, the main advantage of placing the traversal code in the
accept method is that it can help achieve stronger encapsulation because the inter-
nal structures can be accessed without being part of the class’s interface. The main
disadvantage of placing the traversal code in the accept method, however, is that
the traversal order is fixed in the sense that it cannot be adapted by different visitors.
In our simple example, the traversal order did not really matter. But let us say that in
our print visitor we care about the order in which the cards are printed. The code for
the accept method, above, implements a pre-order traversal (visit the node, then the
children). Some operations, however, might require a post-order traversal (visit the
children, then the node). If the traversal code is implemented in accept, concrete
visitors cannot change it. In a nutshell, if encapsulation of target elements is more
important, it is better to place the traversal code in the accept method. If the ability
to change the traversal order is more important, then it is better to place the traversal
code in the visit method.
8.8 The V ISITOR Design Pattern 237
The question of where to place the traversal code brings up the issue of code D UPLI -
CATED C ODE† again. If we place the traversal code in the visit methods, and have
more than one concrete visitor class, every class is bound to repeat the traversal
code in its visit method. A common solution to alleviate this issue is to define an
abstract visitor class to hold default traversal code.9 In our case, the following
would be a good implementation of an abstract visitor class:
public abstract class AbstractCardSourceVisitor
implements CardSourceVisitor {
public void visitCompositeCardSource(
CompositeCardSource pCompositeCardSource) {
for( CardSource source : pCompositeCardSource ) {
source.accept(this);
}
}
There are two important things to observe about this implementation. First, I re-
tained the interface. Because most concrete visitors would be implemented as sub-
classes of AbstractCardSourceVisitor, one can wonder, why not just use this
abstract class to serve in the role of abstract visitor, and get rid of the interface? The
general reason is that interfaces promote more flexibility in a design. For example,
one concrete drawback of using an abstract class is that, because Java only supports
single inheritance, defining the abstract visitor as an abstract class prevents classes
that already inherit from another class to serve as concrete visitors.
The second notable detail in the above code is that the visit methods for
classes Deck and CardSequence are implemented as empty placeholders. Given
that AbstractCardSourceVisitor is declared abstract, we do not need these
declarations. However, providing empty implementations for visit methods allows
the abstract visitor class to serve as an adapter. In more realistic applications of
the pattern, the element type hierarchy can have dozens of different types, with a
corresponding high number of visit methods. With empty implementations, con-
crete visitors only need to override the methods that correspond to types they are
interested in visiting.
As an example, the following declaration creates an anonymous visitor class that
prints the number of cards in every CardSequence in a card source structure, and
ignores the rest. Because the class inherits the traversal code, card sequences aggre-
gated within composite card sources will also be reached.
9Here it is important to distinguish between an abstract visitor class and an abstract visitor,
which is usually an interface.
238 8 Inversion of Control
The result of using this visitor on the object graph of Figure 8.23 would be:
Composite
Deck
Composite
Deck
CardSequence
This example introduces two new aspects to our discussion so far. First, the visi-
tor is stateful, as it stores data. Specifically, the class defines a field aTab that stores
the depth of the element currently being visited. Depth increases when visiting the
elements aggregated by a composite card source. Correspondingly, the second no-
table aspect in the code above is the reuse of the traversal code through a super call.
Here, the pre-order traversal implemented in the abstract visitor class is what we
need. However, additional code is required when visiting a composite card source.
8.8 The V ISITOR Design Pattern 239
So far our examples of concrete visitors have carefully avoided the issue of data
flow, because the PrintVisitor neither requires input nor produces output. Most
realistic operations, however, do involve some data flow. For example, a visitor to
compute the total number of cards in a card source must be able to return this num-
ber. As another example, an operation to determine if a certain card is contained
in a card source must receive the card of interest as input. When operations are
implemented in traditional methods, this kind of data flow is not an issue: input is
passed in as argument to a method, and output can be returned to the calling context
through return statements. In the V ISITOR pattern, this is more complex. To support
a general and extensible mechanism for defining operations on an object graph, the
pattern requires that no assumption be made about the nature of the input and output
of operations.
Data flow for V ISITOR-based operations is thus implemented differently, by stor-
ing data within a visitor object. Input values can be provided when constructing a
new visitor object and made accessible to the visit methods. Output values can be
stored internally by visit methods during the traversal of the object graph, and
made accessible to client code through a getter method.10 Let us consider each case
in turn, starting with output values. To exemplify the process, we implement a vis-
itor to count the total number of cards in the source. This version assumes that the
abstract visitor class defined above is available:
public class CountingVisitor extends AbstractCardSourceVisitor {
private int aCount = 0;
10 Using generic types, it is also possible to design a solution that does not necessarily require
this accumulation of state for output values. In this solution, the accept and visit methods
return a value of a generic type. Design with generic types is outside the scope of this book, so I
do not present the solution here. However, a sample implementation is available on the companion
website.
240 8 Inversion of Control
Insights
• It can be useful to think of the GUI component graph from three different per-
spectives: user experience, source code, and run time;
• The component graph must be instantiated before the user interface becomes
visible. In JavaFX this instantiation is triggered in the start method of the ap-
plication class;
• You can inherit from component classes of the GUI framework to create cus-
tom graphical components that can be added to an application’s GUI component
graph;
• To make a GUI application interactive, it is necessary to define handlers for GUI
events that originate from different objects in the component graph. Handlers are
defined as observers of objects in the component graph;
• Handlers can be defined as function objects, or the handling can be delegated to
objects of the component graph.
• Consider using the V ISITOR pattern to allow extending an object structure with an
open-ended set of operations, without requiring modification to the interface of
the classes that define this object structure.
Further Reading
As for the other patterns, the Gang of Four book [6] has the original treatment of the
O BSERVER and V ISITOR patterns.
In the book Patterns of Enterprise Application Architecture [4], Fowler pro-
vides a description of the Model–View–Controller as a web presentation pattern.
The book Pattern-Oriented Software Architecture Volume 4: A Pattern Language
for Distributed Computing [2] presents it as a pattern for software architecture, and
integrates it into a general system of patterns for designing distributed applications.
It is possible to find extensive additional information on the JavaFX framework
on the websites of Java technology providers, and in particular Oracle and Open-
JDK.
Chapter 9
Functional Design
Object-oriented design offers valuable principles and techniques for structuring data
and computation. However, alternative ways to structure software can also be lever-
aged when designing applications. This chapter provides an introduction to a style
of design that uses the function as its primary building block. With functional-style
design, structuring the code is achieved through the use of higher-order functions,
that is, functions that take other functions as argument. To use higher-order functions
requires the programming language to provide support for functions as a first-class
program entity. This chapter provides an overview of the Java mechanisms that sup-
port functional-style programming and how to use them to integrate elements of
functional style into the design of an overall application.
Design Context
The design problems considered in this chapter focus on the processing of col-
lections of objects to represent playing cards. Problems include sorting a collec-
tion of cards, comparing cards, filtering cards, and computing various aggregate
values about a collection of cards. We will also revisit the implementation of the
CardSource interface introduced in Section 3.1.
The reason the code above is contrived is that, from a software design point
of view, what the sort method needs is only the desired comparison behavior for
cards, yet what we actually supply is a reference to an object, something generally
understood as an assembly of data and methods to operate on this data. There is thus
a conceptual mismatch between the design goal and the programming mechanism
employed to fulfill it. The design goal is to parameterize the behavior of the sort
method, and the mechanism we use to do this is to pass a reference to an object.
What would be a better fit, would be for the sort method to take in as input the
desired sorting function directly.
Providing functions as input to other functions, however, requires the program-
ming language to allow this by supporting first-class functions. This essentially
means treating functions as values that can be passed as argument, stored in vari-
ables, and returned by other functions.
Since version 8, Java supports a syntax which, in practice, emulates first-class
functions. For example, we could define a function in class Card that compares two
cards by rank:
public class Card {
public static int compareByRank(Card pCard1, Card pCard2) {
return pCard1.getRank().compareTo(pCard2.getRank());
}
}
and supply a reference to this function as the second argument to method sort:
Collections.sort(cards, Card::compareByRank);
1In this chapter, the term function is used as a general abstraction of computation. In Java, the term
would refer to both static and instance methods.
9.2 Functional Interfaces, Lambda Expressions, and Method References 245
This code, which compiles and does what we want, is actually syntactic sugar that
gives the illusion of first-class functions but actually converts the method reference
Card::compare into an instance of Comparator<Card>. The syntax and detailed
behavior of the code above is described in Section 9.2. The implications of being
able to design with first-class functions is significant. They are a major design tool
which, in some cases, allows us to consider design solutions that make the intent
behind the solution clearer, reduce clutter in the code, and help reuse code more
effectively.
With first-class functions, it becomes possible to design functions that take other
functions as arguments. Such functions are called higher-order functions. In a way,
when considering the above code from a functional point of view, we can say that
Collections#sort is a higher-order function. In some contexts, it is possible to
build entire applications from the principled use of higher-order functions. In such
cases, we would say that the application is designed in the functional programming
paradigm. Using higher-order functions does not, by itself, mean that an applica-
tion’s entire design becomes functional. Functional programming is a much more
comprehensive paradigm whereby computation is organized by transforming data,
ideally without mutating state.
Functional programming, even in the limited context of the Java language, is a
major topic whose detailed treatment is outside the scope of this book. There are
good references available for learning about the ins and outs of functional program-
ming features in Java and beyond (see Further Reading). The goal of this chapter
is to provide enough of an introduction to basic functional programming features
to allow the integration of functional elements into an otherwise object-oriented de-
sign. First-class functions support a whole new level of versatility for exploring the
design space, realizing design principles, and applying design patterns. For this rea-
son, it is important to know about functional-style programming even if we are not
building an application strictly in the functional paradigm. This being said, the last
part of the chapter introduces the map–reduce programming model, which will take
us as close to full-fledged functional programming as we will get in this book.
The three mechanisms that enable first-class functions in Java are functional inter-
faces, lambda expressions, and method references.
Functional Interfaces
To use a library type instead of our custom Filter interface, we use the func-
tional interface Predicate<T>, which represents the type of a function with a single
argument of type T and returns a boolean.3 The name of the abstract method for
Predicate<T> is test(T). We can thus rewrite the code above as follows:
Predicate<Card> blackCardFilter = new Predicate<Card>() {
public boolean test(Card pCard) {
return pCard.getSuit().getColor() == Suit.Color.BLACK;
}
};
Because they define function types, functional interfaces serve as the basis for all
functional-style design in Java.
Lambda Expressions
With functional interfaces, we get one step closer to being able to program with
first-class functions. However, with anonymous classes, specifying the behavior of
our example function still has a definite object-oriented look. If we recall our imple-
mentation of the black cards predicate, above:
Predicate<Card> blackCardFilter = new Predicate<Card>() { ... };
The use of the new keyword in the definition of the behavior of our predicate be-
trays the fact that we are still creating an object. To more directly express our design
in terms of a first-class function, we can define the implementation of a functional
interface as a lambda expression. Lambda expressions are a compact form of expres-
sion of functional behavior whose name is derived from the term lambda calculus,
a mathematical system for expressing computation. In Java, lambda expressions are
basically anonymous functions. They were briefly introduced in Section 3.4. Now
we can take a second look at them in the context of functional-style programming.
To convert our example to use a lambda expression, we would write:
Predicate<Card> blackCardFilter =
(Card card) -> card.getSuit().getColor() == Suit.Color.BLACK;
The syntax of lambda expressions is detailed below, but for now it is sufficient to
know that the function parameter is declared on the left of the arrow (->), and the
expression on the right of the arrow represents the body of the function. Although
this code has the same effect as using an anonymous class, the syntax no longer
makes use of the new keyword. In addition to being more compact, the code makes
it more obvious that what we are trying to achieve is to initialize blackCardFilter
with behavior (a function) as opposed to data (an object). We can also say that the
function is anonymous because no function name appears in its declaration. From a
3 In contrast to Function<T,R>, Predicate<T> has a single type parameter because the re-
turn type is implied by the interface. The function type that corresponds to Predicate<T> is thus
T → boolean . We could also specify our filter interface as Function<Card, Boolean>,
but this option is less efficient because it relies on autoboxing.
248 9 Functional Design
design point of view, we do not care about the actual name of the function because
it will get called polymorphically through the name of the method in the functional
interface. Because functional interfaces are intended to be reused, the name of the
method they define tends to be very general, and so it carries little information about
what the method does. In our example, the method in the Predicate<T> functional
interface is test(T): this says nothing about the actual behavior of our lambda
expression (which is to return true if the suit of the card is black). Information
about the behavior of the lambda expression is typically the code of the lambda
expression itself or, at best, an informative variable name (as above). In Java, lambda
expressions are not typically documented with a header comment.
The syntax of lambda expressions comprises three parts: a list of parameters, a
right arrow (the characters ->), and a body. In the example above, the list of parame-
ters is (Card card). When the lambda expression requires no parameter, we simply
provide an empty set of parentheses ()->. The body of the lambda expression can
take one of two forms:
• a single expression (e.g., a == 1).
• a block of one or more statements (e.g., {return a == 1;}).
In the blackCardFilter example, the definition of the body of the lambda ex-
pression uses the first option. Because, given the functional interface, the return
type is expected to be boolean and the expression evaluates to a Boolean value,
the use of the return keyword is superfluous and can be assumed to be the re-
sult of the evaluation. Using the return keyword would turn the expression into
a statement, thus breaking the syntax. It is worth noting how expressing the body
of a lambda as an expression does not require a semicolon after the expression. In
the blackCardFilter example, the final semicolon terminates the entire assign-
ment statement, not the lambda expression. Let us rewrite the lambda expression to
express the body as a block:
Predicate<Card> blackCards =
(Card card) ->
{ return card.getSuit().getColor() == Suit.Color.BLACK; };
This code does exactly the same thing as previously. However, because the body of
the lambda expression is no longer a single expression, we need to add curly braces
around the block that consists of a single statement, use the return keyword to
indicate what we are returning, and terminate the statement within the block with
a semicolon. As can be seen, the first form (using an expression) is more com-
pact. Normally, when we write lambda expressions, we define them as expressions
whenever possible and, when the computation is complex and we need multiple
statements, fall back on defining them as a block.
Behind the scenes, lambda expressions are checked by the compiler and turned
into function objects through a process of inference. Essentially, when the Java com-
piler sees a lambda expression, it tries to match it to a functional interface. In the
code above, the right-hand side of the assignment is a lambda expression. The com-
piler will thus look for the type of the variable to which this lambda is assigned to
make sure everything matches, namely:
9.2 Functional Interfaces, Lambda Expressions, and Method References 249
In fact, if the function type takes a single parameter, we can even omit the parenthe-
ses around the parameter:
Predicate<Card> blackCardFilter =
card -> card.getSuit().getColor() == Suit.Color.BLACK;
Lambda expressions are also a good match for providing behavior in-place when
required by library or application functions. For example, the method removeIf of
class ArrayList takes a single argument of type Predicate<T> and removes all
elements in the ArrayList for which the predicate is true. Given an ArrayList of
Card, we can remove all black cards from the list with a single call:
250 9 Functional Design
Method References
Lambda expressions are especially useful when we need to supply custom behavior
not defined anywhere else. However, it is also common that one part of the code
requires behavior that is already implemented. Let us consider a slight variant of
the design of the Card class where the class includes the definition of the helper
method:
public final class Card {
public boolean hasBlackSuit() {
return aSuit.getColor() == Color.BLACK;
}
}
If, as above, we are writing some code to delete all back cards from an ArrayList:
ArrayList<Card> cards = ...
cards.removeIf(
card -> card.getSuit().getColor() == Suit.Color.BLACK);
This is better, but what we really want in the present scenario is to reuse our
method hasBlackSuit as a first-class function. In other words, we want to pass a
reference to hasBlackSuit as an argument to method removeIf. We can do just
that with method references. In Java, method references are indicated with a double
colon expression P::m where m refers to the name of the method of interest and P is
a prefix that can take different forms. In our case, P refers to the class in which the
method is defined. Thus, Card::hasBlackSuit refers to method hasBlackSuit of
class Card. With this method reference, we can rewrite our code as:
cards.removeIf(Card::hasBlackSuit);
It hardly gets more compact and explicit than that: the code almost reads like plain
English.
Using method references in Java is not trivial, though, because there are differ-
ent ways to refer to a method. In the code above, we have used a reference to an
instance method of an arbitrary object of a particular type. In this scenario, the ar-
gument of the call to the method of the functional interface is bound to the implicit
9.2 Functional Interfaces, Lambda Expressions, and Method References 251
parameter of the method reference. This can be seen from the lambda equivalent
card -> card.hasBlackSuit(). There are other ways in which the compiler can
match method references to functional interfaces.
Another way is to use a reference to a static method. For example, we could also
have the following static method in some utility class:
public final class CardUtils {
public static boolean hasBlackSuit(Card pCard) {
return pCard.getSuit().getColor() != Color.BLACK;
}
}
Although the method reference looks exactly the same as the instance method,
the compiler uses the reference in a different way. In this case, the method reference
is interpreted as:
cards.removeIf( card -> CardUtils.hasBlackSuit(card));
As we see, in this case, the argument is bound to the explicit parameter of the
method reference.
Java also supports supplying a reference to an instance method of a particular
object, using the notation o::m where o is an expression that evaluates to a reference
to an object and m is the method. For example, let us assume that our Deck class has
a method topSameColorAs(Card) which returns true if the argument is of the
same color as the card at the top of the deck. To remove all cards in the list whose
color is the same as the card at the top of the deck, we would do:
Deck deck = new Deck();
...
cards.removeIf(deck::topSameColorAs);
In this case, the Card argument would be matched to the explicit argument of the
instance method of Deck that is called on a specified instance of deck. The equivalent
lambda expression is:
cards.removeIf(card -> deck.topSameColorAs(card));
take as input a single parameter of type Card. In the case of the instance method of
a given type (Card::hasBlackSuit), the parameter is the implicit parameter of the
method; In the case of the static method (CardUtils::hasBlackSuit), the param-
eter is the explicit parameter of the method; In the case of the instance method of a
particular object, the parameter is also the explicit parameter of the method, whose
implicit object is specified in the method reference. In all cases, the function type
is Card → boolean and the reference can thus be assigned to a variable of type
Predicate<Card>.
This design uses a static factory method to return a comparator object that compares
two cards in terms of their suit, as defined by the declaration order in the enumerated
type Suit. Because we use a lambda expression, the code expresses the solution
more in terms of a first-class function than a function object.
This solution is incomplete because if two cards have the same suit, their relative
order is undefined, which is not ideal for many card sorting contexts. To complete
the solution, we need to specify a secondary comparison order by rank. One way to
do this would be to extend the code of the lambda expression:
public static Comparator<Card> bySuitThenRankComparator() {
return (card1, card2) -> {
if( card1.getSuit() == card2.getSuit() ) {
return card1.getRank().compareTo(card2.getRank());
}
else {
return card1.getSuit().compareTo(card2.getSuit());
}
};
}
This code supports a well-defined total order for cards, at the cost of a more complex
lambda expression for which we need to resort to the less compact block form. This
code is also less flexible, because if we wish to sort by rank, then suit instead, we
9.3 Using Functions to Compose Behavior 253
need to write an entirely new comparator that repeats most of the code, but with
the order of comparison switched. Moreover, if we want to sort in descending order
instead of ascending order for either rank or suit, we need to yet again rewrite the
code. Ultimately, we have eight options for a basic card comparator: by rank then
suit or suit then rank (two options), where either rank or suit can be ascending or
descending (times four options). To cover all possibilities with factory methods, we
would thus need eight factory methods, and plenty of D UPLICATED C ODE†.
To work with finer abstractions, we could start by offering comparison in both
relative levels (suit, then rank, and rank, then suit) by creating two factories for
single-level comparison (rank or suit) and two additional factories for complete
comparisons, where the two complete orders are composed of the single-level com-
parisons by suit and rank.
public static Comparator<Card> byRankComparator() {
return (card1, card2) ->
card1.getRank().compareTo(card2.getRank());
}
Unfortunately, without extra help, this idea does not mitigate the complexity of
the composite function (and does not even cover the option to reverse the order of
either suit- or rank-based ordering). The way out of this situation is the insight that
if we want to express a solution in terms of first-class functions, we can also use
functions to do the composition. In the case of functions to express comparisons,
the Comparator interface provides many static and default methods intended to
254 9 Functional Design
compose comparison functions out of smaller abstractions. Let us try to rewrite our
solution to the problem of supporting the comparison of cards by either rank, then
suit, or suit, then rank, in ascending or descending order, using these helper methods.
We will proceed bottom up, from the smaller abstractions to the more complex ones.
A first key method is comparing(...). The signature of this method is a bit
complex but, essentially, comparing(...) creates a comparator by building on a
function that extracts a comparable from its input argument. For example, we could
rewrite byRankComparator() as:
public static Comparator<Card> byRankComparator() {
return Comparator.comparing(card -> card.getRank());
}
and similarly for bySuitComparator. How exactly the method comparing works
is explained in the next section. For now, it is sufficient to understand the behavior
intuitively: the argument to the method is itself a function that extracts the value we
want to compare on, and the return value is a comparator structure. The resulting
behavior is identical to the original solution using the direct comparison between
Rank instances.
The second major service available in class Comparator is a method to cascade
comparisons (for example to compare by suit if the rank is the same, or vice versa).
This functionality is provided by the method thenComparing. This method is a
default method called on a comparator that takes as input another comparator for
the same type.5 With thenComparing, we can express our cascaded comparison
more directly:
public static Comparator<Card> byRankThenSuitComparator() {
return byRankComparator().thenComparing(bySuitComparator());
}
We can observe how this code is already much more explicit about the intent of the
computation than the version above, which explicitly does the cascading of com-
parisons. Inversing the comparison levels then becomes a question of inversing the
order of the comparators in the call chain:
public static Comparator<Card> bySuitThenRankComparator() {
return bySuitComparator().thenComparing(byRankComparator());
}
The final step required to complete our solution is to provide a way to reverse
the comparison order, from ascending to descending and vice-versa. For example,
this would mean going from either ace to king or from king to ace for the rank com-
parison (assuming ace is the first card in the sequence, called an ace-low sequence).
To accomplish this without helper methods, we would need to go back to our basic
implementation of comparators and switch the order of the arguments:
public static Comparator<Card> byRankComparatorReversed() {
return (card1, card2) ->
card2.getRank().compareTo(card1.getRank());
}
5 Or a super type, although this eventuality is not covered here.
9.3 Using Functions to Compose Behavior 255
In the code above, the order of the two parameters in the body of the lambda
expression are reversed. Expressing this difference requires a different factory. For-
tunately, it is possible to avoid this D UPLICATED C ODE† thanks to the helper method
reversed(), which creates a new comparator that orders elements using the re-
verse of the order used by the implicit argument of reversed(). We can then use
reversed() to reverse either or both of the comparison levels. For example, to sort
by descending suit, then ascending rank, we create a comparator factory as follows
public static Comparator<Card>
bySuitReversedThenRankComparator() {
return bySuitComparator()
.reversed()
.thenComparing(byRankComparator());
}
At this point, we can express all eight possible comparison orders simply by com-
bining functions with the help of other functions. The resulting code is so straight-
forward to understand that the abstraction benefit gained by encapsulating compara-
tors in a factory method becomes marginal. In the last code fragment above, the
name of the factory method is basically the list of steps directly visible in the func-
tion call chain in the body of the method. For this reason, here it would make sense
to get rid of the factory methods for the comparators.
Because the only part of the Card interface needed to define the comparison be-
havior is already available through the getter methods getSuit() and getRank(),
the factory methods are not strictly necessary. Removing the comparator factories
from the interface of class Card helps mitigate the threats of S PECULATIVE G ENER -
ALITY†, namely to provide services that are never used. With the helper methods
of Comparator, developers will be able to provide compact and explicit definitions
of the desired comparison behavior directly where needed. For example, if a code
location requires sorting cards by descending order of suit, then rank, the following
code could be used:6
List<Card> cards = ...
cards.sort(Comparator.comparing((Card card) -> card.getSuit())
.reversed()
.thenComparing(Comparator.comparing(
(Card card) -> card.getRank())
.reversed()));
6In this context the parameter types must be supplied as part of the lambda expression because the
compiler does not have enough information to infer them.
256 9 Functional Design
Although this code is already explicit, there are three significant ways in which we
can further improve it. First, we can use Java’s static import feature to eliminate the
need to qualify the static methods:
import static java.util.Comparator.comparing;
Finally, we can observe that class Comparator has an overloaded version of then-
Comparing that combines the behavior of comparing and thenComparing by di-
rectly taking a function that returns the value of the key we wish to use for compari-
son. In this case we can move the reversal of the comparison to the final comparator.
Our code can thus be reduced to:
cards.sort(comparing(Card::getSuit)
.thenComparing(Card::getRank)
.reversed());
7 This assumes there are only two colors, which is true in this case and for a standard deck of cards.
9.4 Using Functions to Supply, Consume, and Map Objects 257
In Section 9.1, I presented how first-class functions can be used to parameterize the
behavior of a higher-order function. Another way to think about this design feature
is that first-class functions allow us to specify some processing behavior but to defer
its execution to the point where it is required. In this section I discuss examples
of three common types of deferred processing: supplying an object, consuming an
object, and mapping an object to another object.
Let us start with the problem of defining an implementation of CardSource (in-
troduced in Section 3.1) that can provide an infinite number of cards:
public class InfiniteCardSource implements CardSource {
public Card draw() {
// Return a card.
}
How can we return an infinite number of cards? Clearly, it will not be possible to
initialize the card source with all of the different cards to return, because there will
be an infinity of them. One potential solution is to initialize InfiniteCardSource
with a S TRATEGY that is a card factory. In our context, a card factory is any function
that can return a Card object. In Section 3.7 we saw how to implement this idiom in
pure object-oriented style. I now present a variant of the solution that uses the con-
cepts seen in this chapter. The Java class library conveniently provides a functional
interface to capture the behavior of a method responsible for returning an object:
Supplier<T>. Its method get takes no argument and returns a value of type T, the
type argument of the Supplier<T> interface. In our case we replace the type pa-
rameter with the concrete type Card to yield the function type () → Card . A basic
supplier-based solution would look like this:
public class InfiniteCardSource implements CardSource {
private final Supplier<Card> aCardSupplier;
With this class, we can now easily create various kinds of infinite card sources.
For example, we could define a static method random() on class Card, which re-
turns a random card, and do:
InfiniteCardSource randomCardSource =
new InfiniteCardSource(Card::random);
As another example, we could also create an infinite source of Ace of Hearts
cards:
InfiniteCardSource aceOfHearts =
new InfiniteCardSource(()-> Card.get(Rank.ACE, Suit.HEARTS));
The key insight to observe from this demonstration is that, conceptually, what
we are handling are functions to obtain objects, as opposed to the required objects
themselves. This allows us to defer the execution of the factory method until the
very point where the object is required.
A similar idea can be used to create designs where we wish to parameter-
ize how a certain object is used, or consumed. As an example, we will design a
ConsumingDecorator which executes some parameterized behavior whenever a
card is drawn from a CardSource (see Section 6.4 to review the design of a D EC -
ORATOR ). In this case, we need to parameterize what will happen to the card being
drawn. This requires a function type Card → void , which can be realized by invok-
ing the generic functional interface Consumer<T>, which has the abstract function
accept(T) which returns void.
public class ConsumingDecorator implements CardSource {
private final CardSource aSource;
private final Consumer<Card> aCardConsumer;
When comparing is called, it creates a new function object that binds Card::
getSuit to keyExtractor, but without calling either apply or its delegate getSuit.
8 In consequence, a reference to println can be used when a Consumer is ex-
pected. For example, the default library interface method Iterable#forEach takes a
Consumer as input. Hence, to print all elements in an iterable, we can simply call
forEach(System.out::println) on that iterable.
260 9 Functional Design
That is, comparing uses the function as a building block when creating a new func-
tion. Similarly to how we used suppliers and consumers, above, this indirection is
necessary because the comparison behavior needs to be executed on-demand within
the compare method. Hence, when method compare is called, only then is apply
called, this time twice, once for each card. Because apply delegates to getSuit,
at that point the suit value is obtained from the card and used in the comparison.
Figure 9.1 illustrates the complete sequence.
Fig. 9.1 Sequence of calls for comparing two cards using a comparator created with
Comparator#comparing
Many design patterns rely on polymorphism to enable variation points in the so-
lution. For example, the S TRATEGY pattern relies on polymorphism to allow client
code to dispatch the execution of an algorithm to a dynamically selected variant
(see Section 3.7). Similarly, the O BSERVER pattern relies on polymorphism to allow
a subject to notify observers whose exact nature is also determined at run time (see
Section 8.3). In the original object-oriented description of the design patterns, this
polymorphism is enabled by extending classes and implementing interfaces.
In functional-style design, first-class functions provide a different perspective on
behavior parameterization. Instead of creating objects of different classes and en-
abling polymorphism through a common supertype, we can define families of func-
tions whose type is compatible and invoke them interchangeably. This is possible in
any context, but it is interesting to note that first-class functions allow a re-thinking
of the implementation of design patterns. By way of illustration, in this section I re-
visit the implementation of the S TRATEGY and O BSERVER patterns using a functional
style.
Functional-Style S TRATEGY
In simple cases where strategies are stateless and their interface boils down to a
single method, we can express the abstract strategy as a functional interface. I have
already illustrated this scenario in Section 9.4, by using the Supplier<T> interface
as an abstract strategy for card factories.
As a different illustration, let us consider a context where client code can use
different strategies for selecting a card in a list. Here, concrete strategies are imple-
mentations of method apply of interface Function<List<Card>, Card>, which
becomes the abstract strategy. In our case, method apply takes as input a list of
cards and returns a single card. As an example of a client class for the strategy, we
could have:
public class AutoPlayer {
private Function<List<Card>, Card> aSelectionStrategy;
In this design, the card selection strategy is provided as an argument to the con-
structor when the client AutoPlayer object is created. Because the strategy is a
first-class function, defining it involves defining the behavior of this function at any
convenient point in the code. One option could be to define it on the fly at the loca-
tion where the instance of AutoPlayer is created. For example, a strategy to always
select the first card would be:
AutoPlayer player = new AutoPlayer(cards -> cards.get(0));
This implementation style is very compact, and even perhaps too much so. The
use of the general Function functional interface in this context has two poten-
tial limitations. First, it has low documentation effectiveness. Looking at the type
Function<List<Card>, Card>, all we know is that it can return a Card given a
list of cards. For this reason, any reference to the card selection strategy needs to
be done through well-named variables for the code to remain readable. Here, field
aSelectionStrategy fulfills the requirement. A second problem is that the single
method in the Function interface is also general-purpose, and for this reason can-
not include any context-specific information. In our context, we need to determine
how to handle the case where the list is empty. One possibility would be to redefine
the strategy as Function<List<Card>, Optional<Card>, and somehow remem-
ber that by convention passing an empty list results in an empty optional object.
Another possibility would be to state that the input list must not be empty is a pre-
condition for the strategy. In both cases, it is not clear where one would document
this critical piece of information.
For these reasons, defining an additional functional interface to represent the
strategy will lead to clearer code and a more self-explicit design. The new inter-
face can also be used to hold some standard strategies. The following code shows
an implementation of a S TRATEGY application for selecting cards that uses design
by contract to guard against the case of selecting from an empty list and uses the
Optional type to guard against the case where a strategy yields no card. Although
in this case it would make sense to return Optional.empty() if the input list is also
empty, both options are included for sake of illustration.
9.5 First-Class Functions and Design Patterns 263
Functional-Style O BSERVER
In the O BSERVER pattern, an observable object notifies its observer objects by calling
their callback method(s). In contexts where we can define an abstract observer with
a single callback, we can use functional-style design to create a compact application
of the pattern.
As an example, we will create an ObservableDeck class that is essentially a
version of Deck whose calls to method draw() can be observed. Using the push
264 9 Functional Design
data-flow strategy, we want to notify observers every time a card is drawn from the
deck, letting them know which card was drawn. The functional type for the callback
is thus Card → void . This is exactly the functional type of Consumer<Card>, so
we can use Consumer<Card> as our abstract observer interface:
public class ObservableDeck extends Deck {
private Consumer<Card> aDrawHandler;
To create an observable deck, we instantiate the class with a function that imple-
ments the callback. For a basic logging feature, we could just use println:
ObservableDeck deck = new ObservableDeck(System.out::println);
With this code in place, extending the class to support observer to shuffle()
events would simply be a matter of duplicating the design to support a second ob-
server.
It is interesting to contrast the design of ObservableDeck with the design of the
ConsumingDecorator, presented in Section 9.4. In light of the current discussion,
it should become apparent that the ConsumingDecorator is essentially an observ-
able CardSource. While the ConsumingDecorator used aggregation to attach an
observer, the ObservableDeck used inheritance. The use of the Consumer<Card>
interface, however, fulfills exactly the same role in both designs: to inject additional
behavior that executes in response to an event in the life-cycle of the observable
object.
Up to now, the design ideas presented in this chapter involve introducing functional
elements into an otherwise object-oriented design. In some cases, the design context
motivates solutions that have a much stronger flavor of functional-style program-
ming. One scenario where functional-style programming shines is to structure code
responsible for processing a collection of data. In a way, most of what software does
is to process data, so here we can tighten the definition and consider that functional-
style programming provides good support for design problems that involve applying
transformations to sequences of data elements. An example of data processing that
meets this definition is counting the number of acronyms in a body of text. In this
case, the input is a sequence of words, and the transformations are to filter the input
for acronyms, and then to compute the total number of instances found.
Functional-style design is a good match for this type of data processing because
it naturally calls for the use of behavior parameterization and higher-order functions.
Higher-order functions implement the general data-processing strategies, which are
then parameterized for a particular context with first-class functions. In the text-
processing example above, the general strategy is to check whether each input ele-
ment (a word) matches a certain predicate (acronym or not). Although the general
strategy of filtering over a predicate is likely to apply to many different problems,
the predicate itself (acronym detection) is specific to the particular design context.
In other cases we might want to write code that detects short words, proper nouns,
etc. This idiom can be illustrated by the statement:
data.higherOrderFunction(firstClassFunction);
Data as a Stream
The main concept that enables functional-style data processing in Java and similar
technologies is that of a stream. Simply stated, a stream is a sequence of data el-
ements, a bit like a collection. However, the major conceptual difference between
a stream and a collection is that a collection represents a store of data whereas a
stream represents a flow of data. This distinction is similar to the difference be-
tween storing music as a file vs. playing music via an on-line streaming service. For
software design, the distinction between collections and streams has many practical
implications:
• Elements in a collection have to exist before they are added to the collection, but
elements in a stream can be computed on-demand.
266 9 Functional Design
• Although collections can only store a finite number of elements, streams can
technically be infinite. For example, although it is not possible to define a list
that contains all the even numbers, it is possible to create a stream that produces
this data.
• Collections can be traversed multiple times, but the traversal code is located out-
side the collection, for example in a for loop or iterator class. In contrast, streams
can only be traversed once: their elements are consumed as part of the traversal.
However, the traversal code is hidden within the higher-order functions provided
by the stream’s interface.
• Streams are amenable to being parallelized, mainly because the traversal of their
elements is hidden as part of the stream abstraction.
An additional, more pragmatic, difference relates to the evolution of the Java lan-
guage. Collection classes (List, Set, etc.) were released before the language had
explicit support for first-class functions, so collections provide almost no support for
higher-order functions.9 In contrast, Java 8 provides support for first-class functions
(in the form of method references and lambda expressions) and includes a powerful
Stream API designed to support functional-style design. The remainder of this sec-
tion shows how to design functional-style data processing in Java using the Stream
API.
In Java, a simple way to obtain a stream is to call the stream() method on an
instance of a collection class. For example, if we have a method to return the list
of cards in an instance of the Deck class, we can also stream this data. Figure 9.2
shows the structures involved.
Fig. 9.2 Version of the Deck and CardStack classes with methods for returning the cards they
contain
Obtaining a stream of cards from the deck is then just a matter of calling
stream() or the output of cards():
new Deck().cards().stream();
Streams also support operations that take a stream as their implicit argument and
output a different stream. This process is called pipelining. For example, the sorted
9 The notable exception is the default method forEach available on the Iterable<T> interface
since Java 8.
9.6 Functional-Style Data Processing 267
method returns the elements of the original stream in sorted order. Because method
sorted() requires the instances in the stream to be subtypes of Comparable, the
code below assumes the version of class Card used implements Comparable:
Stream<Card> sortedCards = cards.stream().sorted();
It is also possible to combine multiple streams. For example, to assemble all the
cards from two decks and sort them, we can do:
Stream<Card> cards =
Stream.concat(new Deck().cards().stream(),
new Deck().cards().stream());
To revert to a single deck, one option is to remove the duplicates using distinct():
Stream<Card> withDuplicates =
Stream.concat(new Deck().cards().stream(),
new Deck().cards().stream());
Stream<Card> withoutDuplicates = withDuplicates.distinct();
The main way that streams support functional-style programming is that they define
a number of higher-order functions. A basic higher-order function for streams is
forEach, which applies an input consumer function to all elements of the stream.
For example, to print all cards in a stream in a functional way, we could do the
following:
new Deck().cards().stream()
.forEach(card -> System.out.println(card));
The method forEach takes an argument of type Consumer<? super T>, which
means we can supply it a reference to a function that defines a single parameter of
type Card (or any supertype of Card). In the example above this reference is sup-
plied in the form of a lambda expression, but we could also use a method reference
System.out::println (see Section 9.4). Because forEach is not guaranteed to
respect the order in which elements are encountered in the stream, a second version,
forEachOrdered, can be used if ordering is important. Because it does not return
a stream, the outcome of the forEach function (either variant) cannot be further
transformed as part of a pipeline. Generally speaking, stream functions that do not
produce a stream of results that can be further processed as part of a pipeline are
called terminal operations. Another example of a terminal operation on streams is
the count function seen above.
Other types of terminal higher-order functions that can be applied to streams
include searching functions such as allMatch, anyMatch, and noneMatch, which
268 9 Functional Design
take as argument a predicate on the stream element type and return a Boolean value
that indicates respectively whether all, any, or none of the elements in the stream
evaluate the predicate to true. For example, to determine whether all cards in a list
are in the Clubs suit, we would do:
List<Card> cards = ...;
boolean allClubs = cards.stream()
.allMatch(card -> card.getSuit() == Suit.CLUBS );
Filtering Streams
The sorted() stream function, mentioned above, shows how we can define inter-
mediate operations to create a pipeline of transformations on a stream. An inter-
mediate operation thus has a stream as an implicit argument, and returns a stream.
An important function in this pipelining process is the filter method, which takes
a Predicate and returns a stream that consists of all the elements of the original
stream for which the predicate evaluates to true. For example, assuming we want to
count the face cards in a list of cards:
long numberOfFaceCards = cards.stream()
.filter(card -> card.getRank().ordinal() >=
Rank.JACK.ordinal()).count();
This allows us to use method references and make the functional code self-explana-
tory:
long numberOfFaceCards = cards.stream()
.filter(Card::isFaceCard)
.count();
At first glance, capturing predicates in dedicated methods may seem like an ob-
stacle to the creation of compound predicates. For example, what if we want to
count only face cards in the Clubs suit? Do we have to revert to our original lambda
expression?
long result = cards.stream()
.filter(card -> card.getRank().ordinal() >= Rank.JACK.ordinal()
&& card.getSuit()==Suit.CLUBS).count();
A key insight to avoid ugly code like this is to observe that filters, being an interme-
diate operation, can also be pipelined:
9.6 Functional-Style Data Processing 269
At this point, it should become apparent that our functional-style code is starting
to look much more like a set of high-level rules for processing data than a set of in-
structions telling a program how to operate on inputs. Indeed, one major advantage
of writing data-processing code in a functional style is that the result is more declar-
ative than imperative, and thus better conveys the intent behind the code. Here, for
example, a single glance at the statement shows that we wish to only consider face
cards, then further restrict the data to only consider cards in the Clubs suit, and then
finally count the data elements. It is also worth noting how the code is formatted,
with each stream operation indented and prefixed with its period starting the visible
part of a line. This coding style is a usual convention for formatting stream oper-
ations in Java. Its benefit is that it emphasizes the declarative nature of the code.
There are often situations in data processing where we need to transform all data el-
ements in a stream into a derived value. In this case, we leverage the idea of mapping
objects to their desired value, already introduced in Section 9.4. In functional-style
programming, the word mapping is employed in the mathematical sense synony-
mous to a function. For example, consider how the function that computes the square
of a number x, usually denoted x2 , actually maps a number x to its square x · x.
Many programming languages that support some form of functional-style pro-
cessing provide a mechanism to apply a map (that is, a function) to every element
in a data collection. In Java the Stream class defines a map method that takes as
input a parameter of type Function<? super T, ? extends R>. In other words,
the argument to the map function is another function that takes as input an object
of type T and returns an object of type R.10 This means that the map function will
transform a stream of objects into another stream where every object is obtained by
applying a function to an object in the first stream.
As an example, we can consider a function that maps an instance of Card to an
instance of an enumerated type Color that represents the color of the card’s suit.
We can apply this function systematically to all elements in a stream using the map
method:
cards.stream().map(card -> card.getSuit().getColor() );
stream. For example, to count the number of black cards in a collection, we could
write:
long result = cards.stream()
.map(card -> card.getSuit().getColor() )
.filter( color -> color == Color.BLACK )
.count();
Although the same result can be achieved more directly by using filter with a
lambda expression that retrieves the card’s color, the example above illustrates how
we can use map to unpack an object and use only the part of the object that is of
interest for a given computation.
Mapping, however, can accomplish much more than extracting data from an input
element. Let us consider a second example, where we want to compute the score that
a card represents. In some games, cards are assigned a point value that corresponds
to their rank (for instance, Three of Clubs is worth three points), except for face
cards which are all worth ten points. With a mapping process, we can convert a
stream of card objects into a stream of integers that correspond to the score of each
card in the original stream:
cards.stream()
.map(card -> Math.min(10, card.getRank().ordinal() + 1));
The result of this expression will be a stream of Integer objects that represent the
score of each card. As usual, whether to encapsulate the score computation in an
instance method of class Card is a context-sensitive design decision. If the score
value is used in multiple calling contexts, then it would make sense to add a method
getScore() to the interface of class Card. Otherwise, the lambda expression will
suffice.
When mapping to numerical values, as in this case, it is useful to know that the
language provides specialized support for streams of numbers in the form of classes
such as IntStream and DoubleStream. These types of streams work like other
streams, but they define additional operations that only make sense when processing
numbers, such as summing the elements in the stream. To adapt our scoring example
to get the total score, the code needs to explicitly map to an IntStream, and then
call the sum terminal operation:
int total = cards.stream()
.mapToInt(card -> Math.min(10, card.getRank().ordinal() + 1))
.sum();
How can we operate on all the cards reachable through the list? Streaming the
list with deck.stream() will produce a stream of instances of Deck, not Card. We
need an additional operation to unpack the decks into a stream of cards. We could
try mapping using Deck#cards():
listOfDecks.stream()
.map(deck -> deck.cards().stream())
.forEach(System.out::println);
This, however, will not work because the map function returns a stream of the
return type of its argument function. Because the function type of the argument is
Deck → Stream<Card> , map will return an instance of Stream<Stream<Card>>
when what we want is just Stream<Card>. The requirement to map an object to
multiple (zero or more) objects can instead be handled using a special kind of map-
ping function called a flat map. Conceptually, a flat map operation maps each input
object to a stream, but merges the resulting streams into a single one instead of col-
lecting the streams as individual elements of another stream. In Java, this service
is provided by method flatMap. In our scenario, we would thus use flatMap as
follows:
listOfDecks.stream()
.flatMap(deck -> deck.cards().stream())
.forEach(System.out::println);
Reducing Streams
When working with streams of data, a common scenario is that we want to not only
process each data element, but also do something with the data as a whole. Typically,
this means either:
• Aggregating the effect of the operations into a single result. Terminal operations
such as count() and sum() are good examples of data aggregation in that sense;
• Collecting the individual results of the operations into a stored data structure. In
Java this would typically mean a List or other collection type.
Although they seem different, these alternatives actually have in common that,
conceptually, they represent reducing a stream to a single entity. In the second case,
the entity may be a collection of many elements, but conceptually it is nevertheless
a single, stored structure as opposed to a stream. The advantage of generalizing all
types of data aggregation as a single high-level operation, reduction, is that it intro-
duces a clear distinction between intermediate operations, namely mapping,11 and
In practice, a summing reduction uses 0 as the base case and accumulates re-
sults by iteratively adding elements. The code below shows a simplified mock-up
implementation of the reduce method for IntStream:
public int reduce(int pBase, IntBinaryOperator operator) {
int result = pBase;
Iterator<Integer> iterator = this.iterator();
while( iterator.hasNext() ) {
int number = iterator.next();
result = operator.applyAsInt(result, number);
}
return result;
}
Initially, the result of the reduction operation is set to the base provided, in our
case 0. Then, for each element in the stream, the binary operator provided as input
to reduce is applied, using as arguments the current result and the next element in
the stream. In our case the binary operator was specified using the lambda expres-
sion (a,b) -> a+b. Thus, for each element in the stream, the value of the current
reduction will be assigned to itself plus the value of the next element.
This being said, because most common reduction operations (min, max, count,
sum, etc.) are directly supported by the stream classes, it is possible to get started
with streams and actually get quite far without mastering the art of writing reduc-
tions.
Reductions that serve to accumulate data in a structure are a special case. Let us
say we wish to collect all face cards in a list of cards in a separate list. One quick
solution would be to use the forEach method to store the elements of the stream in
the target list:
List<Card> result = new ArrayList<>();
cards.stream()
.filter(Card::isFaceCard)
.forEach(card -> result.add(card));
Insights 273
In this last example, the details of the implementation of the accumulation of ele-
ments into a list remain hidden, and the code directly expresses the desired intent:
to collect the elements of the stream into a list.
Insights
• When designing methods, keep in mind how they could be used through refer-
ences: ensure they are a good match for likely functional interfaces;
• Compose functions using functions, as opposed to imperative statements;
• Use the helper methods of library types to compose functionality in intuitive
ways;
• Consider using supplier, consumer, and mapping function types to parameterize
behavior;
• Consider defining strategies in the S TRATEGY pattern as functional interfaces;
• Consider the possibility of composing strategies;
• Consider using first-class functions to define abstract observers in the O BSERVER
pattern;
• Structure data-processing code so that it is more declarative than imperative in
style;
• Use the mapping abstract operation to convert data elements into the values that
are directly used by a computation;
• Use collector objects to accumulate the result of stream operations.
Further Reading
Variables store values. In Java, variables are typed and the type of the variable must
be declared before the name of the variable. Java distinguishes between two major
categories of types: primitive types and reference types. Primitive types are used to
represent numbers and Boolean values. Variables of a primitive type store the actual
data that represents the value. When the content of a variable of a primitive type
is assigned to another variable, a copy of the data stored in the initial variable is
created and stored in the destination variable. For example:
int original = 10;
int copy = original;
In this case variable original of the primitive type int (short for integer) is as-
signed the integer literal value 10. In the second assignment, a copy of the value 10
is used to initialize the new variable copy.
Reference types represent more complex arrangements of data as defined by
classes (see Section A.2). The important thing to know about references types is
that a variable of a reference type T stores a reference to an object of type T . Hence,
values of reference types are not the data itself, but a reference to this data. The
main implication is that copying a value means sharing a reference. Arrays are also
reference types. For example:
In this case, copy is assigned the value stored in original. However, because the
value stored in original is a reference to an object of an array type, the copy also
refers to the object created in the first statement. Because, in effect, copy is only a
different name (or alias) for original, modifying an element in copy also modifies
that element in original.
Essentially, an object is a cohesive group of variables that store pieces of data that
correspond to a given abstraction, and methods that apply to this abstraction. For
example, an object to represent the abstraction book could include, among others,
the book’s title, author name, and publication year. In Java, the class is the compile-
time entity that defines how to build objects. For example, the class:
class Book {
String title;
String author;
int year;
}
states that objects intended to represent a book will have three instance variables
named title, author, and year of type String, String, and int, respectively.
In addition to serving as a template for creating objects, classes also define a cor-
responding reference type. Objects are created from classes through a process of
instantiation with the new keyword:
Book book = new Book();
The statement above creates a new instance (object) of class Book and stores
a reference to this object in variable book declared to be of reference type Book.
Instance variables, also known as fields, can be accessed by dereferencing a variable
that stores a reference to the object. The dereferencing operator is the period (.).
For example, to obtain the title of a book stored in a variable book, we do:
String title = book.title;
The effect of declaring a field static means that the field is not associated with
any object. Rather, a single copy of the field is created when the corresponding class
is loaded by the Java virtual machine, and the field exists for the duration of the
program’s execution. Access to static fields can be restricted to only the code of the
class in which it is declared using the access modifier private. If declared to be
public, a static field can be accessed by any code in the application, in which case
it effectively constitutes a global variable. Because it is generally a bad practice to
modify globally-accessible data, global variables are best defined as constants, that
is, values not meant to be changed. Globally-accessible constants are declared with
the modifiers public, static, and final, and typically named using uppercase
letters (see Appendix B).
class Book {
public static final int MIN_PAGES = 50;
...
}
Static fields are accessed in classes other than the class that declares them by
prefixing their name with the name of their declaring class, followed by a period.
For example:
int minNumberOfPages = Book.MIN_PAGES;
A.4 Methods
explicitly list all their parameters in their signature. Method abs(int), declared in
the library class java.lang.Math, is a typical example of a static method. It takes
an integer as an input and returns an integer that is the absolute value of the input
number: no object is involved in this computation. Static methods are declared with
the static modifier:
static int abs(int a) {...}
and called by prefixing the name of the method with the name of the class that
declares the method, for example:
int absolute = Math.abs(-4);
the object referenced by variable book becomes bound to the this pseudo-variable
within the body of getTitle().
A.6 Generic Types 279
Compilation units that declare types such as classes are organized into packages.
Types declared to be in one package can be referenced from code in a different pack-
age using their fully-qualified name. A fully-qualified name consists of the name of
the type in the package prefixed by the package name. For example, class Random of
package java.util is a pseudo-random number generator. Its fully-qualified name
is java.util.Random. Declaring a variable using a fully-qualified name can be
rather verbose:
java.util.Random randomNumberGenerator = new java.util.Random();
For this reason, it is possible to import types from another package using the import
statement at the top of a Java source code file:
import java.util.Random;
This makes it possible to refer to the imported type using its simple name (here
Random) instead of the fully-qualified name. In Java, the import statement is only
a mechanism to avoid having to refer to various program elements using fully-
qualified names. In contrast to other languages, it does not have the effect of making
libraries available that were not already available through their fully-qualified name.
In addition to importing types, Java also makes it possible to import static
fields and methods. For example, instead of referring to the abs method of class
java.util.Math as Math.abs, we can statically import it:
import static java.lang.Math.abs;
In the code fragments in this book, all types referenced in the code are assumed
to be imported. When necessary, the surrounding text will clarify the source of the
imported type.
A type definition can depend on another type. For example, we can consider the
following type OptionalString, which may hold a String (the concept of the
optional type is covered in more detail in Section 4.5):
public class OptionalString {
String object = null;
OptionalString(String object) {
this.object = object;
}
280 A Important Java Programming Concepts
boolean isPresent() {
return object != null;
}
String get() {
return object;
}
}
A class such as this one could, in principle, be used to wrap any other kind of
reference type. For this reason, it is useful to be able to parameterize some of the
types that a class depends on. This concept is supported in Java through generic
types. Generic types are type declarations that include one or more type parameters.
Type parameters are specified in angle brackets after the type name. In the declara-
tion of a type, a type parameter acts as a placeholder for an actual type, which will
be supplied when the generic type is used. Class OptionalString can be rewritten
to work with any reference type by parameterizing the type of the object it holds:
class Optional<T> {
T object = null;
Optional(T object) {
this.object = object;
}
boolean isPresent() {
return object != null;
}
T get() {
return object;
}
}
In the above code, the letter T does not represent an actual type, but a parameter (i.e.,
a placeholder) that is replaced by the actual type when the generic type is used:
Optional<String> myString = new Optional<>();
The type declaration for variable myString includes a type instance String. The
effect of this type parameter instantiation is to replace the type parameter T with
String everywhere in the declaration of Optional<T>. In the corresponding con-
structor call, the argument of the type parameter can be inferred, so an empty set of
angle brackets (<>) need only be provided. This empty set of angle brackets is also
called the diamond operator.
Generic types are used extensively in the library implementations of abstract data
types (see Section A.7). Other features that involve generic types include generic
methods, type bounds, and type wildcards. This book does not delve into the design
of generic types because it is a relatively specialized topic. The content occasionally
uses generic types to elaborate design solutions, but to the extent possible, these are
limited to the basic instantiation of type parameters.
A.8 Exception Handling 281
Many of the examples in this book use library implementations of abstract data types
(list, set, etc.). In Java, this set of classes is commonly referred to as the Collections
framework, and located in the package java.util. Collection classes are generic
(see Section A.6). This means that the type of the elements held in a collection,
such as an ArrayList, is a parameter that is provided when the collection type is
used. For example, the following statement declares and instantiates a list of String
instances:
ArrayList<String> myStrings = new ArrayList<>();
Java provides a way for methods to indicate when they cannot complete normally
through an exception-handling mechanism. In Java, exceptions are objects of a type
that is a subtype of Throwable. To throw an exception, an exception object must be
first created, and then thrown using the throw keyword:
void setMonth(int month) {
if( month < 1 || month > 12)
throw new InvalidDateException();
}
Throwing an exception causes the control flow of the executing code to jump to
a point in the code where the exception can be handled, unwinding the call stack as
it goes. To handle an exception, it is necessary to declare a try block with one or
more catch clauses. A catch clauses declares a variable of an exception type. An
exception raised in or propagated into a try block is caught by the block’s catch
clause if the type of the exception can be legally assigned (through subtyping) to the
exception variable. In this example:
try {
calendar.setMonth(13);
} catch( InvalidDateException e ) {
System.out.println(e.getMessage());
}
the exception would not have been caught, and would have propagated to the previ-
ous enclosing try block in the control flow.
Appendix B
Coding Conventions
Coding conventions are guidelines for organizing the presentation of source code.
Aspects that fall under coding conventions include naming conventions, indenta-
tion, use of spaces, and line length. Following a set of coding conventions can help
improve the readability of the code and prevent some types of errors. Coding con-
ventions can vary from one organization to another because of cultural or practical
reasons (each convention has its advantages and disadvantages).
In this appendix, I highlight of the coding conventions used in this book and
in the example projects (see Appendix C). For an extensive discussion of coding
conventions and why they matter, see Chapters 2, 4, and 5 of the book Clean Code:
A Handbook of Agile Software Craftmanship by Robert C. Martin [7].
As is usual in Java, the identifier names use medial capitalization, also known as
camel case. With medial capitalization, words in a phrase are in lower case and each
new word in the phrase starts with an uppercase letter. Type names start with an up-
percase letter (e.g., ArrayList, HashMap) and method names start with a lowercase
letter (e.g., indexOf, replaceAll). Instance variables (i.e., fields), class variables
(i.e., static fields), and local variable names also follow medial capitalization, but
with a special convention for fields (see below).
Constants (i.e., fields declared static and final) are named in all uppercase letters,
with an underscore separating words (e.g., WINDOW_SIZE).
Field names are prefixed with a lowercase a (for attribute, a synonym for field),
e.g., aData. Method parameter types are camel-cased and prefixed with a lowercase
p (for parameter), e.g., (pData). Local variables are camel-cased and start with a
lowercase letter, without a prefix (e.g., data). The only exception to these guidelines
is for the names of the parameters in lambda expressions, which are named like local
variables. The advantages of this convention are:
• Within a code block, it is always possible to determine what type of variable a
name refers to without having to navigate to the declaration of this variable;
• The convention eliminates the risk of having a local variable hide a field by
reusing the same name;
• The convention eliminates the necessity to use the this keyword to disam-
biguate a field that has the same name as a method or constructor parameter
(e.g., this.data = data;).
The systematic use of prefixes is one of the rare points where I disagree with the
advice of Robert C. Martin (referenced above). He states:
Your classes and functions should be small enough that you don’t need [prefixes]. And you
should be using an editing environment that highlights or colorizes members to make them
distinct. [7, p. 24]
Although desirable, both conditions (small classes and the use of an editor) are
not guaranteed to hold. In particular, viewing code hosted on a website such as
GitHub or reading it in a book means we cannot always rely on dynamic code high-
lighting tools. In a book that makes extensive use of partial code fragments, the
prefixes are also helpful for providing the necessary context for a name.
In Java, code blocks are defined with braces. There are two families of conventions
for structuring code blocks in Java, based on where the opening brace is located.
A first style is to locate the opening brace on the same line as its corresponding
declaration or statement:
String getTitle() {
return title;
}
An alternative style is to position the braces on their own line such that corre-
sponding braces are vertically aligned:
String getTitle()
{
return title;
}
B Coding Conventions 285
In either case, code statements within a block are indented by one unit (typically
four spaces or one tab character) with respect to the statement or declaration that
introduces the block. In the book, I use the same-line variant because it is more
compact and thus amenable to presentation in a book. In the sample code avail-
able on-line, I instead use the vertically-aligned variant because, without the space
constraint, it makes the scope of a code block more salient.
Code Comments
Classes and interfaces should include a Javadoc [9] header comment, along with the
methods they declare. In-line comments are kept to a minimum.
Code in the example applications follows these coding conventions strictly. How-
ever, for code fragments in the chapter content I make various concessions for con-
ciseness.
In particular, code fragments should not be assumed to constitute complete im-
plementations. In most cases, I silently elide parts of the code not essential to the
discussion. When there is a risk of ambiguity, I use an ellipsis (...) to indicate eli-
sion, either in a block of code or in the parameter list of a method signature or the
argument list of a method call.
I also use an indentation tighter than four characters. For one-line methods, I may
also inline the statement and both curly braces. If necessary to avoid a page break in
a code fragment, I place the body of the method on the same line as its signature. I
will also typically not include the comments and @Override annotation. The code
below is a version of the toString() method above with the three adaptations
discussed:
public String toString() { return String.format(...); }
Appendix C
Sample Applications
Reading and trying to understand existing code is an essential part of learning soft-
ware design. The two software projects described below provide sample code in the
form of complete working applications.
Both applications were developed following the principles and techniques pre-
sented in this book. Throughout the chapters, brief sections titled Code Exploration
illustrate how some of the material presented in the chapter is applied in practice. To
maximally benefit from the sample applications, I recommend downloading a local
copy of the code. The Code Exploration sections are indexed with the name of the
application followed by the class where the relevant code can be found. The intent
for this structure is to facilitate diving into code with a minimum of effort by using
the open file shortcut key combination available in most development environments.
The two applications offer distinct levels of challenge in code understanding.
The complete source code and installation and usage instructions can be found on
GitHub at the URLs indicated below.
Solitaire
The first application, Solitaire, implements the card game of the same name. This
application serves as the context for many of the running examples in the book.
It realizes some non-trivial requirements while remaining of overall manageable
complexity. It should thus be possible to understand the general architecture of this
project and many of the detailed design and implementation decisions after a few
months of study. For some of the discussions in the chapters, knowledge of the game
terminology will be useful. Figure C.1 illustrates the layout of a game of Solitaire
in progress and includes overlays to indicate important terms. At the top-left is the
deck of face-down cards. A user draws a card from the deck and places it face up
in the discard pile. The four piles at the top right of the layout are the foundation
piles (these can be empty). Finally, the seven piles of cards that fan downwards are
jointly called the tableau (tableau piles can also be empty). The code discussed in
the book is consistent with Release 1.2.
https://github.com/prmr/Solitaire
JetUML
The second application, JetUML, is the interactive tool used to create all of the UML
diagrams in this book. Although still modest in size compared to many software
applications, it can be considered real production code and its design involves some
decisions that go beyond the material covered in the book. The code discussed is
consistent with Release 3.3.
https://github.com/prmr/JetUML
References
AssertionError 38 visibility of 20
assertThrows method 115 Class class 105–107
class diagram 49, 50, 67
B aggregation in 128
and source code perspective 219
bad smell see design antipattern inheritance in 159
base class see also superclass, 159 class hierarchy 162
abstract vs. non-abstract 177 class literal 106, 107
avoiding restrictions 186 ClassCastException 162
dependency with subclasses 183 ClassNotFoundException 106
downcasting from 161 client code 14
subtyping relation for 178 and use of Optional 78
behavior decoupling from with polymorphism 158
composition 252–256 hiding information from 19
parameterization 244 in design by contract 37
blame assignment 38 role in C OMPOSITE 131
brace 284 role in ISP 63
branch coverage see test coverage sharing references with 26
use of interfaces by 47
C use of S TRATEGY by 58
using null references 34
C++, as supporting multiple inheritance 162 clone method 145, 173
caching clones see code clones
and object equality 84 cloning 173
as bad practice 73 shallow vs. deep 173
callback method 200, 202 to copy internal objects 33
and ISP 204 use in P ROTOTYPE 148
and pull data-flow strategy 204 vs. polymorphic copying 145
as event 205, 206 closure 95
call sequence 201 code clones 159
in library types 204 code comments 285
in V ISITOR 232, 234 and lambda expressions 248
naming 200 as means to capture design rationale 7
parameters for 202 as program metadata 109
return value for 205 in interface definition 45
single vs. multiple 215 use in design by contract 38
supporting push and pull data-flow 204 code obfuscation 2
unused 206, 211 coding conventions 6, 283–285
camel case see medial capitals Collections class 47, 244
cardinality, in class diagrams 50 Collections framework 46, 281
cast operator see downcasting, 161 and equality testing 85
catch clause 281 collections vs. streams 265
for reflective operations 105, 106, 108 collector 272
class see also base class, 16, 276 combinatorial explosion 130
abstract see abstract class C OMMAND design pattern 149–153, 178
anonymous see anonymous class combined with T EMPLATE M ETHOD
as abstraction 4 183–184
final 184 inheritance and 177–180
immutable see immutability comments see code comments
inner see nested class Comparable interface 46–48, 51
interface of 43 Comparator interface 51–53, 58, 94, 95,
local see nested class 244–246, 252, 253
multi-mode 138 with dependency injection 62
to match domain concepts 16 comparing method 254, 259
Index 293
T U