Académique Documents
Professionnel Documents
Culture Documents
Orientador:
Co-Orientador:
Jri:
Presidente:
Vogais:
Julho de 2007
Resumo
O modelo de domnio o elemento central de uma aplicao com objectos moderna.
l que reside o conhecimento que a aplicao tem sobre o domnio do problema. Logo,
medida que se desenvolvem aplicaes para problemas com domnios maiores e mais
complexos, os modelos de domnio tornam-se mais ricos. Infelizmente, a implementao de modelos de domnio ricos utilizando as linguagens de programao com objectos
actuais uma tarefa difcil.
Para simplificar esta tarefa, eu proponho estender as linguagens de programao com
novos construtores que permitam a utilizao de aces atmicas, a especificao da estrutura do modelo de domnio, e a implementao de regras de consistncia. Estes novos
construtores so introduzidos na linguagem Java de uma forma amigvel para o programador, de modo a que os programadores possam us-los sem alteraes significativas no
processo de desenvolvimento.
Para implementar aces atmicas, proponho um novo modelo de Memria Transaccional em Software, e descrevo uma implementao deste modelo em Javaa JVSTM.
Depois, proponho uma nova linguagema Domain Modeling Languageque permite a
especificao das entidades e das relaes existentes num domnio. Finalmente, proponho a utilizao de Predicados de Consistncia que tiram partido da existncia de aces
atmicas para permitir a implementao de regras de consistncia de forma ortogonal
implementao do restante comportamento.
Abstract
The domain model is the central element of a modern object-oriented application. It embodies the applications knowledge about the problem domain. Therefore, as applications
encompass problems with larger and more complex domains, domain models become
both larger and richer than ever. Yet, implementing a rich domain model with current
object-oriented programming languages is a difficult task.
To simplify this task, I propose to extend object-oriented programming languages with
new constructs that allow the use of atomic actions, the specification of a domain models
structure, and the implementation of domain consistency rules. I introduce these new
constructs in the Java programming language in a programmer-friendly way, so that
programmers may use them without major changes in the development process.
To support atomic actions, I propose a new model of Software Transactional Memory
and describe an implementation of this model as a pure Java librarythe JVSTM. Then,
I propose a new languagethe Domain Modeling Languageto allow the specification of
the entities and the relationships between entities of a domain. Finally, I propose the
use of Consistency Predicates that build on the support for atomic actions to allow the
implementation of consistency rules orthogonally to the implementation of the remaining
behavior.
Palavras-chave
Programao Concorrente com Objectos
Memria Transaccional em Software
Programas de Domnios Complexos
Modelao de Domnio
Predicados de Consistncia
Arquitecturas de Software
Keywords
Concurrent Object-Oriented Programming
Software Transactional Memory
Domain-Intensive Applications
Domain Modeling
Consistency Predicates
Software Architectures
Acknowledgments
No man is an island. . . is a famous quotation from John Donne, and it is so for a
good reason. We all have others with whom we work, with whom we talk, with whom we
despair, with whom we laugh, and with whom we dream. All of these people, one way or
another, influence what we do and who we are.
The work leading to this dissertation was a long journey that would not have been
possible without the help of many. To thank them all, I have written the most inspired
acknowledgments of all time, which, unfortunately, the margins of these pages are too
small to contain. So, instead, I shall give a much paler alternative that does not do justice
to all the help that I got.
First and foremost, I would like to thank Professor Antnio Rito da Silva, my adviser
and my mentor, who went much beyond the call of duty in his advising work. The completion of this work owes much to his enthusiastic encouragement, continuous guidance,
and permanent availability. Moreover, any coherence that you may find in this dissertation is a direct consequence of his unique ability to rise up above the trees and see the
forest.
I would like to thank, also, Professor Joo Pavo Martins, my co-adviser, both for
having initiated me into the arts of scientific research, and for his support throughout my
work.
I am grateful to INESC-ID, and specially to its Software Engineering Group, for providing me not only the means to do my work, but also an healthy and enjoyable environment
to work in. I am deeply indebted, also, to all the ESWs members, and, in particular, to
my colleague and friend Antnio Leito, for all the numerous discussions that helped me
to shed light into the more obscure parts of this work. I owe to Antnio, also, much of my
knowledge on the art of programming and on programming languages.
This work would not have been possible without the support of CIIST, and, specially,
of the Fnix team. I am grateful to them for their enthusiasm in applying my work to the
Fnix system. I would never had started this work in the first place if Fnix did not exist.
I am specially grateful to Eng. Lus Cruz for all his support and insightful discussions
during my work. It was a pleasure to work with him.
Julho de 2007
Joo Manuel Pinheiro Cachopo
Contents
1 Introduction
. . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.4 Notation
11
. . . . . . . . . . . .
12
12
13
14
15
17
18
18
19
20
ii
CONTENTS
22
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
23
26
26
27
2.5 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
28
31
31
32
35
39
42
43
46
50
. . .
51
. . . . . . . . . .
52
3.5 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
54
57
58
58
59
60
61
62
62
CONTENTS
iii
64
65
67
70
71
. . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
73
74
79
80
82
85
87
. . . . . . . . . . . . . . . . .
88
95
4.6 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
97
99
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
iv
CONTENTS
. . . . . . . . . . . . . . . . . . . 119
. . . . . . . . . . . . . . . . . . . . . . . 124
. . . . . . . . . . . . . . . . . . . . . . . 125
6 Consistency Predicates
141
. . . . . . . . . . . . . . . . . . 144
. . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
CONTENTS
7 Validation
163
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
7.1.3.1 Implementation of the Fnix Domain Model with the DML . . 166
7.1.3.2 Other Benefits of Using the DML . . . . . . . . . . . . . . . . 168
7.1.3.3 The JVSTM in the Fnix System . . . . . . . . . . . . . . . . 169
7.1.4 The Fnix Transactional Workload
. . . . . . . . . . . . . . . . . . . 171
. . . . . . . . . . . . . . . . . . 180
. . . . . . . . . . . . . . . . 181
. . . . . . . . . . . . . 188
. . . . . . . . . . . . . . . . 192
vi
CONTENTS
8 Conclusions
203
Bibliography
207
List of Figures
16
17
22
24
3.1 The possible execution of two concurrent calls to the method deposit for
the same Account instance. . . . . . . . . . . . . . . . . . . . . . . . . . .
33
3.2 The possible execution of two concurrent calls to the method deposit for
the same Account instance. . . . . . . . . . . . . . . . . . . . . . . . . . .
34
3.3 The UML class diagram with the relationship between the class Bank and the
class Account: A bank can have many accounts, but an account belongs
to exactly one bank. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
35
3.4 Execution of the method transfer during the execution of the method
totalBalance. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
36
3.5 Execution of the method transfer during the execution of the method
totalBalance. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
36
3.6 Deadlock caused by the concurrent execution of two calls to the method
. . . .
38
. . . . . . . . . . . . . .
39
3.8 Implementation-level class diagram for part of the banking applications domain model. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
44
viii
LIST OF FIGURES
. . . . . . . . . . . . . . . . .
63
68
69
70
. . . . . . . . . . . . . . . . . . . . . . . .
4.5 Parallel deposits on the same account using the STM model based on versioned boxes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
71
4.6 Execution of the method transfer during the execution of the method
72
4.7 Structure that represents a versioned box with three values in its history. .
81
84
85
86
88
5.1 The effect that the DML has on the tool chain typically used for Java application development. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
5.2 The result of compiling an entity type in DML.
. . . . . . . . . . . . . . . . 109
LIST OF FIGURES
ix
6.1 UML class diagram for two classes with a bidirectional association between
them. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150
6.2 UML class diagram showing the central elements for the implementation of
consistency predicates in the JVSTM. . . . . . . . . . . . . . . . . . . . . . 157
7.1 Evolution of the number of classes and associations in the Fnix domain
model.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
7.2 Total transactions successfully processed by the Fnix web application from
October 2006 to June 2007.
. . . . . . . . . . . . . . . . . . . . . . . . . . 172
7.3 Total daily transactions successfully processed by the Fnix web application
on February 2007. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
7.4 Total hourly transactions successfully processed by the Fnix web application on the first day of enrollments, the 17th of February 2007. . . . . . . . 174
7.5 Average daily number of transactions successfully processed by the Fnix
web application for each day of the week from October 2006 to June 2007.
174
. . . . . . . . . . . . . . 175
7.7 Monthly total of read transactions, write transactions, and conflicts in the
Fnix web application from October 2006 to June 2007. . . . . . . . . . . . 176
7.8 Hourly average of read transactions, write transactions, and conflicts in the
Fnix web application from October 2006 to June 2007. . . . . . . . . . . . 176
7.9 Hourly total of read transactions, write transactions, and conflicts in the
Fnix web application on the first day of enrollments, the 17th of February
2007. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177
7.10 Transactions per second processed by each method for the List Benchmark
with 100% of updates. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183
7.11 Transactions per second processed by each method for the List Benchmark
with 50% of updates.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183
7.12 Transactions per second processed by each method for the List Benchmark
with 10% of updates.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184
7.13 Transactions per second processed by each method for the List Benchmark
with 0% of updates. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184
LIST OF FIGURES
7.14 Transactions per second processed by each method for the Red-Black Tree
Benchmark with 100% of updates. . . . . . . . . . . . . . . . . . . . . . . . 186
7.15 Transactions per second processed by each method for the Red-Black Tree
Benchmark with 50% of updates.
. . . . . . . . . . . . . . . . . . . . . . . 187
7.16 Transactions per second processed by each method for the Red-Black Tree
Benchmark with 10% of updates.
. . . . . . . . . . . . . . . . . . . . . . . 187
7.17 Transactions per second processed by each method for the Red-Black Tree
Benchmark with 0% of updates. . . . . . . . . . . . . . . . . . . . . . . . . 188
7.18 Transactions per second processed by each method for the Skip List Benchmark with 100% of updates.
. . . . . . . . . . . . . . . . . . . . . . . . . . 190
7.19 Transactions per second processed by each method for the Skip List Benchmark with 50% of updates. . . . . . . . . . . . . . . . . . . . . . . . . . . . 190
7.20 Transactions per second processed by each method for the Skip List Benchmark with 10% of updates. . . . . . . . . . . . . . . . . . . . . . . . . . . . 191
7.21 Transactions per second processed by each method for the Skip List Benchmark with 0% of updates. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 191
7.22 Operations per second processed by each synchronization strategy for the
STMBench7 benchmark with all the long traversals disabled and a readdominated workload.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195
7.23 Operations per second processed by each synchronization strategy for the
STMBench7 benchmark with all the long traversals disabled and a read-write
workload. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196
7.24 Operations per second processed by each synchronization strategy for the
STMBench7 benchmark with all the long traversals disabled and a writedominated workload.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196
7.25 Operations per second processed by each synchronization strategy for the
STMBench7 benchmark with all the read-write long traversals disabled and
a read-dominated workload.
. . . . . . . . . . . . . . . . . . . . . . . . . . 198
7.26 Operations per second processed by each synchronization strategy for the
STMBench7 benchmark with all the read-write long traversals disabled and
a read-write workload. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199
7.27 Operations per second processed by each synchronization strategy for the
STMBench7 benchmark with all the read-write long traversals disabled and
a write-dominated workload. . . . . . . . . . . . . . . . . . . . . . . . . . . 199
List of Tables
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170
7.4 Number of boxes accessed by each type of transaction by the Fnix web
application. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178
7.5 Number of large transactions, for each type of transaction, in the Fnix web
application. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178
7.6 The results for the List Benchmark with 100% of updates. . . . . . . . . . . 182
7.7 The results for the List Benchmark with 50% of updates.
. . . . . . . . . . 182
7.8 The results for the List Benchmark with 10% of updates.
. . . . . . . . . . 182
7.9 The results for the List Benchmark with 0% of updates. . . . . . . . . . . . 182
7.10 The results for the Red-Black Tree Benchmark with 100% of updates. . . . 185
7.11 The results for the Red-Black Tree Benchmark with 50% of updates. . . . . 186
7.12 The results for the Red-Black Tree Benchmark with 10% of updates. . . . . 186
7.13 The results for the Red-Black Tree Benchmark with 0% of updates. . . . . . 186
7.14 The results for the Skip List Benchmark with 100% of updates. . . . . . . . 189
7.15 The results for the Skip List Benchmark with 50% of updates. . . . . . . . . 189
7.16 The results for the Skip List Benchmark with 10% of updates. . . . . . . . . 189
7.17 The results for the Skip List Benchmark with 0% of updates. . . . . . . . . 189
xii
LIST OF TABLES
7.18 The results of the STMBench7 benchmark with all the long traversals disabled and a read-dominated workload. . . . . . . . . . . . . . . . . . . . . . 195
7.19 The results of the STMBench7 benchmark with all the long traversals disabled and a read-write workload. . . . . . . . . . . . . . . . . . . . . . . . . 195
7.20 The results of the STMBench7 benchmark with all the long traversals disabled and a write-dominated workload. . . . . . . . . . . . . . . . . . . . . 195
7.21 The results of the STMBench7 benchmark with all the read-write long traversals disabled and a read-dominated workload.
. . . . . . . . . . . . . . . . 197
7.22 The results of the STMBench7 benchmark with all the read-write long traversals disabled and a read-write workload. . . . . . . . . . . . . . . . . . . . . 197
7.23 The results of the STMBench7 benchmark with all the read-write long traversals disabled and a write-dominated workload. . . . . . . . . . . . . . . . . 198
7.24 Maximum latency results for read-only short traversals and short operations
with all the operations enabled and a read-dominated workload.
. . . . . . 200
7.25 Maximum latency results for read-only short traversals and short operations
with all the operations enabled and a read-write workload. . . . . . . . . . . 200
7.26 Maximum latency results for read-only short traversals and short operations
with all the operations enabled and a write-dominated workload. . . . . . . 201
List of Listings
3.1 A non-thread-safe class Account in Java with the basic getBalance,
32
33
3.3 The implementation in Java, without any concerns for thread-safety, of the
class Bank with the two methods transfer and totalBalance. . . . . .
35
3.4 An implementation of the class Bank that uses fine-grained locks on the
accounts accessed by each method. . . . . . . . . . . . . . . . . . . . . . .
38
3.5 Reimplementation of the methods withdraw and deposit, for the class
40
. . . . . . . . . . . . . . . .
41
42
45
47
BankAssets. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
49
50
3.12 Generic implementation of the methods deposit and withdraw for the
class Account. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
51
52
3.14 Checking the clients total balance before the withdrawal is performed. . . .
53
3.15 Checking the clients total balance after the withdrawal is performed. . . . .
53
74
75
76
76
4.5 Changes needed in the class Account to use a VBox to hold the Accounts
balance.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
77
xiv
LIST OF LISTINGS
78
4.7 Use of the annotation Atomic to make the method deposit atomic.
. . .
79
81
. . . . . . . . . . . . . . . .
82
91
4.11 The operation used during the start of a new transaction to find the transactions number. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
93
94
. . . . . . . . . 129
. . . . . . . . . . . . . . . . . 134
6.1 Ensuring that a client has always at least an active checking account. . . . 148
6.2 Implementation of the constraints for closed accounts. . . . . . . . . . . . . 149
6.3 Methods implementing the consistency predicates for the classes A and B. . 151
6.4 Consistency predicate for checking that the client total balance is not negative.153
6.5 Consistency predicate for checking that a client always have a non-closed
checking account. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
6.6 Consistency predicate for checking that a closed ClientAccount has no money.153
6.7 Consistency predicate for checking that a SavingsAccount with no money
must be closed.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154
Chapter 1
Introduction
The research work that I describe in this dissertation is concerned with the development of
domain-intensive object-oriented applications. In particular, I concentrate on the implementation of the domain model of such applications. Simply put, the main contribution
of this work is the simplification, in a programmers friendly way, of the implementation
of robust and complex domain models.
In this introductory chapter, I start with a characterization of what I call domainintensive applications. Then, I discuss the general approach that guides me through my
research work. These two elements are essential to present my thesis statement, which
I shall do next, followed by the description of how I intend to validate the thesis. Then,
I introduce some basic notation and terminology. Finally, I present the outline of the
dissertation.
1.1
Domain-Intensive Applications
Introduction
Data-intensive
applications
Complexity of domain logic
Domain-intensive
applications
Figure 1.1: The continuous space from data-intensive to domain-intensive applications. We move from data-intensive applications, on the left, to domain-intensive
applications, on the right, as we add more complexity into the domain logic of the
application.
enterprises deal with large volumes of data during their daily operations. Typical examples
of the early days are banks, insurance companies, or any company with many clients,
suppliers, or employees. Unlike the numeric-intensive applications, the first applications
developed for these enterprises do not perform complex numeric computations. Instead,
they store, retrieve, and update the data that represent the various entities relevant to
the enterprises business processes: For instance, the data about each of the enterprises
clients. Because of this emphasis on the storage and retrieval of large volumes of data,
these applications are typically classified as data-intensive. The special needs of dataintensive applications are reflected in the programming language of choice, for many
years, for this kind of applicationsCOBOLand in the later development of database
management systems.
Domain-intensive applications are a natural evolution of data-intensive applications.
Like in data-intensive applications, in domain-intensive applications there are many different types of entities to deal with. In domain-intensive applications, however, the emphasis is put on the domain logic that governs the behavior of and the interactions among
these entities, rather than on the volume of entities processed. Obviously, given this definition, there is no clear-cut separation between data-intensive and domain-intensive
applications. Rather, they form a continuous space of applications, as depicted in Figure 1.1: We move from data-intensive applications to domain-intensive applications as
we add more domain logic into the application.
This trend towards more complex domain logic in the applications evolved alongside
the development of the object-oriented paradigm, which have come to displace COBOL in
the development of new enterprise-grade applications. In fact, object-oriented design and
object-oriented programming languages matured over the last two decades, and are now
the de facto standard for most of the industrial software development.
Yet, even though I firmly believe that object-orientation is the approach that most
effectively copes with the complexity of current applications, I argue that current objectoriented programming languages are still limited in how they support the implementation
of rich and complex domain models.
Thus, the central subject of this dissertation is the development of domain-intensive
object-oriented applications. More specifically, I concentrate on the implementation of
one special element of domain-intensive applicationstheir domain model.
The domain model of an application embodies all the knowledge about the domain
of the problem that the application is meant to solve; it plays, therefore, a key role
on a domain-intensive application. The importance of domain models (and thereby the
relevance of this work) is reinforced by the recent surge of interest directed towards the
approach of Domain-Driven Design and Development [Evans, 2003].
1.2
In the previous section, I described the context of my research work and justified its
relevance. In this section, I shall describe the general approach that guides my work.
Knowing which approach I followed is important because the approach constrains the
solutions that can be developed. In fact, more than constraining the solutions, the approach that I describe here shapes them. So, by justifying the underlying approach of my
work, I justify the path and many of the options I took in the development of that work.
1.2.1
General Approach
The key idea is that I should strive to propose solutions that are compatible with the
way programmers currently work. The pursuit of this goal is essential, if I intend that a
significant number of programmers1 ever uses the results of this dissertation.
Programmers resist to revolutions, notwithstanding the merit of the newly proposed
approaches. And, in most cases, I agree that their resistance is the most rational approach. Programmers invest years of their lives in education, training, and practice to
become proficient with a given set of knowledge, technologies, and tools. Revolutions
break with most of all this accumulated knowledge, forcing programmers to start it all
over again. Naturally, I believe that revolutions are necessary for the advance of science
and of human knowledge. Yet, good revolutions are hard to find and, when found, are
difficult to bring into common use.
In this dissertation I am not aiming at a revolution in the way of programming com1
Introduction
puter applications. Instead, I propose a set of small evolutionary steps on how programming is currently done. I take a more pragmatic stancean engineering approach, if you
willto the problem of improving the current state-of-the-practice on software development.
Likewise, I recognize the value of the enormous amount of effort put into the research
and development of current programming languages, tools, development environments,
and methodologies. So, instead of trying to replace any of these, I intend to leverage on
all of this work by developing solutions that integrate as seamlessly as possible with it.
1.2.2
Guiding Principles
The fundamental tenet of the approach I followed to develop this work is that my proposals should be programmer friendly, in the sense that I should make things easier for
programmers, whilst giving them more powerful constructs. To accomplish this goal, I
established some guiding principles, which I describe next.
As a matter of fact, although not described in this dissertation, part of this work was also implemented
in ANSI Common Lisp [ANSI and ITIC, 1996].
Introduction
is not much to specify. But, if we need more complex behavior, then we can use optional
configuration options to accomplish that behavior.
Additionally, the difficulty in learning a new construct depends on how well the new
construct integrates with the remaining constructs, either new or already existing. For
instance, if a new construct replaces existing constructsor provides an alternative way of
doing somethingthen it should not break the programmers expectations on how things
work. The key idea here is that, to have simple to use and to learn constructs, I should
avoid surprises. The new constructs semantics should be natural; the semantics should
be what a programmer expects, given her knowledge from the language or other languages
where a similar construct exists.
Finally, the simplicity of use of a construct is related, not only with its use when
developing new code, but also with its effect on other development activities, such as
refactoring and maintenance.
This principle, unlike all the previous, aims primarily the concerns of whoever implements the constructs proposed here. As explained above, I implemented all my proposals
without extending the language. Nevertheless, I expect that, once tested and proved
useful, some of the extensions that I propose become incorporated in the programming
language. Therefore, they will have to be reimplemented in that new context.
Typically, more powerful constructs are more complex to implement, more costly in
terms of computational resources, or both. On one hand, if a construct is difficult to
implement, then tool suppliers will resist to implement that construct. On the other hand,
if the implementation of a construct is computationally inefficient, then programmers will
avoid the construct.
Therefore, new constructs should seek a compromise between the power they provide
and the effort needed to support them. In general, it is preferable to have less ambitious
constructs that, still, provide good value to their users, but that, at the same time, are
simple to implement.
Note, however, that, despite this principle, I will not restrain myself from presenting
new constructs that I find useful just because they are difficult to implement or their
implementation is computationally inefficient. Rather, I use this principle as a guide to
find the best compromises.
1.3
Thesis Statement
This dissertations thesis is that it is possible to simplify significantly the task of implementing an object-oriented domain model by making a small, non-disruptive, and easy
to implement set of additions to the current object-oriented programming languages.
Namely, that it is possible to achieve that goal by adding the following to a language:
Atomic actions with support for concurrency control and failure recovery
A declarative language for specifying the structural aspects of a domain model
Consistency predicates that validate an atomic action at commit-time
To validate this thesis, I give specific proposals in the dissertation for adding each of
these elements to the Java programming language. I describe in detail how these new
constructs integrate with the Java language and demonstrate, by comparison with the
current best-practices in implementing a domain model, what are the benefits of these
new constructs for the implementation task.
To show that the addition of these new elements may be made with small changes at
the programming language level, the specific proposals made in this dissertation have a
minimal interface, thereby requiring minimal learning effort from the programmers that
intend to start using them.
To demonstrate that such additions do not necessarily entail a disruptive change in
how programmers work, the proposals made in this dissertation integrate seamlessly both
with the Java programming language, and with the existing software development tools
and processes.
To demonstrate that these new constructs are easy to implement, I implemented all of
them and I give, for each of the proposals made, a detailed description of its implementation in this dissertation.
One of the additional benefits of implementing all the constructs is that, having a
practical implementation for all of them allowed me the opportunity to further validate
their effectiveness, by introducing their use in the development of a real-world large web
application. This web application is developed by a medium-sized team of programmers
and the readily adoption of the new constructs in the development of the application
demonstrates both the usefulness of the new constructs and their ease of use.
Furthermore, having these proposals deployed in a real-world environment for several
years gave me, not only the necessary feedback to make improvements, but also the possibility of collecting statistics about the workload of a typical domain-intensive application.
I report on these statistics to demonstrate that they confirm the assumptions that I used
in the design of some of the proposals of this dissertation.
Introduction
result returned by
methodCall
method m1
waiting on lock
horizontal bar
represents the
execution of
o1.m1(arg)
method called
within m1
methodCall(...) result
parallel execution of
o1.m1(arg)
o1.m1(arg)
and o2.m2()
o2.m2()
result
methodInstruction
method call
that starts the
execution of m2
Time
t1
o1 20
o2 "Hello"
instruction
executed by
m2 at this time
objects state
at instant t1
result returned
by o2.m2()
Figure 1.2: Graphical notation used to illustrate the concurrent execution of methods. Time progresses from left to right, and each horizontal bar represents the execution of a method. When I need to refer to method calls within another method, I
use a smaller horizontal bar super imposed on the bar of the calling method.
1.4
Notation
Throughout this dissertation, I will need to show source code that implements some part
of an application. I shall use always the same language for that: Java.
The code shown, however, will seldom be a complete Java class: To simplify the
presentation, I will show only the fragments of code that illustrate what is being discussed
at the time; it may be a class with just a few of its members, a single method, or even only
part of a method. To make clear that something is missing, I will often use the sequence
... in place of the code that is not relevant to the understanding of the fragment
shown.
I omit, also, the usual Java access control modifiers when their presence is not relevant
to the discussion.
Besides Java code, I will also need to discuss the (eventually parallel) execution of
Java programs. To that end, I use the graphical notation shown in Figure 1.2 on the
preceding page.
1.5
Introduction. The first chapter establishes what is the subject of concern of this
dissertation. Specifically, it introduces the class of domain-intensive applications
and their implementation as the target of the work proposed here. Furthermore, it
describes a set of guiding principles that influence significantly the type of solutions
presented in the dissertation, and presents the thesis statement.
Motivation, Problem Statement, and Approach. Chapter 2 expands on the motivation
given in the first chapter and situates this dissertations work into the larger contexts
of software engineering and software development processes. It presents the domain
model as a central element in the development of a domain-intensive application and
introduces an extended example of a banking domain model, which is then used
throughout the remaining of the dissertation. Finally, it identifies more precisely
the problem that this dissertation addresses and describes the general approach
that was followed to solve the problems identified.
The Difficulties of Implementing a Domain Model. To illustrate the problems described
generically in Chapter 2, this chapter goes through the implementation of part of
the rich domain model introduced there, concentrating on the implementation problems for which this dissertation proposes solutions. In particular, it discusses the
difficulties of: implementing a domain model that is safe in a concurrent execution environment, implement the associations that are part of a domain models
structure, and implementing the domain models constraints.
Versioned Software Transactional Memory. Chapter 4 proposes the use of a Software
Transactional Memory as an enabling technology that allows a rather different way of
implementing a domain model. It describes a novel Software Transactional Memory
that was designed specifically for the needs of a domain-intensive application, and
describes a practical and robust implementation of the system proposed.
Domain Modeling Language. Chapter 5 proposes a new declarative language for the
implementation of a domain models structure in an object-oriented programming
language. It describes the syntax and the semantics of the language in detail,
and describes how a domain models structure described in that language may
be transformed into a conventional object-oriented programming language such as
10
Introduction
Java. For that implementation, it proposes a novel pattern for the implementation
of bidirectional associations.
Consistency Predicates. Chapter 6 expands on the examples of the banking domain
model to introduce a set of development problems that are raised by the current
way of implementing domain constraints. To solve those problems, this chapter
proposes a new programming construct that allow a much simpler implementation
of domain constraints. Finally, it describes how that new construct may be effectively implemented by leveraging on the support for atomic actions given by the use
of a Software Transactional Memory.
Validation. To validate the applicability of the work proposed in this dissertation,
Chapter 7 describes how it was used in the development of a large real-world web
application. Also, it compares the performance of the Software Transactional Memory described in Chapter 4 in a series of benchmarks available in that area.
Conclusions. Chapter 8 summarizes the main contributions of this dissertation and
discusses which new areas of research are opened by some of its proposals.
Chapter 2
context of this work is already well-established, I shall often use the term application,
12
2.1
The development of software may range from single-person small programs that comprise
just a few lines of code, to ultra-large-scale software systems with millions of lines of code
developed by large teams of people.
Whereas in the first case it may be relatively simple to develop the program in a completely ad-hoc way, in the latter case, the mere dimension of the system introduces problems in the management of the development process that are very hard to solve [Gabriel,
Northrop, Schmidt, and Sullivan, 2006; Polak, 2006].
In general, to be able to develop with success a non-trivial application, it becomes crucial to have some way to tackle the complexity of the software development process [Royce,
1987]; in a very general sense, this is the subject of the software engineering discipline.
The software engineering discipline comprises many different subareas of expertise
from requirements engineering to software maintenance, going through project planning,
domain analysis, or software testing, to name just a few. Each of these areas of expertise
addresses only part of the entire software development process.
2.1.1
One way to look at the software development process is as a series of activities that either
create new artifacts from scratch or that transform previously built artifacts into other
artifacts. For instance, the activity of requirements elicitation typically produces one or
more documents detailing the requirements that describe the problem that the program
should solve; then, from these requirements, other activities produce other artifacts, such
as: a plan for the execution of the project, a set of documents describing the software
architecture of the system, or the source code of the program that solves the problem.
The exact nature of the activities and the artifacts of a software development process
may vary considerably from one process to another. They depend not only on the complexity of the system, but also on the methodology and on the development process model
that is adopted by whoever is developing the system.
Naturally, some of the activities are simpler than others. In some cases an activity
may be performed automatically by some tool, but in most cases they require hard human
labor and high expertise. The high complexity of software development results, precisely,
from the combined difficulty of performing each of its activities. Thus, a contribution
that simplifies one of the activities simplifies, also, the software development process as
a whole.
The work described in this dissertation addresses a very specific activitythe transformation of a domain model into the source code that implements it. This activity is
commonly called implementation or coding. It is, also, an activity that is always present in
a software development process, regardless of the methodology or process model chosen.
In fact, the only artifact that a software development process must always produce is
a computer program (or set of programs) that may be executed on a computer; it is the
execution of this program that solves the problem that led to the software development
process in the first place.
2.1.2
A domain model, like any model, is a representation of (part of) the real thing; in this
case, a representation of an applications domain. The domain of an application is the
sphere of knowledge comprising all the entities and the processes of the real world that
is the subject area of that application. A domain model is an abstraction that represents
only the parts of the applications domain that are relevant to the development of the
application.
A domain model may take many forms and may serve many purposes, but it is indispensable in the development of an application. Even if it only exists in the mind of the
person that is developing the application: After all, that person would not be able to write
the application if she had no knowledge about the applications domain.
Indeed, for many small programs, the domain model only exists in the mind of the
developer, and is never put into a more explicit and available form (other than the program
implementing it). Yet, for larger software systems, involving more people, it becomes
infeasible not to have the domain model explicitly represented. It may take the form
of a document in natural language, a formal representation using some kind of formal
language, or some combination of these. In fact, it is common, nowadays, to use the
UML language [Booch, Rumbaugh, and Jacobson, 1999] to represent parts of the domain
model and complement it with natural language descriptions.
The domain model is a useful artifact for most of the activities in a software development process. Its construction starts with the activity of requirements elicitation, evolves
through the analysis and the design phases, and influences such late phases in the
process as the quality assurance, and the system maintenance.
The domain model plays, therefore, a central role in the development of any software
system. Yet, despite its importance, only in the late eighties did the research community
of software engineering started to address the activity of domain modeling as a research
13
14
2.1.3
In this dissertation I do not address the task of building a domain model. Rather, I assume
that it is already built using standard practices in the area. Once it is built, however, it
needs to be expressed in softwarethat is, implemented in a program that may execute to
solve the applications problem. As mentioned above, the target of the research described
in this dissertation is precisely this transformation from a domain model to source code.
This is an area where improvements are particularly welcomed, because, in general,
the implementation is the most labor-intensive activity during the development of software. Not only because the program is the most detailed artifact of the entire software
development process, but also because implementing a domain model is not just a matter
of filling in the missing details; rather, it involves the conversion of the concepts of the
domain model, which are expressed using the constructs of a particular domain modeling
language, into semantically equivalent concepts that are expressed using the constructs
available at the programming language level. Thus, the difficulty of implementing a domain model is greater when there is a greater mismatch between the modeling language
and the programming language.
Traditionally, given the wide gap between these two languages, the common advice
in the software engineering area has been to consider intermediate artifacts to facilitate
this transformation. So, instead of implementing the domain modelwhich is primarily
an artifact of the analysis phasethe traditional software engineering best-practices prescribe the construction of an intermediate design model. The goal of the design model
is to bring the concepts of the domain model closer to the constructs available in the
programming language, thereby facilitating the domain models implementation.
Unfortunately, having yet another language to represent the design model compli-
15
cates the software development process and increases the probability of translation errors
among the different models. To solve this problem, recent research in the software engineering area propose to reduce the distance between the analysis model (also called
conceptual model) and the design model by using a single modeling language for both
models (see, for instance, [Evermann and Wand, 2005]). In fact, one of the goals of the
model-driven design approach is to merge the analysis and the design models into only
one model [Evans, 2003, Chapter 3].
As the difference between these two types of models is not relevant for what I present
in this dissertation, I will not distinguish between both types of models in the remaining
of this document. Instead, I shall use the term domain model to refer to the artifact from
which software developers start their implementation activity. Moreover, I shall refer
generically to the task of passing from the problem description to a domain model as the
design task, rather than talk about analysis and design separately.
Finally, even though my contributions are at the programming language level and are,
therefore, largely independent of the language chosen to represent the domain model, I
shall use only UML class diagrams and natural language descriptions to represent domain
models.
2.1.4
To build an application and have it work as expected, we have to consider several other
aspects, other than the implementation of its domain model:
Rather, they
are usually represented in artifacts such as use cases, presentation models, and
navigational models, from which software developers implement the corresponding
code in the application.
System qualities: To satisfy architectural requirements such as security, scalability,
performance, or availability, an application must include code that implements the
tactics necessary to achieve those qualities.
Technological environment: Applications run in some execution environment and
must, therefore, have code that implements the interfacing with that environment.
For instance, code that accesses a database or that handles network requests.
Obviously, all these elements must be put together with the code that implements the
domain model to make a complete application. Whether the code that implements these
16
Presentation
Domain
Data Source
Figure 2.1: The layered architecture from [Fowler, 2002]. This is a pure layered
architecture, where each layer (represented by a box) may use only the layer immediately beneath it.
various concerns is intermingled with one another or separated in its own module, is a
matter of software architecture.
The advantages of modularization are well-known in the area of software development [Parnas, 1972]. One of those advantages is the ability to separate by different
modules code that we expect to change at different rates, so that changes in one module
do not influence significantly code outside that module. The difficulty, of course, is in
knowing which parts of an application are expected to change more than others.
Yet, over the years, the software development industry converged into the common
assumption that changes in the code that implements the domain model of an application
occur independently of changes in the code that deals with the user interface, for instance;
likewise for the code that interfaces with the execution environment. Whereas the domain
model varies only with changes in the application domain, the other two vary either with
the available technology or the deployment environment.
This difference became more acute, first with the advent of the web-enabled applications, and then with the growing trend towards service-oriented architectures. In both
cases, organizations felt the need to deploy their existing applications using either new
ways of accessing the application, or using a rather different infrastructure, while retaining the same domain logic in the application.
Therefore, it is now a common practice in the area of enterprise software development to adopt a layered software architecture for the development of an application.1
In particular, an architecture that separates the implementation of the domain model
from the remaining aspects of the application. For instance, Fowler [2002] proposes the
three-layered architecture that I show in Figure 2.1, whereas Evans [2003] proposes the
architecture shown in Figure 2.2 on the next page.
1
For a good presentation of the layered architectural style, see [Bass, Clements, and Kazman, 2003]
and [Clements, Bachmann, Bass, Garlan, Ivers, Little, Nord, and Stafford, 2003, Chapter 2].
User Interface
Application
Domain
Infrastructure
Figure 2.2: The layered architecture from [Evans, 2003]. Unlike the layered architecture depicted in Figure 2.1, in this case each of the layers may use all the layers
below it: The User Interface layer may use all the remaining layers; the Application layer
may use both the Domain and the Infrastructure layers; and the Domain layer may use
only the Infrastructure layer.
Although the two architectures are not exactly the same, in both cases there is a
domain layerthat is, a module that contains the implementation of the domain model
separated from the remaining aspects of the application (aspects such as the user interface). Moreover, this layer is restricted to use only the layer beneath it: the layer providing
infrastructural support. Thus, to understand the implementation of a domain model we
may need to consider also the infrastructural layer, but nothing else.
2.1.5
Ideally, the domain layer should remain independent of any other layer in the system.
Given that the domain layer implements the applications domain model, it should not
be affected by changes that may occur in other parts of the application that deal with
aspects not related to the domain model.
In practice, however, in the standard architecture for an enterprise application, the
domain layer appears above the infrastructural layer. This dependence results from the
fact that the implementation of a domain model for an enterprise application needs to
take into consideration a range of other concerns that are extraneous to the applications domain model. These concernsoften called non-functional requirementsinclude
such things as: the need to support concurrent access to the domains entities, the need
to support the distributed execution of the application, the need to store persistently the
information about the domain, or the need to inter-operate and integrate with other external applications. So, whereas the responsibility of supporting most of these requirements
17
18
rests upon the infrastructural layer, the domain layer may still need to use the infrastructural layers services to provide an implementation of the domain model that satisfies
these requirements.
A solution that is commonly used in an enterprise application, and that addresses
many of these requirements, is the use of an external database management system to
store persistently the information about the domains entities. Unfortunately, this common approach has the undesirable consequence of influencing the programming model
that is used to implement the applications domain: Notwithstanding the best efforts of
patterns, frameworks, and tools, that try to isolate the implementation of the domain
model from the specifics of persistently storing the information about the domain,2 that
isolation is not perfect and the usefulness of the object-oriented programming languages
becomes severely impaired, thereby further adding to the difficulty of implementing a
domain model.
Furthermore, by conflating the solutions of all the problems into a single approach
makes it more difficult, not only to analyze each of the problems and to improve on each
of the solutions separately, but also to employ that solution in contexts where not all the
requirements existfor instance, when no need for persistence exists.
Therefore, in this dissertation I shall ignore all the extraneous requirements for a
domain model and concentrate instead on how best can we implement the domain models
core requirements with the current object-oriented programming languages.
2.2
To discuss the difficulties of implementing a domain model, I shall use throughout this
dissertation a simple example of an application from the banking domain.
The banking domain is a well known example, often used in the literature of objectoriented programming. But, whereas in most cases, the example is given with only a
couple of entities and relationships, in this section I extend the example with more entities
and relationships; also, entities have more properties and more complex behaviors than
usual.
2.2.1
Examples Rationale
In fact, many of the patterns described, for instance, in [Fowler, 2002; Alur, Crupi, and Malks, 2001]
target specifically this problem.
models. In fact, I want to show that, even in such simplistic examples, we encounter
problems that are difficult to solve using current object-oriented languages.
By presenting this extended example, however, I do not intend to discuss its implementation in all the details. Also, I shall not discuss all the possible alternatives either
for the design of a solution, or for the implementation of a given design, as they are too
numerous. Rather, I shall present a design that I deem as reasonable, and, for that design, I shall concentrate only on the implementation aspects that illustrate the problems
of the current approaches and the advantages of my proposals.
Finally, I shall use this example, not only as a means to introduce the problems that
I propose to solve in this dissertation, but also to introduce some terminology related to
the development of domain models.
2.2.2
Applications Functionality
The core functionality of the banking application is the management of the accounts that
belong to the clients of a bank. The clients accounts may be either checking accounts,
or savings accounts. Clients may have as many accounts of each kind as they want, but
they must own at least one checking accountthus, when a new client is added to the
bank, a new checking account is created for that client. Each account, on the other hand,
is owned by a single client.
As usual, accounts have a current balance, which corresponds to some monetary
amount in some particular currency. Deposits and withdrawals into an account change
the accounts current balance by the amount deposited or withdrawn. Yet, when the
amount to deposit or to withdraw is not in the same currency as the accounts balance, the
amount deposited or withdrawn must be converted to the accounts currency. Moreover,
depending on the currencies involved in the exchange and on the account for which the
exchange is made, the bank may charge the requesting account with a currency-exchange
fee.
Whereas clients are allowed to make as many deposits and withdrawals as they want
from their checking accounts, the operations on savings accounts are much more restricted. Savings accounts are created for a given time periodfor example, seven days,
or three monthsand with a corresponding interest rate. Clients may create a new savings account whenever they want by choosing one time period, and by transferring some
amount from one of their checking accounts. At the end of that time period, the bank
automatically transfers the savings accounts balance, plus interest, to the checking account that was used to create the savings account. Therefore, clients may earn money (in
the form of interest) by putting their money in savings accounts. The tradeoff is that they
cannot make further deposits into the savings account, nor make partial withdrawals.
19
20
Clients, however, are allowed to withdraw all of the savings accounts balance before the
end of the time period is reached, but in that case the bank does not pay interest. Instead,
the bank charges a fee for the anticipated withdrawal. Finally, once the balance of the
savings account is withdrawn, either by the client or by the bank, the account is closed.
Checking accounts may be closed, also, provided that their balance is zero, and that
all of their corresponding savings accounts are closed. Naturally, neither deposits nor
withdrawals are allowed for a closed account.
The bank allows that checking accounts have negative balances, but the total balance
of each clientthat is, the sum of the balance of all the clients accountsmust be greater
or equal to zero. A checking account with a negative balance, however, pays interest to
the bank, on a daily basis.
At the end of each day, the bank calculates the interest due by each of the checking
accounts that have a negative balance, and charge each of the accounts accordingly. Only
then, it processes the savings accounts that end in that day. If, during this process, a
clients total balance becomes negative, all the accounts for that client are closed.
To pay the interest, or to receive the fees and the interest that is due in each of
the above mentioned situations, the bank itself has an account. This account has no
restrictions on its balance. Moreover, the deposits and withdrawals from this account
that need a currency conversion are not charged with a fee.
Finally, to issue periodic reports about the banks managed accounts, the bank needs
to calculate both the total of the clients checking accounts and the total of the clients
savings accounts. Each of these totals is the sum of all the corresponding accounts
balances.
2.2.3
From the description of the banking applications responsibilities in the previous section,
it is straightforward to identify some of the objects that are relevant to the functioning
of the applicationobjects that belong to the applications domain. For instance, objects
such as bank, client, checking account, savings account, monetary amount, currency, and
time period result from the simple approach of identifying the nouns used in the description of the functionality. Typical object-oriented analysis and design methodologies3
start with these objects and proceed by identifying the attributes and the behaviors that
characterize each of the objects, as well as the associations among them.
It is common practice, however, to distinguish between entities and value objects (see,
for instance, [Evans, 2003, Chapter 5], [Fowler, 2002, Chapter 18], and [Riehle, 2000,
3
Chapter 3]). Entities are objects that have a distinguished identity in the program. On
the contrary, value objects do not have a distinguished identity. Rather, they are used
only for their value. Thus, we can replace an instance of a value object by another that
has the same value without affecting the semantics of the program. Whereas both kinds
of objects typically have attributes that describe the object state, only entities may have
their state changed during the program execution; value objects are immutable and, once
created, represent always the same value. Examples of entities from the banking domain
are banks, clients, checking accounts, and savings accounts. Also from that domain,
examples of value objects are monetary amounts, currencies, and time periods.
This distinction between entities and value objects extends naturally to their types.
So, I shall use the terms entity type and value type to refer to the type of a particular
entity and value object, respectively. As a matter of fact, the types, rather than their
instances, are the elements that are most used in the representation of a domain models
structure.
Besides entity types and value types, the other elements which are essential to represent the structure of a domain model are associations.4 Associations represent relationships between entitiesfor example, the relationship between clients and their accounts
is represented in the domain model by an association between the client type and the
account type. Binary associations may be either unidirectional or bidirectional, depending on whether they are traversable in only one or in both directions, respectively. A
binary association between two types A and B is traversable from A to B if the instances of
A can refer to the instances of B with which they relate to. Also, following the terminology
used in the UML 2.0 specification [Object Management Group, Visited in 2007], I shall
use the term link to refer to an instance of an associationthat is, the pair of objects that
relate to each other.
But, naturally, not all domain knowledge is structural. In a typical domain model,
such as the one described above, entities are subjected to constraints, also. A constraint
on one or more entities is a condition that those entities must satisfy. For instance, in
our example, that the total balance of each client must be greater or equal to zero.
Finally, another crucial element of a domain model is the description of its processes.
A process is a sequence of domain activities, possibly affecting some entities, that is
executed for a given purpose. It is important to know, not only which processes exist, but
also what they do, and how they do it. An example of a process in the banking domain is
the daily processing of the interest due by each of the checking accounts.
Even though some modeling methodologies and languages distinguish between different types of
relationshipsfor example, aggregations and compositionsin this dissertation I do not make such a distinction and consider only associations.
21
22
bank 1
client 0..*
1
owner
Bank
1
owner
Client
bank 1
assets 1
Bank Assets
Checking Account
1
checking
savings
Savings Account
Figure 2.3: First design for the banking domain model. In this design, only the
classes and the associations that are mentioned explicitly in the problems description
are used.
2.2.4
correspond to each of the relationships that are explicit in the problems description.
But, from an object-oriented programming perspective, this design has a few shortcomings. Checking accounts, savings accounts, and bank assets are all different kinds of
accounts, with things in commonfor example, all the accounts have a current balance
and an operation that allows us to deposit some amount. Yet, this design fails to capture
this commonality. Likewise, both the checking accounts and the savings accounts are
client accounts, for which fees may be charged when the amounts deposited or withdrawn
are in a different currency.
Moreover, although with this design we can reach all the accounts of a bank by
going through the banks clients (who own the accounts), it is easier to implement some
of the applications functionalities if there is an association between the bank and its
clients accounts. As a matter of fact, it is useful to have several of such bankaccount
associations. For instance, for the daily processing of the checking accounts with a
negative balance, it would be helpful to have in the bank the set of all the checking
accounts with a negative balance; for processing the savings accounts that reached the
end of their time period, it would be helpful to have in the bank a list of savings accounts
sorted by their term date; and, for finding a client account given its account number, it
would be helpful to have in the bank a map that indexes the banks accounts by their
number.
In Figure 2.4 on the next page, I show an alternative design that takes some of these
aspects in consideration. Note that the two associations that, in the first design, exist
between the client and the two kinds of clients accounts, are replaced in this second
design by a single association between the client and the class that generalizes both kinds
of accountsthe class Client Account. Yet, whereas in the first design it was explicit in
the class structure that each client must have at least a checking account, in the second
design this structural restriction is no longer represented in the class diagram.
Even though the design space for this problem has many other solutions, I shall not
discuss them here. I remember that my goal with this exercise is not to discuss which
design is best. Rather, I want to discuss the implementation of domain models. Thus,
I shall use this second design as a reasonable solution to this problem and base my
discussion on it, which I shall do in Chapter 3.
2.3
Problem Statement
To implement a domain model, we must find appropriate representations for all the domain models elements in our programming language. In this task lies much (if not most)
of the software development complexity.
23
24
Account
Bank Assets
Client Account
1..*
1
owner
account
Client
client 0..*
Checking Account
0..*
1
checking
0..*
savings
Savings Account
0..*
1
0..1
Bank
0..1
Figure 2.4: Second design for the banking domain model. This design has a more
complex structure than the design presented first. Two new classes were added to
capture the commonality between the different kinds of accounts. Moreover, in this
design there are two redundant associations between the class Bank and the classes
Checking Account and Savings Account, to simplify the banks daily processing of
accounts.
The use of the object-oriented programming paradigm helps to some extent. For
instance, we may use classes to represent entities types, slots to represent entities attributes, and methods to represent some of the domain models processes. Unfortunately,
this apparent ease of implementation does not extend naturally to all the remaining concerns of a domain models implementation.
Indeed, I argue that what makes the implementation of a domain model such a difficult
task is that the current object-oriented programming languages lack the appropriate
expressiveness to implement some of the typical requirements of a domain model.
In this dissertation, I address specifically the following areas where current objectoriented programming languages have manifest shortcomings:
Representing associations: Even though associations are essential in the representation of a domain models structure, their implementation is not as simple as the
implementation of other structural aspects of a domain model, as we shall see in
Section 3.3. On the contrary, implementing correctly an association between two
entities may turn into a difficult task; more so, if we consider bidirectional associations, which must remain consistent even in the presence of failures during the
establishment of a link between two objects.
Representing constraints: Like associations, constraints on domain entities are
present in every domain model. But, whereas implementing very simple constraints
on a single object may be relatively easy, implementing constraints that involve
more than one object becomes very difficult on a moderately complex domain, as we
shall see in Section 3.4.2.
Concurrent execution: A common requirement for enterprise applications is that
they be able to process multiple requests from their users simultaneously. As the
common solution for this problem is to have each request processed by a different
thread, we have to face the difficulties of concurrent programming. The implementation of a domain model is particularly sensitive to this fact, because domain
entities are implemented as shared mutable objects that must maintain their identities throughout the entire applications life-cycle. Thus, we must ensure that these
objects are consistently manipulated in a concurrent setting, which is not at all
simple with the current object-oriented programming languages, as we shall see
in Section 3.1.
Failure Recovery: Domain operations may fail to complete, either because their
execution would violate a domain constraint, or simply because they received erroneous information. Most often, when that happens, nothing should change in
the domains state. But, given that current programming languages have no provision to undo changes, the implementation of domain operations must verify all
the preconditions necessary for the success of the operation before they make any
25
26
change, or else, they must explicitly undo the changes in case of failure. This style
of programming, however, is very difficult to use in more complex operations, as we
shall see in Section 3.2.
2.4
Now that I explained why implementing a domain model is an important and difficult
task, and that I want to contribute to its simplification, the question that remains to
be answered is: How do I propose to accomplish it? The full answer to this question,
of course, lies ahead in the remaining of this dissertation. But, before I conclude this
chapter, I present two general and complementary approaches that we may use to solve
this problem, so that I make clear which approach I am following in this work.
2.4.1
One way to simplify the implementation of a domain model is to automate it. The general
idea in this case is to use code generation tools to generate automatically all the source
code necessary to implement a domain model, much in the same way as we use a compiler
to generate an executable program from its source code.
This approach is called the model-driven development approach [Selic, 2003] and is
best illustrated by the proposal of the Model Driven Architecture (MDA) framework by the
Object Management Group [Mellor, Scott, Uhl, and Weise, 2004; OMG, 2007]. The MDA
approach proposes that, rather than implementing an application in a specific execution
platform, developers should specify instead a platform independent model (PIM) for their
application; then, this PIM is transformed into a platform specific model by way of MDA
tools that generate all the code.
To accomplish this level of automation, however, the PIMs must be much more detailed than domain models typically are, or even than currently used domain modeling
languages allow them to be. Indeed, this need for operational models brought up by the
MDA approach is one of the major incentives for much of the work on the UML 2.0 specification [Object Management Group, Visited in 2007] and on the idea of an executable
UML [Raistrick, Francis, and Wright, 2004].
Yet, even though the MDA approach is enticing, not all researchers agree that it is the
best approach to solve the software development process [Thomas, 2003; Thomas and
Barry, 2003], or that UML provides a good foundation for it [Hailpern and Tarr, 2006;
France, Ghosh, Dinh-Trong, and Solberg, 2006].
In fact, it is not clear whether an MDA approach is feasible with current proposals.
Also, even if we consider that it becomes a reality, it is questionable whether the development of the PIMs would be any simpler than implementing the source code, given the
level of detail necessary to make them fully executable. Finally, if the PIMs are meant to
be our domain models, then they loose part of their usefulness of being a higher-level and
abstract description of our programs; if, on the other hand, the PIMs are not meant to be
the domain models, then we have to face again the task of transforming a domain model
into a PIM, bringing us back to where we started.
A less ambitious approach, but still with the same general idea of generating code
automatically, is to consider only a fragment of the UML language, generate automatically
code for that fragment, and let the programmers complement the generated code with the
missing pieces that are necessary to complete the program. For instance, some proposals
along these lines exist for generating a set of classes in an object-oriented programming
language given a UML class diagram describing those classes [Harrison, Barton, and
Raghavachari, 2000; Gnova, del Castillo, and Llorens, 2003]. One of the difficulties
in this case is to avoid round-trip problemsthat is, that the code may be regenerated
without throwing away the changes that the programmers made manually.
2.4.2
Most of my proposals in this dissertation follow an entirely different route (with a notable
and justified exception that I shall present in the end of this discussion).
As we saw before, the difficulty in implementing a domain model is caused by an
impedance mismatch between the two languages used in this task: the modeling and
the programming languages. So, another way to alleviate the problem is to reduce that
impedance mismatch, by bringing the two languages closer together.
In principle, if we make our programming language more expressive, we expect that
our implementation tasks become easier in return. In fact, it is not only a matter of being
more expressive in general, but of being more expressive towards our domain modeling
languagethat is, of having in the programming language constructs that correspond
more closely to the constructs that are used at the domain modeling level.
Object-oriented programming languages are an excellent example of the effectiveness
of this approach. By adding to procedural programming languages constructs such as
classes, objects, inheritance, or polymorphism, programming language researchers created a new programming paradigm that allows us an arguably simpler implementation
of our domain models; this happens because the newly added constructs allow a more
direct representation of the domain modeling concepts. For instance, implementing an
entity type as a class in an object-oriented programming language is much simpler than
in a procedural language that does not provide an equivalent construct.
27
28
2.5
Summary
This chapter establishes the context of this research work in the larger context of the
software engineering discipline. In particular, it identifies the task of implementing a
domain model as the target of the work.
After discussing briefly what is a domain model, I introduce an extended example of
a rich domain model that is used throughout the dissertation. This example serves also
to introduce some basic terminology on domain modeling.
5
Even though all the work in this dissertation is specialized for the Java programming language, most of
the results apply equally to any other mainstream object-oriented language.
2.5 Summary
Then, I introduce the implementation problems that I propose to solve with this work.
Namely, the difficulty in implementing associations, domain constraints, and domain
operations that may fail, all of these in the context of a concurrent program.
Finally, I present my approach to solve the problem, which consists in extending the
programming languages with new programming constructs that allow them to implement
the domain model elements more easily.
29
30
Chapter 3
3.1
To demonstrate the problems that programmers face when they have to implement a
concurrent domain model, we do not need to go much farther in the complexity of a
domain model. In fact, even in rather simplistic examples we encounter problems that
32
class Account {
long balance;
Account(long balance) {
setBalance(balance);
}
long getBalance() {
return this.balance;
}
void setBalance(long balance) {
this.balance = balance;
}
void withdraw(long amount) {
setBalance(getBalance() - amount);
}
void deposit(long amount) {
setBalance(getBalance() + amount);
}
}
Listing 3.1: A non-thread-safe class Account in Java with the basic getBalance,
deposit, and withdraw operations.
3.1.1
Basic Thread-Safety
In Listing 3.1 I show a reasonable implementation of the class Account in Java, with
no concerns for thread-safety.
The lack of thread-safety in the class Account means that, in the presence of multiple
executing threads, the interleaving of the various threads can cause inconsistent updates
(and readings) of the instance variable balance: We can have an account with a balance
of 100, make two deposits of 50 each and arrive at a state where the final balance is 150,
instead of 200. This situation is illustrated in Figure 3.1 on the next page, where I show
33
setBalance(150)
getBalance() 100
acc.deposit(50)
acc.deposit(50)
getBalance() 100
setBalance(150)
Time
t1
t2
t3
acc 100
acc 150
acc 150
Figure 3.1: The possible execution of two concurrent calls to the method deposit for
the same Account instance. Because no synchronization exists, the final accounts
balance is not correctone of the deposits was lost. The notation used in this Figure
was introduced in Figure 1.2.
class Account {
...
synchronized long
synchronized void
synchronized void
synchronized void
}
getBalance() {...}
setBalance(...) {...}
withdraw(...) {...}
deposit(...) {...}
Listing 3.2: The thread-safe version of the class Account. Thread-safety is obtained
by adding the synchronized modifier to all the classs methods.
two concurrent executions of the method deposit for the same Account object, acc.
Both executions call the method getBalance before any change is made to the balance
of the account. Thus, both obtain the same resultthe balance of the account before any
of the two deposits. Then, each execution of the method deposit adds to that value the
amount to deposit and sets the new balance. The problem is that the final call to the
method setBalance overwrites the change made by the previous execution.
This problem is widely known, of course, just as well as its trivial solution: To make
the class thread-safe we just have to add the synchronized modifier to each of the
methods in the class, as sketched in Listing 3.2. When a thread executes a synchronized
method on a particular object, the thread must acquire, first, an exclusive lock associated
with that object, which is released at the end of the method execution. While the thread is
holding the exclusive lock, no other thread can acquire the same lock. So, the execution
of a synchronized method for a given object cannot be interleaved with the execution, by
34
setBalance(150)
getBalance() 100
acc.deposit(50)
acc.deposit(50)
getBalance() 150
setBalance(200)
Time
t1
t2
t3
acc 100
acc 150
acc 200
Figure 3.2: The possible execution of two concurrent calls to the method deposit
for the same Account instance. In this case, because the method deposit is
synchronized, the second thread waits until the first thread finishes its methods
execution.
other threads, of any other synchronized method for the same object. If we make all the
methods of the class Account synchronized, then only one thread at a time can execute
code for each account object, therefore preventing inconsistencies. The new behavior of
the concurrent executions of the method deposit, after the addition of synchronization,
is depicted in Figure 3.2. In this case, when the second call to the method deposit is
made, the executing thread waits until the first thread releases the lock, before continuing
with the execution of the method.
Unfortunately, this simple solution brings its share of problems with it, also. For
instance, it prevents two threads from executing concurrently the method getBalance
for the same account, even though that method only reads the state of the object. In
this case, we are limiting the concurrency unnecessarily. In the end, putting too much
synchronization into the application eliminates all the parallelism from the application
(to the point of generating deadlocks). On the contrary, too few synchronization leads
to inconsistencies. Thus, the choice of adding a synchronized modifier should be
considered very carefully by each programmer, putting on the programmers shoulders a
great responsibility.
Furthermore, the problems become worse when more than one object needs to be accessed consistently by multiple threads. One of the problems with lock-based approaches,
as this one, is that these approaches do not compose: Given two thread-safe objects, we
cannot access both of the objects in a thread-safe way without adding more synchronization that, almost always, needs to override the existing synchronization. I shall discuss
now an example which illustrates this problem.
Bank
belongs to
35
0..*
account
Account
Figure 3.3: The UML class diagram with the relationship between the class Bank
and the class Account: A bank can have many accounts, but an account belongs to
exactly one bank.
class Bank {
Set<Account> accounts;
...
void transfer(long amount, Account source, Account target) {
source.withdraw(amount);
target.deposit(amount);
}
long totalBalance() {
long total = 0;
for (Account acc : accounts) {
total += acc.getBalance();
}
return total;
}
}
Listing 3.3: The implementation in Java, without any concerns for thread-safety, of
the class Bank with the two methods transfer and totalBalance.
3.1.2
Continuing with our stripped-down example of the banking application, consider now
that the bank accounts are related to the bank, according to the UML class diagram
shown in Figure 3.3. Consider, also, that the class Bank has the following two methods:
(1) the method transfer, which transfers some amount between two accounts; and
(2) the method totalBalance, which calculates the total balance of all bank accounts
belonging to the bank.
I show, in Listing 3.3, a possible implementation in Java for the class Bank. Unlike
the methods of class Account, the methods shown for the class Bank do not change the
state of a Banks instance; the Banks methods just operate over the accounts of a bank.
Therefore, even though this implementation has no synchronization code, interleaved
executions of the method transfer do not interfere with one another, provided that
we are transferring money between instances of the thread-safe implementation of the
class Account shown before. Because the class Account is thread-safe, each deposit
or withdrawal leaves the corresponding account in a consistent state.
If we consider, however, the concurrent execution of the methods totalBalance
36
trg.getBalance() 100
100
src.getBalance()
totalBalance()
200
t2
t3
src 100
trg 0
src 0
trg 0
src 0
trg 100
Figure 3.4: Execution of the method transfer during the execution of the method
totalBalance. The result returned by totalBalance is incorrect: The result is
200, but the correct total balance for the bank, either before or after the transfer, is
100.
trg.getBalance() 0
src.getBalance() 0
totalBalance()
t2
t3
src 100
trg 0
src 0
trg 0
src 0
trg 100
Figure 3.5: Execution of the method transfer during the execution of the method
totalBalance. In this case, the balance of the src and trg accounts are read in
such a way that makes the result returned by totalBalance be 0. As in Figure 3.4,
however, the correct total balance for the bank, either before or after the transfer, is
100.
and transfer, it is easy to see that the method totalBalance will return an incorrect
result, if it executes in the middle of the method transferthat is, immediately after
the withdrawal from the source account and before the deposit on the target account; at
that time, the total balance of the bank, as calculated by the method totalBalance,
does not correspond to the correct value. In fact, the method totalBalance can give an
incorrect result whenever the execution of this method is interleaved with the execution
of at least an execution of the method transfer in any of the following ways:
The method totalBalance reads the balance of the source account before the
withdrawal, and the balance of the target account after the deposit. In this case,
the result returned will exceed the correct value by the amount transferred. This
situation occurs in the execution shown in Figure 3.4 on the facing page.
The method totalBalance reads the balance of the source account after the
withdrawal, and the balance of the target account before the deposit. In this case,
the result will be incorrect, also, but now the value returned will be below the correct
value exactly by the amount transferred, as shown in Figure 3.5 on the preceding
page.
Unfortunately, solving this problem with lock-based mechanisms is not that easy.
On one hand, the simple solution of making both methods synchronized impairs greatly
the concurrency of the application. For instance, it would neither be possible to execute
concurrently two transfers between two pairs of unrelated accounts, nor call the method
totalBalance in two parallel threads. The problem here is that the lock on the bank
object is too coarse-grained. Moreover, it is reasonable to expect that bank accounts can
be accessed in other places other than these two methods of the class Bank. Having the
Banks methods synchronized does not help in getting the correct behavior in that case.
On the other hand, trying to use more fine-grained locks, by synchronizing on the
accounts as shown in Listing 3.4 on the following page, introduces other problems, such
as the possibility of deadlocksfor example, consider a thread transferring money from
account A to account B, and a concurrent thread transferring from B to A (see Figure 3.6
on the next page).
Solving the problems introduced by fine-grained locking (for instance, by acquiring
locks in a specific order to avoid deadlocks) makes the code much more complex and
is practically unfeasible for any moderately complex domain-intensive application, in
which the objects are densely intertwined. Rather, I argue that the only way to deal
effectively with this problem is by using the notion of atomic methods (or blocks), as
introduced by the work on Software Transactional Memories. In Chapter 4, I propose a
new practical implementation of Software Transactional Memory that is specially suited
for implementing rich domain models.
37
38
class Bank {
...
void transfer(long amount, Account source, Account target) {
synchronized (source) {
synchronized (target) {
source.withdraw(amount);
target.deposit(amount);
}
}
}
long totalBalance() {
return lockAndSum(accounts.iterator());
}
private long lockAndSum(Iterator<Account> accounts) {
if (accounts.hasNext()) {
Account acc = accounts.next();
synchronized (acc) {
long remainingSum = lockAndSum(accounts);
return remainingSum + acc.getBalance();
}
} else {
return 0;
}
}
}
Listing 3.4: An implementation of the class Bank that uses fine-grained locks on the
accounts accessed by each method. This implementation, however, does not handle
the problem of possible deadlocks.
synchronized (B)
synchronized (A)
transfer(50, A, B)
Deadlock!
transfer(50, B, A)
Deadlock!
synchronized (B)
synchronized (A)
Time
t1
Figure 3.6: Deadlock caused by the concurrent execution of two calls to the method
transfer, which synchronizes on the source and target accounts. The deadlock
occurs at time t1.
39
trg.deposit(100)
src.widthdraw(100)
transfer(100, src, trg)
Time
t1
t2
t3
src 100
trg 0
src 0
trg 0
src 0
trg 0
Figure 3.7: Failure during the deposit of a transfer operation. The cross in the
method deposit represents that the method throws an exception, causing the termination of both the deposit and transfer methods. The state of the program,
however, changed at instant t2 and, thus, remained changed after the exception:
Whereas at the beginning of the transfer the src accounts balance is 100, at the
end the balance is 0 and the account trg remains the same.
3.2
Failure Recovery
In the above discussion, I argued against the use of locks and in favor of using atomic
actions to ensure the consistency of a concurrent application. In fact, the concurrency
control aspects of atomic actions are the primary concern in most of the published work
in Software Transactional Memory research. Yet, for the purposes of this dissertation,
the problem of maintaining the consistency when a failure occurs during the execution of
an operation is an equally, if not more, important concern. This problem occurs whether
we have a concurrent application or not. Thus, it is independent of concurrency and it is
not solvable by the use of locks.
Consider, again, the method transfer shown in Listing 3.3 on page 35. Except
for the problems of concurrency already discussed, this method appears to be correct.
Unfortunately, the problem I am discussing in this section manifests itself even in this
simple method if the method deposit may fail in some cases.
Given the implementation of the class Account in Listing 3.1 on page 32, the methods
withdraw and deposit never fail.1 On a more realistic setting, however, neither can we
withdraw an arbitrary amount from an account, nor can we deposit on all accounts. For
instance, assume that we cannot withdraw an amount larger than the account balance,
and we cannot deposit on a closed account. In Listing 3.5 on the following page, I show
the changes made in the class Account to incorporate these new requirements.
1
Unless some runtime error, such as an out-of-memory error occurs. Nevertheless, I am not concerned
with runtime errors due to the malfunction of the program. Rather, I am concerned with the normal,
domain-dependent, exceptions that may occur.
40
class Account {
...
boolean closed;
...
void withdraw(long amount) {
if (amount > getBalance()) {
throw new InsufficientBalanceException(...);
} else {
setBalance(getBalance() - amount);
}
}
void deposit(long amount) {
if (closed) {
throw new ClosedAccountException(...);
} else {
setBalance(getBalance() + amount);
}
}
}
Listing 3.5: Reimplementation of the methods withdraw and deposit, for the
class Account, to limit the withdrawal of an amount to the balance of the account,
and to refuse deposits in a closed account.
With this new definition for the class Account, the method transfer is no longer
correct. Consider the case when we are transferring some amount to a closed account.
The call to the method deposit on the target account will fail, by throwing the exception
ClosedAccountException. But, because the failure occurs after the source account
was withdrawn, it causes the loss of the amount to transfer (see Figure 3.7 on page 39),
which is not an acceptable behavior for such an application. Note that this problem is not
related to concurrency. The case that I presented here is a purely sequential program.
To solve this problem, we have essentially two options:
We verify before executing any of the operations that they will not fail, as in Listing 3.6.
We save the previous state of the source account and restore it in case of failure, as
in Listing 3.7 on the next page.2
canDeposit which receives the amount to deposit and returns whether that amount
can be deposited or not, is not viable, either: In the end, we have one such predicate
for each operation that may fail. Not only that, all invocations of the operations have to
2
In this particular case, we could just re-deposit the amount in the source account, undoing the original
operation. That approach, however, is not generally applicable, because the performed operation first may
be irreversible.
41
42
be guarded by calls to these predicates. Finally, this approach is not applicable easily
in all cases. Sometimes, it is impossible to know in advanceor to factor them outthe
conditions that prevent the correct execution of each operation.
In the second case, we have similar problems. On one hand, the state of the object
may not be publicly available or it may not be easy to save. On the other hand, the
complexity of the code needed to save and restore the state increases substantially for
more complex methods, when several operations that may fail are executed within more
sophisticated control structures. I believe that the difference between the original version
of the method transfer and the version corresponding to this approach illustrates very
well this latter point.
3.3
In this and in the following section, I shall now consider the full domain model introduced
in Section 2.2.4.
Class diagrams, as those shown in Figure 2.3 on page 22 and in Figure 2.4 on page 24,
represent only (part of) the class structure of a domain model. The dynamic aspects of
a domain modelits behaviorare not captured by such diagrams. In fact, as I discuss
briefly at the end of Section 2.2.4, not even all the structural aspects of a domain model
are captured by class diagrams. To model the dynamics of an object model, modeling
languages such as UML provide complementary notations, such as sequence diagrams
and state charts.
This separation, at the modeling level, between the structural and the behavioral
aspects of a domain model, becomes diluted when we are at the implementation level,
because object-oriented programming languages conflate the two aspects into the same
artifactthe class. Yet, for the purposes of this presentation, it is useful to discuss the
implementation of each of these aspects in separate. So, in this section I shall discuss
only the implementation of the domain models structure. Then, in the next section, I
address the behavioral aspects of the domain models implementation.
The class diagram shown in Figure 2.4 on page 24 is a high-level class diagram. As
such, it omits many details which are essential for an implementation of the domain
modelfor example, no attributes are shown for the classes in the diagram. The omission
of such details during the initial design phase is an important abstraction mechanism
that allows us to experiment with different designs without spending much time with the
details of each one.
But, once a design is chosen, the class diagram must be filled-in with the details
necessary for an implementation of that design. In Figure 3.8 on the next page, I show
a class diagram in which some of the classes previously shown in Figure 2.4 have more
informationnamely, information about their attributes and their methods. The methods
in each class implement that class instances behavior; the attributes, on the other hand,
are used to hold the state of each class instance.
To implement the structure of a domain model, programmers must provide an implementation for each of the following two kinds of domain model elements: (1) the classes,
with their attributes; and (2) the associations between classes.
3.3.1
Implementation of Classes
The implementation of the basic structure for each of the classes in a UML class diagram
is straightforward in most modern object-oriented programming languages. Each of the
classes in a class diagram maps naturally into a programming language constructfor
example, a Java class or interface. Furthermore, UMLs generalization relationships3
between classes are implemented by the usual type extension constructs that are available
in object-oriented programming languages. Finally, for the UMLs class attributes we have
a direct correspondence with the class member slots (or fields) found in object-oriented
programming languages. For instance, using Java, each class attribute is implemented,
typically, as a private class field, with two accessor methods: one to obtain the field value,
and the other to set the field to a new value. The visibility of each of these class members
varies, depending on whether they are meant to be used outside the class or not.
In Listing 3.8 on page 45, I show a partial implementation, in Java, of the classes
Account and ClientAccount. Each class implements the attribute that is depicted
in Figure 3.8 on the next page.
43
44
Account
balance:Money
deposit(Money)
withdraw(Money)
ClientAccount
BankAssets
Client
closed:boolean
1..*
close()
isClosed():boolean
account
1
owner
name:String
totalBalance():Money
SavingsAccount
CheckingAccount
1
checking
0..*
savings
interestRate:Percentage
depositPeriod:TimePeriod
termDate:Date
Figure 3.8: Implementation-level class diagram for part of the banking applications
domain model. This class diagram shows some of the classes from the design-level
class diagram shown in Figure 2.4, but with some implementation details added.
Both attributes and methods are shown for some of the classes.
45
46
3.3.2
Implementation of Associations
Unlike classes, associations do not have a direct mapping into the implementation-level
programming language. Thus, their implementation is not as simple as it is for classes.
In fact, in some cases, the implementation of associations is far from trivial and becomes
a great burden for the programmer.
Class associations are common at the modeling level, but they have not found their
way into the realms of modern object-oriented programming languages, even though
their absence at the implementation level has been noted two decades ago [Rumbaugh,
1987]. The lack of support for associations at the programming language level forces
programmers to use other constructs to implement them. In some cases that is easily
done; in many others, however, it is not.
One of the factors that influences the implementation of an association is its multiplicity. One-to-one associations are simpler to implement than many-to-many associations,
whereas one-to-many associations stay somewhere in between.
Another factor that affects the implementation of an association is whether the association is unidirectional or bidirectional. Bidirectional associations are harder to implement
than unidirectional associations. As a matter of fact, bidirectionality poses the most difficulties on the implementation of associations. So much so that programmers are advised
to avoid bidirectional associationsor to limit their useeven though such associations
are obviously necessary and useful for expressing the structure of a domain model. For
example, consider what Eric Evans wrote on his book on domain-driven design:
In real life, there are lots of many-to-many associations, and a great number
are naturally bidirectional. The same tends to be true of early forms of a model as
we brainstorm and explore the domain. But these general associations complicate
implementation and maintenance. Furthermore, they communicate very little about
the nature of the relationship.
There are at least three ways of making associations more tractable.
1. Imposing a traversal direction
2. Adding a qualifier, effectively reducing multiplicity
3. Eliminating nonessential associations
It is important to constrain relationships as much as possible. A bidirectional association means that both objects can be understood only together. When application
requirements do not call for traversal in both directions, adding a traversal direction
reduces interdependence and simplifies the design.
I agree with the argument that bidirectional associations increase the coupling between classes, and that they may, therefore, reduce our ability to reason about the domain
in a modular way. I do not agree, however, with the argument that we should remove
Bank and the class BankAssetsthat the association only allows the traversal from
the Bank to the BankAssets. The implementation of this association is similar to the
implementation of any other class attribute for the class Bank: We add a field to the class
Bank to keep a reference to the only instance of BankAssets that is related with each
Banks instance, and we add the necessary methods to access that field. In Listing 3.9,
I show such an implementation. Note the similarity with the code from Listing 3.8 on
page 45.
Bidirectional one-to-one associations
Unfortunately, the implementation of a bidirectional one-to-one association is no longer
that simple: At least, an implementation that gives us some guarantee of domain consistency.
47
48
Consider again the case of the BankBankAssets one-to-one association, but now as
a bidirectional association. A simple implementation consists in keeping the class Bank
as shown in Listing 3.9 on the preceding page, and in changing the class BankAssets
so that this class has a reference to an instance of a Bank. The problem with this
implementation, however, is that there is nothing in it to prevent domain inconsistencies.
For example, this implementation allows that a client of these classes creates an instance
of the class Bank that has a reference to an instance of the class BankAssets, which
in turn has a reference to a different instance of the class Bank. Yet, in a bidirectional
one-to-one association, if an object A refers to another object B, then the object B must
refer to the object A also; otherwise, the relationship is not consistent. Of course, it
is possible to have such a simple implementation and still do not have inconsistencies,
provided that the client code of these classes create and change both classes consistently.
But this puts the responsibility on the wrong place.
A better implementation of this association should ensure that, when we execute
a method call such as bank.setAssets(assets), both the bank and the assets
are updated consistently. Moreover, the result of that method call should be the same
as executing assets.setBank(bank). In Listing 3.10 on the next page, I show an
implementation of the BankBankAssets bidirectional association that satisfies these
requirements.
The code in Listing 3.10 illustrates well enough why programmers avoid bidirectional
associations, if possible: Bidirectional associations are much more difficult to implement.
Although the implementation shown follows the recommendations of the Change Unidirectional Association to Bidirectional refactoring pattern [Fowler, Beck, Brant, Opdyke,
and Roberts, 1999], the mere application of the pattern is highly error-prone.
49
50
implementation which is similar to the change occurred in the one-to-one case. The code
needed, however, varies with each combination of multiplicities.
Furthermore, the choice of which collection to use to implement an association depends on additional properties of the association. If the association is ordered, for instance, then the most appropriate collection might be an ordered set or a list. If, on the
other hand, the association is qualified, then maybe a map would be preferable.
So, even though there are well-known patterns on how to implement the various types
of associations in an object-oriented programming language such as Java, the problem is
that employing those patterns is, nevertheless, a burdensome and error-prone task.
3.4
Once we have the code that implements the basic class structure for the banking domain
model, we may turn now our attention to the implementation of the remaining functionality.
To implement the required functionality, we need to add behavior to the classes that
we have in the domain, either by writing new methods, or by adding new code to the
methods that already exist. In this section, I shall discuss some of these methods.
3.4.1
Because many of the functionality requirements are related to the deposit and withdraw
operations, I start with the implementation of these operations. According to the class
diagram shown in Figure 3.8 on page 44, these two methods are implemented in the class
Account. In fact, even though they need to be specialized for some of the subclasses of
Account, their basic definition is the same for all the accounts: the method deposit
adds the given amount to the accounts balance; the method withdraw subtracts the
given amount from the accounts balance. The amount to add or to subtract, however,
must be in the same currency as the accounts balance. Thus, in Listing 3.12, both
methods use a helper method to perform the conversion of the amount to the appropriate
currency, if needed.
The Banks method convertTo is responsible for making the currency exchange of
the received amount to the new currency, charging the account received as the third
argument with the exchange fees that are applicable. In Listing 3.13 on the next page, I
51
52
normalizeCurrency is made first, it may have already charged the account for the
exchange fees, even though the operation will not conclude successfully. This is yet
another example of the difficulty in dealing with failures in domain operations. Unfortunately, fixing the code to handle this case makes the code much more complex; so, I will
leave the code as shown and assume that it is easy to fix it with the solution proposed in
this dissertation.
3.4.2
One of the functionalities described in Section 2.2.2 is that the total balance of each
client must be greater than or equal to zero. Therefore, to implement this functionality
we must prohibit the operations that, if executed, would cause the total balance of a
client to become negative. Because the only operation that decreases the total balance
of a client is the withdrawal of money from one of its accounts, we may try to implement
this functionality by overriding the method withdraw in the class ClientAccount, as
shown in Listing 3.14 on the facing page.
The strategy for this implementation is to abort the withdrawal before it occurs, by
checking first if the clients total balance is less than the amount to withdraw. Unfortunately, this solution has two errors. First, because the amount to withdraw may be in a
currency different from the currency of the result of totalBalance, the two values may
be incomparable.4 Second, because the call to the inherited method withdraw may, in
fact, withdraw more than the amount requested, if, for example, some fee is charged by
4
Although the class Money is not described here, I assume that it is not possible to compare directly
values with different currencies.
the bank; if that happens, the comparison made in this method is useless to prevent that
the clients total balance after the withdrawal becomes negative.
Assuming again that we can undo the changes already performed by an operation that
fails, an alternative implementation that solves both problems is shown in Listing 3.15.
This implementation relies on the failure recovery capabilities of an operation to perform
first the withdrawal and only then check if the resulting state is consistent with the
domain requirements. If not, then an exception is thrown and the atomic execution of
the method withdraw is aborted, undoing the effect of the withdrawal. Yet, even though
this second solution corrects the two errors identified in the first solution, it is not free of
problems, either. In fact, there are at least two problems with it.
The first problem with this solution is that it may cause a failure, even if the withdrawal
is part of a complex banking transaction that, in the end, would leave the total balance
of the client positive. The failure happens if the withdrawal causes the total balance
to become negative, even if only temporarily. In fact, a simple example illustrates this
53
54
problem. Consider a client with two checking accounts: the first with a negative balance
of 100 EUR, and the second with a positive balance of 200 EUR. The total balance for this
client is 100 EUR. What happens if the client decides to transfer 150 EUR from the second
account to the first? If the transfer operation withdraws the 150 EUR from the source
account first, the total balance of the client becomes negative (minus 50 EUR), which
causes the withdrawal to fail. Of course that, in this case, it may be possible to reorder
the operations so that the deposit occurs before the withdrawal, but this reordering is not
always possible. For instance, consider the case of a deposit of an amount that must be
converted to another currency. As part of the deposit operation, before the balance of the
account is increased with the amount, the amount is converted to the correct currency
and the account is charged with a fee. If the client total balance is 0, then the deposit
fails, because the fee cannot be withdrawn.
So, if we cannot verify that the clients total balance is not negative at the end of the
method withdraw, where can we do it? I argue that we can make this verification only
at the end of the outermost domain operation. Unfortunately, we lack the mechanisms
to do it. In Chapter 6, however, I propose a solution to this problem.
The second problem with the implementation shown in Listing 3.15 is that it is in the
wrong place. The responsibility of checking that the clients total balance is not negative
should not belong to the class ClientAccount. Rather, it should be a responsibility of
the class Client. Imagine that we extend the domain of our example to support different
types of clients. Most probably, each type of client has a different condition regarding
its total balance. For instance, the rule we now have may not be applicable to business
clients, or to clients with a mortgage. Having the verification of the rule in the method
withdraw of the class ClientAccount does not allow us to differentiate between these
cases. If, on the other hand, the responsibility of checking the clients total balance is
on the class Client, then we can specialize it for different types of clients. One way
to implement this verification in the proper place is to use the Observer design pattern
(see [Gamma, Helm, Johnson, and Vlissides, 1995]), and make the client an observer of
all its accounts. But, even if the use of the observer pattern helps in localizing the code
in the proper place, it forces a more complex solution and does not solve the first problem
either. Again, I argue that the solution that I propose in Chapter 6 is a much cleaner way
of solving this problem.
3.5
Summary
This chapter gives several examples of how difficult can be the implementation of a domain
model. The four problems identified in Section 2.3 are illustrated in this chapter with
concrete examples from the banking applications domain model.
3.5 Summary
Even though the domain used as example is very simple, I show that its implementation is far from trivial; specially, if we consider that the domain entities are accessed
concurrently.
55
56
Chapter 4
58
4.1
The key idea underlying the work on Software Transactional Memory is that programmers
specify which operations should execute atomically, rather than protect accesses to the
data with locks. The intended semantics for such atomic actions is that they execute
atomically, independently of which data is accessed by the operationthat is, that their
execution occurs as if nothing else is executing at the same time.
A simple solution to accomplish the intended semantics for atomic actions is to prohibit parallelism in the application when the atomic action is executing. This is similar
to using a global exclusive lock that all the atomic actions must acquire before executing.
Yet, the goal of having STMs is to allow concurrency, rather than to eliminate it. Thus,
the challenge of an STM is to allow the maximum parallelism in a program while ensuring
that atomic actions execute with their intended semantics.
Even though most of the literature on STMs uses the terms atomic action and transaction interchangeably, in this dissertation I use them with a slightly different meaning: I
use the term atomic action to refer to the definition of a series of operations that should
be executed atomically, whereas the term transaction refers to the execution of an atomic
action. There are many places, however, where this distinction is not relevant, in which
case I will use either of the terms.
4.1.1
The property of atomicity has two distinct aspects, which are of special interest to us.
First, that all the changes performed by the execution of an atomic operation are seen at
the same time by the rest of the system. Second, that either all the changes made by the
operation occur or none occurs; there is no in-between case. The first aspect of atomicity
is what ensures the consistency of the data in the case of concurrent executions. The
second aspect of atomicity is what ensures the consistency of the data in the case of
failures: Atomic actions give us the ability to recover from failures.
Having atomic actions in the programming language, the correct implementation of
the class Bank from Listing 3.3 on page 35 becomes much easier: We just have to say
that the two methods (transfer and totalBalance) are atomic actions; then, it is
the programming language responsibility to ensure that the methods execute atomically.
With atomic actions, no locks (or synchronized modifiers) are necessary, either in the
class Bank, or in the class Account.
Obviously, this way of programming a concurrent application is much easier than
using lock-based mechanisms. It is easier to specify which actions should execute atomically, than to make sure that all the relevant locks are acquired, in the correct order,
to ensure the exclusive access to a set of data. Furthermore, atomic actions give us the
added bonus of failure recovery.
4.1.2
Even though the various STMs proposed so far vary considerably in how they ensure the
atomicity property, there are a few concepts that are common to most, if not all, of the
STMs. Two such concepts, which are useful to explain how STMs work, are the read set
and the write set.
From the point of view of an STM, a transaction is a series of operations that either
read values from shared locations or write values to shared locations.1 Thus, the read
set of a transaction is the set of locations read during that transaction. Likewise, the
write set of a transaction is the set of locations written during that transaction.
Given that transactions may occur in parallel, it may happen that two or more of them
access the same shared location. Whereas it may be safe that several transactions read
the same shared location, having one transaction writing to a location that is read by
another would probably break the atomicity property. Thus, it is the job of an STM to
prevent this from happening, usually by keeping track of the read sets and the write sets
of each transaction, and checking for conflicting accesses.
Another important concept regarding STMs is the commit of a transaction. As we
saw, a transaction may execute an arbitrarily large number of operations. Yet, given that
it should execute atomically, none of the values written during the transaction should
become accessible to other transactions until it finishes. The commit of a transaction is
the final operation in a transaction that indicates that all the values written during the
transaction should become accessible to others.
1
The size of a shared location may vary from system to system, from a word of memory to an entire object,
for instance.
59
60
It may happen, however, that a transaction must fail, in which case none of the values
written by the transaction should become accessible. In that case, the final operation of
the transaction is an abort, rather than a commit. An abort may be either executed
deliberately during the transaction as part of an atomic action, or issued by the STM
system when it determines that a transaction should not commit successfully.
4.1.3
Transaction Linearizability
To conclude this brief introduction to STMs, consider again the problematic examples shown in Section 3.1: Figure 3.1 on page 33, Figure 3.4 on page 36, Figure 3.5 on
page 36, and Figure 3.6 on page 38. In which way are these examples changed if we use
STMs? Assume that each of the methods executions in these examples corresponds to a
transaction in some STM-based system, and that no other synchronization exists. Then,
most probably, in all of the examples, the two transactions conflict with each other. Note
that in all the examples there is at least an object that is read by one transaction and
modified by the other. The advantage of using STMs in these examples, of course, is that
the conflict is detected and one the transactions is restarted, whereas in the previous case
incorrect results (or deadlocks) occurred.
4.2
The goal that I established for the STM proposed in this chapter was that it should be
suitable for implementing domain-intensive applications.
Therefore, I started the design of the STM with a set of assumptions regarding the
characteristics of this type of applications:
That the number of updating transactions (transactions that change any data) is
low when compared with the total number of transactions; probably, less than 10%.
That most of the transactions are medium-sized; that is, that the average size of
their read sets and write sets have hundreds to thousands of objects.
That occasionally there are long-running transactions that need to access thousands to millions of objects.
That an updating transaction may read many objects, but typically changes just a
few.
When I started, most of the research on STMs did not deal well with this kind of
workload. In fact, even now, most of the examples and the benchmarks are for very short
transactions: transactions that access from a couple to a few tens of objects before they
commit.
The problem with larger transactions is twofold. First, the need to keep track of large
read sets and write sets may cause serious performance problems. Second, the probability
of a conflict increases both with the number of objects accessed by a transaction, and
with the transactions duration; this problem becomes more dramatic when we have longrunning transactions that access a significant portion of the object space.
Given this set of assumptions, I established the following set of requirements for the
STM proposed in this dissertation:
61
62
In the following section I describe my proposal for an STM that addresses these requirements.
4.3
In the previous introduction to STMs, I have described in very general terms how the
majority of the existing STMs work. Yet, there are many differences among the various
proposalsfor example, different STMs vary in the number of conflicts they generate, use
different conflict detection algorithms, or differ in where and how they store the read and
write sets.
In this chapter I propose a new model for STMs that is significantly better than the existing approaches in handling particular workloads: Workloads which I argue are typical
of domain-intensive applications.
The distinctive element of my approach to STMs is the use of versioned boxes to hold
the mutable state of a concurrent program. Versioned boxes can be seen as a replacement
for memory locations [Harris and Fraser, 2003] or transactional variables [Harris et al.,
2005].
4.3.1
Transactions, as usual, serve to delimit blocks of operations that should execute atomically. A transaction is associated with exactly one threadthe thread that started itand
lasts until that thread either aborts or commits the transaction. A transaction Tc that
starts in the context of another transaction Tp is a nested transaction. Moreover, I say
that Tp is the parent of Tc and that Tc is a child of Tp. Transactions that have no parent
are called top-level transactions.
When a transaction starts, it becomes the threads current transaction until it finishes or another (child) transaction starts. When a transaction finishes, its parent, if any,
becomes the threads current transaction.
63
B
2
1
0
87
23
Figure 4.1: Graphical representation of a versioned box. The historys values are
presented in decreasing order of their version number, which is the number at the
lower right corner of each rectangle. At the top of the historys values I put the name
used to refer the box in the text.
Because child transactions are executed by the same thread of their parents, when a
child transaction is executing, the parent transaction is not. Likewise, it is not possible
to have sibling transactions executing simultaneously. In fact, a sibling transaction of
a transaction Tc can start only after Tc finishes. This model of transaction nesting
corresponds to what Moss and Hosking [2005] call linear nesting.
Each transaction has a version number, which is assigned to the transaction when
the transaction starts. This number comes from a global counter that is incremented
only when a top-level transaction that changed some data successfully commits. So, all
the top-level transactions that start between two commits of such transactions have the
same transaction number. When a nested transaction starts, its number is set to the
number of its parent.
During their execution, transactions may access versioned boxes. Unlike conventional
locations, which keep only a single value, a versioned box is a container that keeps a
tagged sequence of valuesthe history of the versioned box. Each of the historys values
corresponds to a change made to the box by a successfully committed top-level transaction
and is tagged with the number of that transaction; this tag number is the values version.
For instance, if a box B is created in transaction number 5, and then changed twice, in
transactions numbered 23 and 87, the history of B will have three values, each tagged
with one of the previous transactions numbers. I represent such a box graphically as
shown in Figure 4.1.
There are two operations that a transaction can execute on a versioned box B:
The read operation, BoxRead(B), which returns the current value of box B in the
transaction.
The write operation, BoxWrite(B, v), which sets the value of box B to v in the
current transaction.
The behavior of these operations and how they interact with transactions will be explained
below. For now, it is important to note that each of these operations must execute in the
context of some transaction. If the executing thread has no current transaction, then
64
a new transaction is created immediately before and is committed immediately after the
operation.
I say that a box B is read (respectively, written) in the context of a transaction T, when
T is the current transaction of the thread executing a read (respectively, write) operation
on B.
Finally, besides a reference to its parent, each transaction keeps a record of the
following information: (1) a transaction number, (2) a set of versioned boxes that were
read in the context of the transaction, and (3) a map that maps boxes that were written
in the context of the transaction to the values the boxes were set to. To simplify the
presentation below, given a transaction T, I use the notation T-number, T-readSet,
and T-writeMap, to refer to each of these values, respectively.
4.3.2
T, and is used to keep track of the writes performed during T. If the same box is written
more than once during the same transaction T, later values replace earlier values in
T-writeMapthat is, each transaction keeps only the last value written to a box.
A read operation, BoxRead(B), that executes in the context of a transaction T must
return the current value of B. Obviously, the expected behavior for this operation is that it
returns the last value wrote to B in the context of T, if any. To accomplish this behavior,
the read operation searches for an existing mapping for B in T-writeMap. If such a
mapping exists, the corresponding value is returned. Otherwise, the search continues
recursively on Ts parent, until either one mapping is found or no parent exists. If no
mapping is found, then the value to return is obtained from the history of B and B is added
to T-readSet.
The value obtained from the history of B by the read operation executed in T is
the value tagged with the highest number which is less than or equal to the number of
Tfor example, a transaction numbered 30 will read the value 1 from the box B shown
in Figure 4.1 on the preceding page, provided that no value was written to B in the
transaction. As we shall see below when I describe the commit of a transaction, the value
that T reads from B was the last value wrote to B when T started.
I say that a transaction T is a read-only transaction if T-writeMap is empty, regardless of the state of T-readSetthat is, a read-only transaction T is a transaction
during which no box was written, whether some box was read by T or not.
T-readSet is empty. Note that the boxes read during a transaction are added to the
transactions read-set only if they were not written previously in the transaction or in
any of its ancestors; this is one of the distinguishing elements of this model. Thus, a
write-only transaction may actually have read some boxes during its execution, but, if it
did, then it follows that all the boxes read were previously written by the transaction (or
by one of its ancestors).
In the remaining case, when both T-readSet and T-writeMap are non-empty, for
some transaction T, I say that T is a read-write transaction.
Finally, I say that a transaction T is a write transaction when it is either a write-only
or a read-write transaction.
4.3.3
Transactions start with both their read-set and their write-map empty. Also, when they
start, they get a number that will remain the same during the entire transaction, until
the transaction commits. Only when a transaction commits, and only in certain cases,
can this transaction number be changed to a number greater than the originally assigned
to the transaction.
Intuitively, transaction numbers serve to position the successfully committed top-level
transactions within a serial order of execution. As we shall see, however, the numbers
assigned to each transaction do not unambiguously specify a total order among the transactions. Rather, these numbers specify only a partial order among the transactions, because several transactions may get the same number. I shall discuss how this partial
order is related to the linearizability of top-level transactions in Section 4.3.4. Before that,
however, I shall describe the rest of the transactions life-cycle.
As transactions execute in parallel, accessing shared resources, they may conflict
with one another. Conflicts are detected by examining what is read and written by each
transaction. Other STMs check for conflicts whenever a shared location is accessed during
a transaction. In my model, however, conflicts are detected only at commit time. Until
the transaction commits, it accumulates values in its read-set and write-map, which will
then be used at commit-time to detect conflicts. When a conflict occurs, naturally, the
commit of the transaction fails and the transaction is aborted.
A conflictand, thus, a commit failureoccurs when it is not possible to linearize a
transaction. In principle, a transaction can be linearized at any time between its start and
its end. So, we need to know, for each different kind of transaction, when and whether it
can be linearized.
65
66
The use of versioned boxes, with the definition of the read operation given in the
previous section, ensures that all the transactions access a consistent view of the program
state during their execution: After a transaction T starts, and its number is set, it uses
this version number to read the appropriate value of each box; even if several transactions
commit during the execution of T, the changes made by those transactions are not visible
to T. So, ideally, we could linearize each transaction exactly at the time the transaction
starts.
In fact, if T is a read-only transaction, then, by definition, no boxes are changed during
the execution of T. Thus, if T has no effect on the state of the program, we may safely
assume that T executed instantaneously when it startedthat is, that T is linearizable
at the time that it started. Moreover, as nothing is changed by a read-only transaction,
there is nothing to be done in the commit of a read-only transaction.
If, on the other hand, T is a write transaction, then at least one box is changed during
the execution of T. Thus, if T successfully commits, its changes should become visible
after the commit. Because of this, T can be linearized only at its commit-time. To see
why, imagine that T is linearized before its commit and that between that time and the
commit of T a read-only transaction Tr executes. Then, if Tr reads some box written by
T, it should read the value written by T, because it linearizes after T. But, because the
commit of T is not done yet, Tr cannot read that value. Therefore, either the commit of
Tr should fail, or T should be linearized after Tr. As the main design decision underlying
this model is that the commit of read-only transactions never fails, it follows that write
transactions should be linearized at the time of their commits.
Now, the problem that we have is that, even though a write transaction is linearizable
only when it commits, the consistent view of the program state seen by the transaction
corresponds to the program state at the time that the transaction started. Therefore, to
ensure that the transaction can be linearized at its commit time, we have to make sure
that the (already performed) transactions execution is equivalent to the (hypothetical)
transactions execution at commit time. Assuming that the transaction is deterministic,
its execution does not change if the publicly available values read during the transaction
remain the same.
Thus, to ensure that a top-level write transaction T is linearizable, T can commit
successfully if and only if none of the boxes in T-readSet changed after T startedin
this case, I say that T is valid. Note that, by this definition, a write-only transaction is
always valid, because its read-set is empty.
After ensuring that a top-level transaction T is valid, the commit of T proceeds by
renumbering the transaction, so that the new Ts number is one greater than the last successfully committed top-level write transaction. Then, each of the values in T-writeMap
is added to the corresponding history, tagged with this new number. Of course that the
check for transaction validity, the renumbering, and the additions to the boxes histories
should be all executed atomically, so that no concurrent commit executes between the
validity check and the end of the commit operation.
The commit of a nested transaction, regardless of its type, is much simpler: it just
needs to propagate the changes made during the nested transaction to the parents context. More formally, when a nested transaction Tc with parent Tp commits, the elements
of Tc-readSet are added to Tp-readSet, and, also, the mappings in Tc-writeMap
are added to Tp-writeMap, overriding any existing mapping in the parents map. Therefore, the commit of a nested transaction always succeeds, regardless of its type.
Finally, aborting either a top-level transaction or a nested transaction simply ends
the transaction and does not have any effect on the existing boxes. All the transactions
values in its write-map, if any, are lost.
A key result for this model is that a transaction T1 may conflict with another transaction T2, only if T1 is a top-level read-write transaction and T2 is an already successfully
committed top-level write transaction. If T1 is a top-level read-only transaction, a toplevel write-only transaction, or a nested transaction, then T1 will never conflict with any
other transaction.
4.3.4
In Figure 4.2 on the next page, I present the graphical notation that will be used to
illustrate the execution of transactions. In this figure, I show four top-level transactions,
which all start with number 3. Transaction T1 is a read-only transaction that corresponds
to the execution of the method call o1.m1(); it returns true. Transaction T2 is a
write transaction that commits successfully at instant t1 and it is, therefore, renumbered
with the number 4. Transaction T3 is a write transaction, also, but its commit fails
presumably, because it conflicts with T2. Finally, T4 is a transaction that aborts at
instant t2.
The successfully committed transactions in this simple example are the transactions
T1, with number 3, and transaction T2, with number 4. So, in this case, the transactions
numbers induce a total order on their executions. First we have T1, followed by T2. This
is the only linearization possible in this case.
In general, however, several transactions may have the same transaction number.
When that happens, often there are various possible equivalent linearizations for the
transactions. Nevertheless, in either case, each of the possible linearizations respects
the partial order specified implicitly by the transactions numbers. In fact, given the
partial order induced by the transactions numbers of all the successfully committed toplevel transactions, it is possible to determine all the equivalent linearizations of those
67
68
true
T1 o1.m1() 3
T2 3
4
T3 3
T4 3
Time
t1
t2
Figure 4.2: The graphical notation used to represent transactions. This notation is
an extension of the notation introduced in Figure 1.2 on page 8. Each horizontal bar
represents a complete transaction. The number at the beginning of the bar represents
the transaction number when it starts. A bar with a black ending corresponds to a
write transaction, and the black portion of the bar corresponds to the commit of the
transaction. If the commit succeeds, the number set at the end of the bar is the new
number of the transaction, after the commit; a commit failure (because of a conflict)
is represented by a white cross over the end of the black bar. A black cross at the end
of a white bar identifies the abort of the transaction. Additionally, on the left of each
bar, with a gray background, is an id that can be used to refer to the transaction.
transactions.
First, note that the definition of the commit operation forces each write transaction
to have a new number after its commita number which is greater than the number of
any of the previously committed transactions. Thus, it is not possible to have two write
transactions with the same number. Yet, it is possible to have various transactions with
the same number. For instance, in Figure 4.3 on the facing page, I show an example
with eight successfully committed top-level transactions, which have only four distinct
numbers: Transaction T1 has the number 2; Transactions T2 and T7 share the number
3; transactions T4 and T8 are both numbered 5; and transactions T3, T5, and T6 are
all numbered 4. Nevertheless, for each transaction number, there is at most a write
transaction; all the remaining transactions are read-only transactions.
Second, given a read-only transaction Tr and a write transaction Tw with the same
number, it follows from the semantics of the read and the commit operations that the
transaction Tw must be linearized before Tr: Because, if the transaction Tr reads some
box written by Tw, it must read the value written by Tw.
Therefore, from the combination of these two properties, we can derive the first condition that a total order among transactions needs to satisfy to ensure that the transactions
are linearizable: For each set of transactions with the same number, the (only one possible) write transaction must be linearized before all the other transactions; the remaining
69
T1 2
T2 2
T3 4
5
T4 2
T5 3
T6 4
T8 5
T7 3
Time
t1
t2
t3
t4
Figure 4.3: Example of 8 successfully committed top-level transactions. Transactions T2, T4, and T5 are write transactions. All the others are read-only transactions.
Ti ,Tj S,i ,j n (Ti ) < n (Tj ) n (Ti ) = n (Tj ) w(Ti ) (Ti Tj )
where n (T ) represents the number of the transaction T after the commit, w(T ) is true if
and only if T is a write transaction, and Ti Tj means that Ti appears before Tj in the
total order.
For the example shown in Figure 4.3, the serial order obtained by assuming that
read-only transactions are linearized at the time they start and that write-transactions
are linearized at the end of their commit is:
T1 T2 T7 T5 T3 T6 T4 T8
This serial order is one of the two that satisfies the previous condition. The other equivalent serial order is obtained by swapping the order of T3 and T6 .
70
T1 3
T2 3
T3 4
T4 4
Time
t1
t2
t3
A
1
t4
t5
A
2
1
3
2
1
4.3.5
With versioned boxes, old values are not lost; they are kept in the history of the box. This
is necessary so that running transactions can read the correct values even after later
transactions commit new values to a box. But, as old transactions finish, older values
become unreachable and, therefore, may be garbage collected.
To know precisely when old values may be garbage collected, I introduce the notion
of an active transaction. I say that a transaction is active if and only if it is either the
current transaction of some thread or the parent of an active transaction. Moreover, I say
that a value v of a history h is reachable if and only if: (1) v is the most recent value in
the history h, or (2) there is an active transaction T such that
versionOf (v) T-num < versionOf (successor (v))
where the function versionOf returns the version number associated with a value, and
the function successor returns the next value in a boxs history.
Values that are not reachable are unreachable and may be garbage collected. Note
that, once a value v becomes unreachable, no future transaction may make it reachable
again, because all the new transactions must have a version number that is at least equal
71
setBalance(150)
getBalance() 100
T1 acc.deposit(50) 3
getBalance() 150
T2 acc.deposit(50) 3
T3 4
getBalance() 100
setBalance(200)
setBalance(150)
Time
t1
acc
100
t2
acc
150
100
t3
acc
200
150
100
Figure 4.5: Parallel deposits on the same account using the STM model based on
versioned boxes. Transaction T2 conflicts with transaction T1, causing the deposit to
be restarted. The reexecution of the deposit operation corresponds to the transaction
T3, which commits successfully.
4.3.6
In Section 3.1, I used the banking domain to illustrate, with some examples, the difficulties
of concurrent object-oriented programming in Java. Now, after presenting my proposal
for an STM model, I shall discuss briefly to what extent this proposal solves the problems
identified.
Because the implementation of the versioned STM model is presented only in the
next chapter, here I discuss the examples at the model level only, by assuming the two
following changes: (1) the balance of each account is stored in a versioned box; and (2)
the methods deposit, withdraw, transfer, and totalBalance are atomicthat is,
the execution of each of these methods occurs within a transaction.
In Figure 4.5, I show the case of two concurrent deposits of an amount of 50 into
the same account acc. Like in the original case (see Figure 3.1 on page 33), when no
72
100
src.widthdraw(100)
trg.deposit(100)
Time
t1
src
100
t2
src
0
100
trg
0
trg
100
0
Figure 4.6: Execution of the method transfer during the execution of the method
totalBalance using the STM model based on versioned boxes. The use of versioned
boxes ensures that the execution of the method totalBalance can complete successfully with the correct answer, even though transaction T2 commits before the
totalBalance method reads the balance of the account src.
synchronization exists, the two deposits executions proceed in parallel, each one setting
the balance of the account to 150; those executions correspond to transactions T1 and
T2 in this case. But now, one of the depositsthe deposit that corresponds to transaction
T2fails, when the commit of the transaction T2 detects a conflict.
Transaction T2 is a write transaction because it changes the value of the box that
contains the balance of the account acc.2 But, to make the deposit, transaction T2
needs to read the value of the balance before changing it, also. Thus, the balances box
is added to the T2-readSet. Meanwhile, at instant t2, transaction T1 commits and
changes the balance of account acc. So, when T2 commits later, the validity check for
T2 detects that a box in its read-set was changed by other transaction after T2 started.
So, the commit of T2 fails and the deposit operation is restarted. The restart of the
deposit is shown in the figure as transaction T3, which completes successfully at instant
t3, changing the balance of acc to 200, as expected.
2
In Figure 4.5, I make a simplification of the notation. I use the same name acc to denote both the object
of the class Account and that same objects versioned box that keeps the balance. This same simplification
is used in subsequent figures, whenever the meaning is clear from the context.
The second example that I present here, in Figure 4.6 on the preceding page, shows
the major advantage of an STM model based on multiple versions of data. Whereas
the original version (shown in Figure 3.5 on page 36) produces incorrect results, and
traditional STM models cause a conflict between the two transactions, in the STM that
I propose in this chapter both operations complete successfully with the correct result.
Note that, even though transaction T2 commits before transaction T1 reads the balance
of account src, the changes made by T2 do not discard the old value of any of the boxes
changed by T2; the old values of the boxes are still available, so that T1 can access them.
The old values of the boxes that keep the balance of accounts src and trg become
unreachable only after T1 finishes.
Finally, the example of a failure during the deposit of a transfer operation, as given
in Figure 3.7 on page 39, is trivially solved using an STM: The failure of the deposit causes
the transfer transaction to abort. Given that transactions do not make changes to the
public state of the program until the transaction commits, there is nothing to undo when
the transaction aborts.
4.4
To implement the STM model proposed in the previous chapter, I need to provide answers
for two questions:
Obviously, the answers to these questions are not independent of each otherfor
example, the difficulty of implementing a particular set of constructs influences the choice
of constructs to use to support the model at the language level. Moreover, each of the
questions raises other questions that deserve as much consideration as these ones. For
instance, is the implementation efficient enough to be used for real examples?
In this chapter, I describe an implementation of the STM model based on versioned
boxes that serves as a reference implementation. This implementation is not meant to be
the most efficient, or to be as well integrated as possible with the programming language.
Rather, the implementation I describe here was designed to accomplish a good tradeoff
among the guiding principles described in Section 1.2.2 on page 4.
For instance, regarding the integration of the model with Javathe target programming language of this implementationthe best solution would be, probably, to augment
73
74
the syntax of Java with new keywords to declare versioned boxes and atomic blocks. This
solution, however, interferes with the tools that programmers use.
Likewise, from a performance-only standpoint, it would be preferable to implement
the support for the STM model at the compiler and virtual-machine level. But, again, this
solution interferes with the tools that programmers use. Furthermore, this solution is
harder to implement than others.
Therefore, the implementation that I describe herethe JVSTMis a pure-Java implementation of the model. Besides the guiding principles already mentioned, the design
of the JVSTM was influenced by the two primary goals that I want to accomplish with
this implementation. First, that the implementation serves as an operational semantics
of the STM model proposed. Second, that it can be used readily and effectively in the
implementation of real examples.
I do not present in this dissertation all the details of the JVSTM implementation. In
particular, I do not show the complete Java source code that implements the JVSTM.3
Rather, I describe the most significant design decisions and skip over the implementation
details that do not add much to the work presented in the remaining of this dissertation.
4.4.1
I start with a description of the JVSTMs API, which is all the information that programmers need to know to be able to use the JVSTM in their programs. One of the major
design decisions regarding the JVSTM was that its interface should be simple and easy
to use.
JVSTM is implemented as a pure-Java library that provides only two visible interfaces
for the programmers that use it.
One of the two interfaces is the public interface of the VBox generic class, which is
shown in skeletal form in Listing 4.1. This class implements the versioned boxes of the
3
The complete source code for the JVSTM is freely available at the JVSTM page [JVSTM].
STM model proposed in the previous section: Each instance of this class is a versioned
box, capable of holding a history of values. The method get corresponds to the read
operation, returning the current value of the box. The method put corresponds to the
write operation, changing the value of the box to a new value. Furthermore, according
to the semantics described in Section 4.3.1, if any of these methods is called by a thread
without a current transaction, then the method begins a new transaction, accesses the
box, and commits the just created transactionthat is, the execution of these methods is
atomic. Because the transactions created by these methods are either read-only or writeonly, they never conflict with other transactions. So, the commit of these transactions
never fails.
The class VBox allows us to create and use versioned boxes. Now, we need to be able
to create more complex atomic actions that use these boxes. For that, the JVSTM supplies
the class Transaction, which provides the basic operations shown in Listing 4.2. The
method begin creates a new transaction and makes it the new current transaction for
the executing thread. The new transaction is a top-level transaction if the thread that
calls the method begin has no current transaction; otherwise, it is a nested transaction,
child of the threads current transaction. The method commit tries to commit the current
transaction. If the commit operation detects a conflict between the current transaction
and any previously committed transaction, then the method throws an exceptionan
instance of CommitException, which is a subclass of Javas RuntimeException
and the commit operation fails. The method abort aborts the current transaction, as
expected.
These three operations are the basic building blocks needed to create atomic actions.
But, as we shall see below, often programmers will use simpler constructs to create the
atomic actions that they need in their programs.
To illustrate the basic usage of the JVSTM I show, in Listing 4.3 on the next page, the
JVSTMs version of an HelloWorld program. When this program is executed, it produces
the output shown in Listing 4.4 on the following page. The HelloWorld program creates
a versioned box with an initial value and then changes the box twice, printing the contents
of the box between each change. The first change to the box is made inside a transaction
that aborts. Thus, the change has no effect, and the second value printed is the same as
the first. The second change, however, succeeds, and the third value printed shows the
75
76
import jvstm.*;
public class HelloWorld {
public static void main(String[] args) {
// creates a box that holds a string
VBox<String> box = new VBox<String>("Hello world!");
// print the boxs contents
System.out.println(box.get());
// begin transaction to change the box...
Transaction.begin();
box.put("Hi!");
// ...but abort the transaction
Transaction.abort();
// the value did not change
System.out.println(box.get());
// change the box again...
box.put("Hello, again!");
// ...and the new value is printed
System.out.println(box.get());
}
}
Listing 4.3: Complete implementation of JVSTMs version of the HelloWorld program.
This program illustrates the basic usage of the two classes provided by the JVSTM:
The classes VBox and Transaction.
# java HelloWorld
Hello world!
Hello world!
Hello, again!
Listing 4.4: Output produced by the execution of the HelloWorld program. The
second value printed is equals to the first, meaning that the abort of the transaction
undid the write to the box.
class Account {
final VBox<Long> balance = new VBox<Long>(0L);
...
long getBalance() {
return this.balance.get();
}
void setBalance(long balance) {
this.balance.put(balance);
}
...
}
Listing 4.5: Changes needed in the class Account to use a VBox to hold the
Accounts balance. This listing shows only the parts of the class (shown previously
in Listing 3.1 on page 32) that need changes. Besides changing the field balance
from a long to a VBox<Long>, all the methods that access that field need to be
changed alsoin this case, just the methods getBalance and setBalance.
new value of the box. Note, also, that the calls to the methods get and put do not need
to occur inside a transaction, as explained above.
The program HelloWorld, however, is not a typical example of the programs that use
(or need) the JVSTM; in fact, this program is not even concurrent. Rather, the JVSTM is
meant to be used to create transaction-aware domain objects, which are then manipulated
within atomic actions. For instance, we may reimplement the classes Account and Bank
(originally shown in Listing 3.1 on page 32 and in Listing 3.3 on page 35), to make the
instances of the class Account transactional objects and the methods of the class Bank
atomic.
In general, to use the JVSTM, programmers need to do only two things: (1) use
versioned boxes (instances of the class VBox) to hold all the state of the program that
may change during the programs execution, and (2) use the Transactions methods to
delimit the operations that should execute atomically.
For instance, in Listing 4.5, I show the minimal changes needed to transform the
class Account into a transaction-aware class: We need to replace the classs field by
an instance of the class VBox, and, consequently, replace the expressions that read and
assign to the field by calls to the appropriate get and put methods.
The new class Account, however, is not complete, because the methods deposit
and withdraw should be atomic, or else we may have the problem depicted in Figure 3.1
on page 33. To make these methods atomic, we may use the methods from the class
Transaction, but it is not sufficient to call the method begin on method entry, and to
call the method commit on method exit. Remember that the call to the method commit
can throw a CommitException, because of a conflict. Thus, we need to handle that
77
78
class Account {
...
void deposit(long amount) {
while (true) {
Transaction.begin();
boolean txFinished = false;
try {
setBalance(getBalance() + amount);
Transaction.commit();
txFinished = true;
return;
} catch (CommitException ce) {
Transaction.abort();
txFinished = true;
} finally {
// handles other kinds of non-normal termination
if (! txFinished) {
Transaction.abort();
}
}
}
}
...
}
Listing 4.6: Implementation of an atomic version of the method deposit. This implementation uses only the basic begin, commit, and abort operations to implement the intended atomicity semantics. This code handles the case of a transaction
conflict, which makes the method commit to throw a CommitException, by reexecuting the method again. If the methods original body fails for any other reason,
then the transaction aborts.
exception and to restart the execution of the method, as many times as needed, in case
of a conflict.
In Listing 4.6, I show an implementation of the method deposit that implements the
expected atomicity semantics of the STM model, by using the basic JVSTM transaction
operations. The original body of the method is shown in boldface; the rest of the methods
new body is the idiomatic code needed to handle possible transaction conflicts, in which
case the original methods body is reexecuted.4
Because the code needed to implement atomic actions correctly is so verbose, the
JVSTM provides a method annotation to make it simpler: the annotation Atomic. Classes
that use this annotation should be post-processed to wrap the annotated methods bodies
with the necessary Transactions method calls. In Listing 4.7 on the facing page, I
4
In fact, when the original body of the method may change any of the methods parameters, this idiom is
not correct. In that case, we should use an auxiliary method to execute the original methods body.
class Account {
...
@Atomic void deposit(long amount) {
setBalance(getBalance() + amount);
}
...
}
Listing 4.7: Use of the annotation Atomic to make the method deposit atomic.
This implementation uses only a method annotation to implement the same semantics
of the implementation shown in Listing 4.6. The necessary code to support the
atomicity is introduced by post-processing the byte code.
show an implementation of the method deposit that uses this annotation. Using this
annotation, it is now trivial to make the method withdraw, as well as the Banks methods
transfer and totalBalance, atomic: We just need to annotate each of the methods
with the Atomic annotation.
Finally, note that the execution of an atomic method creates either a nested or a toplevel transaction, depending on whether the method was called during an existing current
transaction or not, respectively. For instance, when the method deposit is called by the
method transfer, its execution creates a nested transaction, because both the method
deposit and the method transfer are atomic. Naturally, when the nested transaction
commits, its changes are merged into its parentthe transfers transaction, in this
caseas expected. This compositional nature of transactions is a fundamental property
to implement rich transactional domain models.
4.4.2
The implementation of the JVSTM relies on the revised semantics for the Java Memory
Model that is now part of Java 5.0 [Gosling, Joy, Steele, and Bracha, 2005, Chapter 17].
In this section, I give an overview of the fundamental concepts of this new memory model
that are necessary to understand the implementation of the JVSTM.
The original semantics of the Java memory model was designed to give some safety
guarantees regarding the execution of a multithreaded Java program while allowing, at
the same time, an efficient implementation in multiprocessor machines. This original
semantics of the Java memory model, however, suffered from a series of flaws that made
its use awkward and error-prone [Pugh, 1999]. Thus, to solve these problems, Java 5.0
adopted a new and improved version of the memory model [Manson, Pugh, and Adve,
2005].
The Java memory model specifies how memory operations in a multithreaded Java
program appear to take effect. For instance, to allow for compiler and hardware optimiza-
79
80
tions, not every write to a memory location performed by a given thread needs to become
immediately visible to reads occurring in parallel threadsfor example, because the write
operation was reordered with respect to other operations, or because the write was performed to the processor cache which has not been flushed to main memory yet. Thus,
to understand how two or more threads may communicate through shared-memory, it
is crucial to understand the semantics of the memory model. In this aspect, the new
memory model for Java is much more intuitive and easy to use than the original.
The most important guarantees of the Java memory model in what concerns the
implementation of the JVSTM are the following:
Writes to and reads of references and primitive types (other than long and double)
are always atomic. This means that a thread that reads from an int variable, for
instance, will always get a value that was necessarily assigned to that variable, even
if it may not be the last value.
The fields declared as final that are correctly initialized during the construction
of an object are immutable and thread-safe. That is, once the constructor returns,
all the writes to final fields must be visible to all the threads and, thus, it is safe
to access these fields from other threads without synchronization.
Variables declared as volatile provide a synchronization point in a program:
A write to a volatile variable synchronizes with all the subsequent reads of that
variable by any thread. An important consequence of this semantics is that, if a
thread t writes to a volatile variable v, then all the writes made by t (even for normal
variables) before the write to v become visible to any other thread that reads v after
the write made by t.
This semantics of volatile variables is crucial to allow us to guarantee that the changes
made to memory by one thread are consistently seen by the remaining threads. The use of
volatile variables, however, should not be made lightly. Because of their semantics, reads
of and writes to volatile variables increase the cache-coherency traffic in a multiprocessor
system, which may result in a significant performance penalty.
4.4.3
81
class VBox<E> {
VBoxBody<E> body;
...
}
class VBoxBody<E> {
final E value;
final int version;
final VBoxBody<E> next;
...
}
Listing 4.8: Structure of the classes VBox and VBoxBody. These classes follow
the Handle/Body idiom to implement versioned boxes. All the VBoxBodys fields are
final to ensure that bodies are immutable and, thus, thread-safe.
B
body:
next:
value:
next:
2
version:
value:
next: null
87
version:
value:
23
version:
Figure 4.7: Structure that represents a versioned box with three values in its history. The box on the left is the VBoxs instance. The three boxes on the right are
the VBoxBodys instances. These objects represent the versioned box B depicted
in Figure 4.1 on page 63.
number, and a reference to the body that maintains the previous value in the boxs
history. Through this reference, the bodies of a versioned box form a linked list, sorted
in descending order of their version number. The version number in each body is the
number of the transaction that committed the body. In Listing 4.8, I show the structure
of these two classes, and, in Figure 4.7, I show the four objects needed to represent a
versioned box with three values in its history (see Figure 4.1 on page 63).
All the fields in the class VBoxBody are final, which makes the bodies immutable
and, therefore, thread-safe. This is essential to ensure that any thread that accesses a
body sees that body properly initialized; in particular, with the correct version number.
The sequence of bodies accessible through a box represent only the values successfully committed to that box. Thus, if a box was written during a transaction, but that
transaction has not committed yet, the value wrote to the box is not in its sequence of
bodies. Rather, the new value is kept in the private memory of the transaction, as we
shall see in the next section. This new value, however, is the current value of the box for
that transaction, and it should be the value returned by a call to the method get, if that
call is made during the same transaction. So, none of the two VBoxs methods, the get
and the put methods, accesses the body of a box directly. Instead, each one delegates
its work to the current transaction.
82
class Transaction {
static volatile int lastCommitted = 0;
int number;
Transaction parent;
Map<VBox,VBoxBody> readMap = ...;
Map<VBox,Object> writeMap = ...;
}
Listing 4.9: Some of the fields of the class Transaction.
The method get asks for the current value of the box to the current transaction.
The value returned by the transaction may be one of the values of the boxs history, if
the box was not written yet during the current transaction, or a value that exists only
in the private memory of the transaction, if the box was previously written during that
transaction. The method put simply asks that the transaction records a new value for
the box in the transactions private memory.
4.4.4
Implementation of Transactions
In Listing 4.9, I show some of the fields of the class Transaction. The static field
lastCommitted, which keeps the number of the last committed transaction, works as
a global counter to give an initial number to each new top-level transaction. The value of
this static field changes only when a top-level write transaction successfully commits, in
which case it is incremented by one.
The volatile field lastCommitted works as a synchronization point that affects all
the transactions in the JVSTM. The implementation of the JVSTM ensures that: (1) a
write transaction writes to the lastCommitted field only after it has performed all
the changes in shared-memory that are needed for a commit; and (2) a new transaction
reads of the lastCommitted field before executing any of its shared-memory operations. Given the semantics of volatile variables, this implementation ensures that all the
changes made by the commit of a transaction t become visible to all the transactions
that start after t has written to the field lastCommitted. The (atomic) write to the field
writeMap, mapping the box to its new value. If, during the same transaction another
value is written to the same box, then that new value overrides the previous value in the
map. The writeMap does not need to be thread-safe because no other thread can access
this map.
When a box is read, the transaction consults first its writeMap to see whether a new
value exists for the box. This search is performed recursively in the transactions parent
until either a value is found in some writeMap, or no parent transaction exists. If no
value is found in any of the ancestors writeMaps, then the transaction searches for the
appropriate value in the boxs sequence of committed bodies.
The search for the correct value in the sequence of bodies of a versioned box is a
linear search in that sequence. The value to return corresponds to the body with the
higher version number which is less than or equal to the current transactions number.
Because the sequence of bodies is sorted in descending order of version number, the
search stops once a body with a version number less than or equal to the transactions
number is found. Typically, the search stops at the first element of the sequence; only if
a concurrent transaction committed a new value for the box after the current transaction
started, will the search need to go further in the sequence of bodies.
If the box has already a new value committed by a concurrent transaction, then this
transaction will read an older value, which may cause a conflict for this transaction if
it ever writes to some box and then tries to commit. But, if the current transaction is
a read-only transaction, it may proceed and commit successfully. Thus, the only case
in which we could cause the failure of the current transaction (if we read a box with
newer committed values), is when we know already that the current transaction is a write
transaction. It is not clear, however, that it is worthwhile to perform this test every time
we read a box. If the probability of a conflict is low, then it might be best to skip that test
and detect the conflict only at commit time; this is the approach that I use in the current
implementation of the JVSTM.
The field readMap implements a transactions read set. A transaction inserts a mapping into the readMap, only when a boxs public body is consulted by the transaction,
after trying first all the transactions writeMaps. In this case, the transaction records in
the readMap that the box was read, and what was the body read for that box.
In Figure 4.8 on the next page, I show an example of one box, identified as box A,
and two transactions, T1 and T2, that access the box A. Note that two new values exist
for box A: one in each of the transactions writeMap. Nevertheless, these values are not
accessible through the box, because neither of the transactions committed yet. Thus,
these values are only visible in their corresponding transactions.
The values stored in the writeMap of a transaction are added to the sequence of
bodies of their corresponding boxes, only when the transaction commits, and only if the
transaction is valid. In Figure 4.9 on page 85, I show what happens if transaction T2
commits. Transaction T2 is valid, because it is a write-only transaction (its readMap is
empty). So, the commit of the transaction proceeds by renumbering the transaction (from
4 to 5), and by committing a new body for each of the entries in the writeMap (in this
83
84
A
body:
T1
number:
next: null
value:
readMap:
100
version:
writeMap:
T2
number:
150
readMap: empty
writeMap:
Figure 4.8: Values stored in the Transactions fields readMap and writeMap.
The box A contains only one body in its history, with the value 100. That value was
read by transaction T1 before the new value 150 was written to the box. Because the
readMap of transaction T2 is empty, either T2 never read the box A, or the read was
done only after writing the value 0 into it.
85
A
body:
T1
number:
next: null
value:
readMap:
100
version:
writeMap:
150
next:
T2
number:
value:
readMap: empty
writeMap:
version:
0
5
Figure 4.9: Final result after transaction T2 commits. The cross on the link between
the field body of box A and its original body represents the fact that this link no longer
exists. Instead, the field body now points to the new body created by transaction T2,
which is shown on the bottom-right corner of the figure. The structures of transaction
T2 are shown in gray to represent the fact that the transaction finished.
4.4.5
Atomic Commits
The commit of a top-level write transaction is the only place in the JVSTM where the
shared state of the program is changed. Thus, at least during such commits there must
be some form of synchronization. In fact, as we saw in Section 4.3.3, the commit of a
write transaction must execute atomically. But, what does this need for synchronization
entails? And to which extent do we need synchronization between the various JVSTMs
transactions?
The careful use of the volatile field lastCommitted, as described in the previous section, allows that transactions proceed without any other form of synchronization during
their entire execution, up to (but excluding) the commit operation. In fact, even when a
transaction is committing new bodies for the boxes that it wrote to, the remaining transactions may continue to access those boxes without any kind of synchronization; not even
some form of volatile-like memory barrier, given that the field of a box is not volatile.
To see why, consider what may happen if a transaction t1 is committing a new body for
a box b while another transaction t2 is reading that boxs history to access the box value.
Given that writes to and reads of references in Java are atomic, either the transaction t2
gets the new body for box b, or the old one; there is no other alternative. The value of b
in t2 should be the old value, given that t2 started before t1 finished its commit. So, the
problem, if it exists, is when t2 gets the new body for b. If that happens, however, t2 will
skip over that body and access the old body either way because the version of the new
body is necessarily higher than the version of t2. Also, the access of t2 to the new body
is perfectly safe because bodies are immutable and, therefore, thread-safe.
86
T1 2
commit()
T2 2
commit()
5
T3 2
commit()
6
T4 2
T5 2
commit()
Time
t1
t2
t3
t4
t5
Figure 4.10: Several write transactions committing at the same time. This figure
shows that, if multiple top-level write transactions try to commit at the same time,
then all but one of the transactions must wait until it gets hold of the exclusive
commit lock.
4.4.6
The implementation of transactions described above registers all the boxes that are read
during the transaction in the transactions readMap . This recorded information is necessary to validate the top-level write transactions when they commit. But, for read-only
transactions, all this registering is useless work, and, worse, constitutes a significant
overhead, both in memory, to store all the maps entries, and in time, to update that
map. Moreover, when we read a box, the transaction searches through the hierarchy of
transactions for an existing body in any of the hierarchys writeMaps, again introducing
a significant overhead. Therefore, I would like to eliminate these overheads for the common case of read-only transactions. Unfortunately, we do not know beforehand whether
87
88
Transaction
ReadWriteTransaction
NestedTransaction
ReadTransaction
TopLevelTransaction
4.4.7
To conclude the discussion of the most relevant aspects of the JVSTM implementation, I
describe in this section how the JVSTM implements the garbage collection of old values
from the boxes histories. As we saw in Section 4.3.5, old values are needed only as long
as there are active transactions that may need to access them. Thus, when an old value
becomes unreachable, we can discard that value from the boxs historythat is, from the
sequence of bodies.
A corollary of the definition of unreachable values is that, if a boxs history value is
unreachable, then all the previous versions in that same history are unreachable, also.
Therefore, to discard an unreachable value, we may simply trim the tail of the sequence of
bodies, from the point where the unreachable value occurs forward. This is accomplished
by setting the field next of the preceding body to null: After this trimming, all the
VBoxBodys objects that were in the tail of the body that was trimmed become garbage
collectable by the Java runtime. In the following, I refer to this trimming process as the
cleaning of a body.
Now that we know how to garbage collect unreachable values, we just need to know
how to find those unreachable values, and when to run the garbage collection process.
Starting with the latter problem of knowing when to run the garbage collection process,
it follows from the definition of an unreachable value that values may become unreachable
only when a top-level transaction finishes (either with a commit or an abort). Thus, in the
JVSTM, the cleaning of unreachable values occurs as part of the finishing of a top-level
transaction: When a top-level transaction finishes, it checks whether its finishing has
made some values unreachable and, if that happens, it cleans those values.
Yet, the finishing of a top-level transaction makes some values unreachable only if
certain conditions are met. Namely, assuming that the finishing transaction is T, both
of these conditions must hold: (1) the current value of the lastCommitted counter is
greater than T-num, and (2) there is no other active transaction with a number less than
or equal to T-num.
If the first condition holds, then we know that all the future transactions will have
a number greater than T-num. Moreover, if the second condition holds, it means that
the finishing transaction is the last active transaction with the number T-num. So, after
the finishing of the transaction, some values will necessarily become unreachable. The
question now is to find which values become unreachable.
The obvious brute force solution to this problem is to sweep all the boxes, cleaning
up any unreachable values found. Yet, this solution forces us to keep a list of all the
boxes in the program. Moreover, it is unnecessarily inefficient, because most of the boxes
do not need any sweeping at all: For instance, boxes with only one value do not have
unreachable values. So, an improvement over the previous strategy is to use a list of
boxes which are candidates for sweeping. We put boxes into this list when they get a new
value, and take them out of the list when they are cleaned up (if they end up with only
one value). Nevertheless, this strategy may still examine too many unneeded boxes.
89
90
The key observation that allows the JVSTM to implement a more efficient garbage
collection process is the following: All the boxes written by one transaction need cleaning
exactly at the same time. To see why, consider that the transaction Ti writes new values
for the boxes B1 , B2 , . . . , Bn . Then, when Ti commits, each of the boxes B1 through Bn will
have a new value tagged with the version number assigned to Ti by the commit operation.
Assume that this number is i. Thus, the previous value of each of the boxes B1 through Bn
becomes unreachable when all the active transactions with a number less than i finish
that is, the previous values become unreachable all at the same time, because the new
value of each box was created at the same time, also.
This answers our question of knowing which values become unreachable when the
finishing of a transaction T triggers the garbage collection process: We need to clean
up, at least, all the bodies committed by the transaction T-num + 1. I say at least,
because the finishing of a transaction T may, in fact, cause that various other values
become unreachable. Consider that, while T was running, n top-level write transactions
committed successfully. Then, the finishing of T may cause that all the values that were
updated by the commits of all those n transactions become unreachable.
Therefore, the JVSTM implements the garbage collection of unreachable values by
keeping track of which transactions are active at a given instant and which boxes need
cleaning for a given transaction number. The difficulty of implementing this approach is
in doing it without introducing two much synchronization overheads in the bookkeeping
of this information. We need synchronization for this bookkeeping because transactions
start and finish concurrently.
The JVSTM uses the class ActiveTxRecord to keep track of all the bookkeeping
information necessary for the garbage collection of unreachable values. The fields of an
class ActiveTxRecord {
final int txNumber;
final AtomicReference<List<VBoxBody>> bodiesToClean;
final AtomicInteger running = new AtomicInteger(1);
volatile ActiveTxRecord next = null;
...
}
Listing 4.10: The fields of the class ActiveTxRecord. This class uses the atomic
variables provided by the standard java.util.concurrent.atomic package.
There may, however, be active transactions with a lower number; so, it is not sufficient to look at the value of this field to know when to clean up unreachable values.
A reference to the next record, in the field next. The field next starts with the null
value when the record is created, and is assigned at most once, when a new write
transaction commits and creates a new record, in which case this field is assigned
to that new record. So, this field creates a linked list of records, with older records
pointing to newer records.
The key idea underlying the JVSTM implementation is to keep track of the count of
active transactions for each possible transaction number and, when that count reaches
zero, to clean up the bodies of the next transaction record that point, now, to unreachable
values. To accomplish this, the JVSTM updates this bookkeeping data structure in each
of the following cases:
When a new top-level transaction starts, in which case we must increment the
running count on the record that corresponds to the starting transactions number.
When a top-level transaction finishes (either after commit, or on abort), in which
case we must decrement the running count on the record that corresponds to the
finishing transactions number.
When a top-level write transaction commits, in which case we create a new record
and link it to the most recent one. When a top-level write transaction commits, it
creates a new record with the new number of the transaction and with the list of
bodies committed by the transaction. The running count starts with the value 1
because there is one transaction running: the transaction that is committing. When
later in the commit process the transaction finishes, it decrements the running
count as any other finishing transaction.
The condition that triggers the cleaning process when a top-level transaction finishes is
reaching a combination of values for three of the records fields: (1) the field running has
91
92
a count of zero, which means that no other transaction with the same number is running;
(2) the field next has a non-null value, which means that all the future transactions will
have a number greater than the currently finishing; and (3) the field bodiesToClean
has a null value, which means that this record was already cleaned up and, thus, there
is no older transaction running, also.
As these values are all read and modified concurrently as part of the starting and the
finishing of transactions, we need to ensure that no data races exist, or that, if they exist,
they are safe. One way to guarantee the safety of these processes is to use locks to protect
the access to these fields in the critical regions. This solution, however, entails too much
synchronization. So, the JVSTM implements a lock-free alternative instead.
The lock-free algorithm that the JVSTM implements uses the atomic variables provided
by the package java.util.concurrent.atomic, which is part of the standard Java
libraries. In particular, the JVSTM implementation depends on the compare-and-swap
operations provided by these atomic variables to detect and recover from eventual data
races.
The highlights of the implementation are the following:
When a new top-level transaction starts it needs to get a transaction number and
to increment the corresponding records running count. Therefore, it starts with
the most recent record and speculatively increments its running count. By doing
so, it prevents that other transactions clean up this record (if the record has not
been cleaned up already). Then, it must check whether the next field is nonnull. If it is, then there is already a more recent transaction number. As each
new transaction must start with the newest transaction number, it backs off of its
speculative increment by decrementing the running count again (which may trigger
the cleaning process, given that the field next is non-null), and tries again with the
next record. The implementation of this operation is shown in Listing 4.11. This
algorithm may cause starvation on a thread that is trying to start a new transaction,
but only if successive write transactions commit in between. Thus, this algorithm
is lock-free, rather than wait-free.
When a top-level transaction finishes, it decrements the running count of the
record that corresponds to its number. If the count reaches zero, it needs to check
whether it needs to clean up the next record. So, it checks if the field next is
null; if it is, then there is nothing to clean up yet. If, on the other hand, the value
of next is non-null, then no new transactions may start for this record (given
the implementation described in the previous point) and it may have to clean the
next record. To see whether this is true, it checks again if the running count is
zero5 and if this record was cleaned up already, cleaning up the next record if both
5
It may not be, because between the first check and the check for the next field, one transaction may
class ActiveTxRecord {
// returns the number to give to the new transaction
int startTransaction() {
ActiveTxRecord rec = this;
while (true) {
rec.running.incrementAndGet();
if (rec.next == null) {
// if there is no next yet, then its because the rec
// is the most recent one and we may return its number
return rec.txNumber;
} else {
// a more recent record exists, so backoff
rec.decrementRunning();
// and try again with the new one
rec = rec.next;
}
}
}
}
Listing 4.11: The operation used during the start of a new transaction to find the
transactions number.
conditions are true. Finally, if the transaction cleaned up the next record, it should
see whether it needs to propagate the cleaning to the record following it. This is
needed because a late finishing transaction may be responsible for cleaning up a
series of records that were all waiting for its finish. The implementation of this
operation is shown in Listing 4.12.
Finally, the commit of a top-level write transaction is easier to deal with. When a
top-level write transaction commits, it creates a new record and sets the field next
of the most recent record to the newly created record. This change occurs within
the mutual-exclusion lock used for the commit operation and, so, is thread-safe
with regard to other commits; also, given that the field next is volatile, this change
synchronizes with any other thread reading the same field. Furthermore, changing
the field next in the commit of a transaction cannot trigger the cleaning process,
because the committing transaction is still running and its number is either equal
to the record that will see its next field changed (in which case, that record must
have a running count greater than 0), or it belongs to an older record (in which
case, the record with the next field updated is not cleaned up). Yet, after changing
the next field, the commit operation decrements the running count for its old
number, which may trigger now the cleaning process.
have started, thereby incrementing the count, and another transaction may have committed, setting the
next field to a non-null value.
93
94
class ActiveTxRecord {
void finishTransaction() {
if (running.decrementAndGet() == 0) {
// when running reachs 0 maybe
// it is time to clean our successor
ActiveTxRecord rec = this;
while (true) {
// it is crucial that we test the next field first,
// because only after having the next non-null,
// do we have the guarantee that no transactions
// may start for this record
if ((rec.next != null)
&& (rec.bodiesToClean.get() == null)
&& (rec.running.get() == 0)) {
if (rec.next.clean()) {
// if we cleaned up, move to the next
rec = rec.next;
// and repeat the test
continue;
}
}
break;
}
}
}
boolean clean() {
List<VBoxBody> toClean = bodiesToClean.getAndSet(null);
//
//
//
//
if
4.5
Related Work
Since the seminal paper on Transactional Memory from Herlihy and Moss [1993], and the
later proposal of a software realization of the same idea by Shavit and Touitou [1995], the
research on Software Transactional Memory remained mostly dormant until 2003, when
a couple of influential papers spurred again the interest in the area [Herlihy et al., 2003;
Harris and Fraser, 2003].
Unlike the original STM from Shavit and Touitou, which was lock-free, both the DSTM
proposed by Herlihy et al. [2003] and the STM from Harris and Fraser [2003] are based on
a weaker non-blocking guarantee: obstruction-freedom. But, by being obstruction-free,
these STMs are also simpler and more efficient.
Since then, the research on Software Transactional Memory has been immensely
active, with many researchers proposing new STM implementations. As a consequence
of all this activity, there is now a large number of STM proposals, covering a significant
portion of the design space. In [Marathe and Scott, 2004; Marathe, Scherer, and Scott,
2004], Marathe and colleagues make an initial comparison of the then existing approaches
to STM, but many other proposals exploring other options were made since then.
The versioned STM that I propose in this dissertation, originally presented in [Cachopo
and Rito-Silva, 2005, 2006], was the first STM to propose the use of multiple versions
for each transactional location. By using multiple versions, this STM is better suited for
long-running read-only transactions than the rest. This comes at the expense of more
memory overheads.
The use of multiple versions to increase the concurrency of transactional systems is
well known in the area of database management systems. Since the seminal work of
Bernstein and Goodman [Bernstein and Goodman, 1983] on multi-version concurrency
control, the technique of using multiple versions as the basis for optimistic concurrency
control was applied in several contexts. One such application was made by Graham and
Barker [Graham and Barker, 1994]. They presented a formalism for describing multiversion object base systems which is similar, at the model level, to my proposal. However,
their work is in the context of object databases, which have different concerns compared
to a programming language level software transactional memory.
The idea of keeping a history of values for each object whenever it is changed, rather
than replacing the old value, was used by Reed in the context of the distributed execution
of atomic actions [Reed, 1978, 1983]. However, many of his concerns regarding the
difficulty of synchronization on a distributed system do not apply in the context of STM.
Moreover, in Reeds approach, when an object is read, the history of the object may be
updated by that read operation, thereby introducing a point of synchronization among
concurrent read-only transactions, which defeats somehow the benefits of this approach.
95
96
4.6 Summary
4.6
Summary
This chapter describes a new Software Transactional Memory (STM) that, unlike other
STMs, uses multi-version transactional locations called versioned boxes.
The chapter starts with a brief introduction to some fundamental concepts of STMs
and then describes the rationale underlying the development of this new STM: To create an
STM suitable for the implementation of domain-intensive applications. More specifically,
to create an STM that handles well workloads that have a high read/write ratio and that
are composed mostly by medium to large transactions.
After describing the rationale, it describes the proposed Versioned STM model and
gives a detailed description of the models implementation in the Java programming language. The proposed STM model ensures the linearizability of a set of successful transactions, and supports (linear) nested transactions. Moreover, the model defines in which
conditions may a garbage collector free the old versions of each box.
The implementation describedthe JVSTMis a full-fledged implementation of the
STM model that provides a simple and easy to use API, thereby facilitating its immediate
adoption for the implementation of rich domain models. Moreover, the implementation
of the JVSTM shows that the versioned STM model is amenable to simple and efficient
implementations.
The chapter describes, also, a lock-free algorithm for keeping track of old values so
that they may be garbage collected when they become unreachable.
Finally, the distinctive features of this new STM model and its implementation are that
read-only transactions never need to synchronize with any other transactions, and that,
also, read-only transactions have the guarantee of being able to complete successfully.
97
98
Chapter 5
And yet, surprisingly, none of the more recently developed object-oriented programming languages provide such a construct. Unfortunately, the consequence of this oversight in the design of new programming languages is an undesirable burden for the
programmers that need to implement domain models, as we have seen in Section 3.3.
Thus, given the current state of the object-oriented programming languages, a possible
approach to simplify the implementation of domain models is to extend those languages
with a set of new constructs for implementing relations. This solution, however, has the
inconvenient that interferes with the development tools that programmers use and goes,
100
5.1
DMLs Rationale
The objects of a domain model have special needs, compared to the remaining objects of
an application, as we have discussed already in Section 2.1.4 and Section 2.1.5. These
special needs may include, for example, that they are stored in a database whenever they
change, or that they be accessed by multiple concurrent threads in a consistent way.
Most often, because of these needs, the classes that represent the domain objects must
follow some coding conventions, to ensure that their instances behave as expected. For
instance, if we are using the JVSTM described in Section 4.4 to make the domain objects
transactional, we must use instances of the class VBox for all the mutable fields of a
domain class.
In some cases, as when using the JVSTM, the code conventions are simple to follow.
But, as simple as they may be, they still must be applied manually by the programmers,
introducing unnecessary effort into the programming task, and opening the possibility of
errors. Thus, the idea of automating these programming tasks was the first incentive that
led me to the development of the DML: I wanted a simple way to specify that a class is
a domain class, and to have this class transformed automatically into a class that uses
versioned boxes for all its fields.
Nevertheless, this reason alone is not sufficient to justify the need of a new language.
In fact, transforming each of a classs field into a field of another type and changing the
code that accesses that field to use a different access expression, is well within the reaches
of Javas annotations and post-processing technologies.
The most important reason for the development of the DML language, however, was the
lack of support in Java for the implementation of associations between classes. To solve
this problem in a convenient way, I would need to extend the syntax of the Java language
101
102
Java Source
DML Source
DML compiler
Java Classes
Domain Classes
Sources
Java Source
Java Classes
Figure 5.1: The effect that the DML has on the tool chain typically used for Java
application development. The dashed line separates the two different scenarios of
tool chain usage. The scenario above the line corresponds to the situation where the
DML is not used. The scenario below the line illustrates the changes caused in the
tool chain by using the DML. Rectangles represent implementation artifacts of the
development process. The rectangles with a white background represent the artifacts
that are edited by the programmers, whereas the rectangles with a gray background
are tool-generated artifacts that the programmers never modify manually. The large
arrows represent the transformation of one kind of artifacts into another kind of
artifacts, performed by the execution of the development tools indicated inside the
arrow. Finally, the single arrow pointing from the Domain Classes Sources to the Java
Source indicates that the former artifacts may be used by IDEs to help programmers
in the development of the latter.
domain model. I decided to generate Java source code, rather than compiled classes, to
eliminate dependencies between this transformation process and other Java classes. As
we shall see, the Java source code generated by the DML compiler may need additional
application-specific classes to compile. Yet, the DML compiler itself does not need any
other application-specific Java class to perform the transformation. So, by generating
only source code, the DML compiler may be executed at any time during the development
process.
5.2
The syntax of the DML language is defined using a context-free grammar that I introduce
in the following sections. To present this grammar, I use the notation that is used in the
Java Language Specification [Gosling et al., 2005, Chapter 2]:
Nonterminal symbols are shown in italic type, with no spaces between words and
with the first letter of each word capitalized. For example, the following are nonter-
QualifiedName:
Identifier
QualifiedName . Identifier
defines the nonterminal QualifiedName as either an Identifier, or a QualifiedName,
followed by the terminal . and an Identifier. This rule is part of the syntactic
grammar for the DML language and defines the nonterminal QualifiedName as a
sequence of one or more identifiers (explained below) separated by dots.
The suffix opt, which may appear on the right-hand side of a rule, indicates that the
symbol that precedes the suffix is optionalthat is, that in fact the right-hand side
corresponds to two alternative right-hand sides, one where the symbol occurs and
another where the symbol does not occur.
A lexical grammar for the Java programming language is given in Chapter 3 of the
Java Language Specification [Gosling et al., 2005]. This grammar defines how sequences
of Unicode characters are translated into a sequence of input elements, which may be
white space, comments, or tokens. The tokens, which consist of identifiers, keywords,
literals, separators, and operators, form the terminal symbols for the syntactic grammar
of Java.
The DML is a much simpler language. For instance, in DML there are no literals nor
operators. DML uses only identifiers, a few keywords, and operators. Yet, because DML
must integrate seamlessly with Java, I use the lexical grammar defined for Java to define
the lexical structure for the DML language. Thus, the syntax of DML for such things as
comments, identifiers, and white space is the same as in Java.
Note that, even though the DML language does not use most of the keywords of Java
nor any literals, the identifiers used in a DML specification must follow the restriction that
they cannot be a keyword, a boolean literal, or the null literal, because these identifiers
will appear as identifiers in the Java source code generated by the DML compiler.
In the grammar of DML presented in the following sections I use the nonterminal
symbol Identifier as it is defined in the Java lexical grammar: Informally, as a sequence
103
104
DomainSpecification:
DomainDeclarationsopt
DomainDeclarations:
DomainDeclaration
DomainDeclarations DomainDeclaration
DomainDeclaration:
ValueTypeDeclaration
EntityTypeDeclaration
AssociationDeclaration
Listing 5.1: Syntactic rules for a domain specification. DomainSpecification is the
goal symbol for the DML syntactic grammar.
of Unicode characters that start with a letter, and is followed by any number of letters
or digits. Furthermore, I use the nonterminal symbol DecimalNumeral as it is defined in
the Java lexical grammar, also: Either as the digit 0, or as a sequence of digits starting
with a digit from 1 to 9. Finally, I use the nonterminal QualifiedName as defined by the
production given above. All the remaining nonterminal symbols are defined in one of the
grammar fragments shown in the following sections.
5.3
Domain Specification
The DML language allows us to represent only the structural aspects of a domain model. It
has no constructs to describe the behavior of a program. So, what the DML compiler reads
and processes should not be called a program. Rather, I call it a domain specification.
The syntax of a domain specification is defined by the grammar rules shown in Listing 5.1. The nonterminal symbol DomainSpecification is the goal symbol of the DML
syntactic grammar.
According to these rules, a domain specification is a sequence of zero or more domain
declarations, which, in turn, are either a value type declaration, an entity type declaration,
or an association declaration. The following sections describe, in detail, the syntax and
the semantics of each of these domain declarations.
Each domain declaration introduces a new name that may be used in other parts of the
domain specification to refer to the domain element introduced by this declaration. Thus,
the only restriction to the order of domain declarations is that the names are introduced
before they are used.
ValueTypeDeclaration:
valueType ValueTypeName AliasClauseopt ;
AliasClause:
as ValueTypeName
ValueTypeName:
QualifiedName
Listing 5.2: Grammar rules for the syntax of a value type declaration.
5.4
Value Types
In the DML language, I distinguish between value objects and entities, as described
in Section 2.2.3. Value objects, unlike entities, are immutable, are not persistent by
themselves (only as part of entities), and do not participate in bidirectional associations.
Therefore, the code used to implement a value type is significantly different from the code
used to implement an entity type.
Furthermore, often the value types used in a domain model are not specific of that
domain model. Instead, they may be used across many different domains. So, in many
cases value types are provided already by a third-party framework or toolkit. If, however, a
value type does not exist yet, the implementation of its structure is mostly trivial, because
value objects are immutable.
For these reasons, the DML language does not allow the specification of new value
types. Yet, value types are needed to define new entity types: All the attributes of an
entity type must be of some value type. That is why the DML language has value type
declarations.
New value types are introduced in a domain specification by value type declarations,
which follow the syntax specified by the grammar productions shown in Listing 5.2.
Each value type declaration introduces into the domain specification the name of a
value type. The definition of this value type, however, is made outside the DMLs domain
specification.
In its simplest form, a value type declaration is only the keyword valueType followed
by the fully-qualified name of a new value type. That name becomes a new valid name
that may be used wherever a value type is expected. When the alias clause is used, the
name that is introduced into the domain specification as a new value type is the name
that follows the keyword as; the real name of the value type is used only by the DML
compiler to generate the code with the appropriate types.
105
106
valueType
valueType
valueType
valueType
valueType
valueType
valueType
valueType
valueType
valueType
valueType
valueType
valueType
valueType
valueType
valueType
valueType
valueType
boolean;
byte;
char;
short;
int;
float;
long;
double;
java.lang.Boolean
java.lang.Byte
java.lang.Character
java.lang.Short
java.lang.Integer
java.lang.Float
java.lang.Long
java.lang.Double
java.lang.Number
java.lang.String
as
as
as
as
as
as
as
as
as
as
Boolean;
Byte;
Character;
Short;
Integer;
Float;
Long;
Double;
Number;
String;
Listing 5.3: Default value types in the DML language. The first eight value type declarations introduce the names of the eight primitive types in Java as value types. The
following eight declarations introduce the wrapper reference types, which, because of
the alias clause, should be used without the java.lang package qualifier. Likewise
for the last declaration, which introduces the name String.
As an example, I show in Listing 5.3 the value type declarations for the value types
defined by default in the DML language.
5.5
Entity Types
Entity types are the basic elements of a domain specification. Each entity type describes
the structure of a set of similar entities, which are the objects that represent the domains
state.
Entities hold their state in a set of attributes, which may change during the
execution of an application as the state of an entity changes. The value of each attribute,
however, must be a value objectthat is, the type of each attribute must be a value type.
Entities may refer to other entities only through the traversal of the associations between
their types.
5.5.1
EntityTypeDeclaration:
class EntityTypeName Superopt EntityTypeBody
EntityTypeName:
QualifiedName
Super:
extends EntityTypeName
EntityTypeBody:
;
{ AttributeDeclarationsopt }
AttributeDeclarations:
AttributeDeclaration
AttributeDeclarations AttributeDeclaration
AttributeDeclaration:
ValueTypeName Identifier ;
Listing 5.4: Grammar rules for the syntax of an entity type declaration.
class Bank;
Yet, in general, entity types have one or more attributes. So, typical examples of entity
type declarations are shown in Listing 5.5 on the following page.
107
108
class Client {
String name;
}
valueType a.business.api.Money as Money;
class Account {
Money balance;
}
class ClientAccount extends Account {
boolean closed;
}
Listing 5.5: Examples of entity type declarations in DML. Both Client and
Account are top-level entity types that do not inherit from any other type. The
ClientAccount type, however, is a subtype of Account. Also, the Account entity
type uses a value type introduced in the previous declaration.
5.5.2
I specify the semantics of a domain specification by prescribing the minimal set of Java
classes that a compiler of the DML language must produce when it compiles the domain
specification. Also, I prescribe which fields and methods each generated class must have.
Different compilers for the DML language may vary in the classes they produce. Yet, all the
compilers must conform to the minimal interface specified here, because programmers
depend on this interface to develop the rest of the domain model.
Therefore, in this section, I specify the semantics of an entity type declaration by
describing the minimal Java interface that a DML compiler must produce when it compiles
an entity type declaration.
We have seen in Section 3.3.1 that classes from a UML class diagram are implemented
naturally as classes in Java: Typically, each class of a class diagramcorresponding to
an entity type in DMLis implemented by a single class in Java.
In DML, however, an entity type is implemented by two classes, as depicted in Figure 5.2 on the next page. Each class implements one of the two aspects of a domain
entity:
The first classthe state classis abstract and implements the domain entitys
structural aspects. The DML compiler generates this class from the domain specification. Thus, programmers should not edit this class manually.
The second classthe behavior classextends the state class and implements the
domain entitys behavioral aspects. This class, unlike the state class, cannot be
109
AState
class A {
ValueType1 attr1;
...
ValueTypeN attrN;
}
DML Compiler
getAttr1(): ValueType1
setAttr1(ValueType1 val)
...
getAttrN(): ValueTypeN
setAttrN(ValueTypeN val)
Figure 5.2: The result of compiling an entity type in DML. An entity type A is transformed into two Java classes AState and A. The abstract class AState has a getter
and a setter for each of the entity types attributes. The gray background in the
class AState indicates that the DML compiler generates this class and, thus, that
programmers should not edit the class.
generated by the DML compiler, because the domain specification does not have
any behavior specification.1 Instead, the implementation of this class is the responsibility of the domain model programmers: This is the class where programmers
implement the behavior of the entity.
The DML compiler uses this compilation strategy of separating a class in two to avoid
round-trip problems: Whenever we change the domain specification, we must reexecute
the DML compiler to update the classes generated in previous compilations, but, because
programmers do not edit the state classes, the DML compiler may regenerate those classes
from scratch each time it runs.
The state class that the DML compiler generates from an entity type has both a getter
method and a setter method for each of the entity types attributes. The DML compiler
forms the names of these methods by concatenating the prefixes get and set, respectively, with the name of the attribute, after capitalizing the first letter of the attribute. So,
given an entity object obj, the method call obj.getAttrName() returns the objs value
for the attribute named attrName, whereas the call obj.setAttrName(newVal) sets
the value of the attribute to the value newVal. No other methods can access the attribute.
Even though a state class has all the attributes of an entity type and no abstract
method, it must be abstract, because it does not represent an entity. An entity contains both state and behavior, but the instances of a state class contain only the state.
Therefore, the state class is abstract to prevent the creation of instances of an incomplete
type.
1
In fact, the DML compiler generates an empty behavior class if the class does not exist yet, so that we may
compile the domain model without errors. If the class exists, however, the DML compiler does not modify it.
110
AState
...
class A {
...
}
A
DML Compiler
class B extends A {
...
}
BState
...
Figure 5.3: The result of compiling an hierarchy of entity types in DML. The Java
state class that results from the compilation of an entity type extends the behavior
class that corresponds to its supertype: In this case, the class BState extends A.
The class that represents both the state and the behavior of an entity is the behavior
class, which implements the behavior and inherits the state from the state class. Note
that the methods in the behavior class may access the state of an instance by using the
getter and the setter methods inherited from the state class.
As a matter of fact, the state class is meant to be used only as the superclass of
its corresponding behavior class. No other classes should extend it. Also, no fields or
methods or any other part of the code should refer to a state class as a type. Instead,
all the remaining code should use the types that correspond to behavior classes. For
instance, in Figure 5.3, I show that, if B is a subtype of A, the class that implements the
structure of B, the class BState, must extend the class A, rather than the class AState.
If the class BState extended the class AState instead of A, it would not inherit the
behavior of A, as expected.
Finally, there is only one restriction regarding the implementation of a behavior class
that it should not have any state of the entity type; the state belongs in the state class.
Besides that restriction, programmers are free to implement the behavior class in whatever
way they want. For instance, programmers may make the class abstract, if it is not meant
to have any instances; they may make the class implement any number of interfaces; or
they may add the methods they need to implement the entitys behavior. In particular,
they may override any of the methods that are specified in this section, which the behavior
class inherits from the state class.
5.6 Associations
5.6
Associations
In UML, relations are called associations and they may connect two or more classes to
represent a relationship that exists among the objects that the connected classes describe.
The DML language uses the name association, also, to represent relationships. But,
unlike UML, DML does not allow the specification of associations with an arbitrary arity;
in DML all associations must be binary. I chose to support only binary associations in
the DML language mostly for pragmatical reasons.
First, because even though the semantics of a generic n-ary relation is naturally
defined in mathematical terms as a set of tuples, the semantics of n-ary relationships as
a domain modeling construct is either ill-defined, or confusing and error-prone [Gnova,
Llorens, and Martnez, 2002].
Second, because the implementation of n-ary associations in the object-oriented programming paradigm is not that simple. In an object-oriented program, programmers use
binary associations to navigate in the object graph, going from one object to another by
traversing a linkthat is, an instance of an associationbetween the two objects. Typically, programmers implement such binary links in an object-oriented domain model as
references or pointers from one object to the other. When the association is not binary,
however, to find the object at the other end of a link, we need more than an object:
We need all the objects in the link but one. Thus, we no longer can use simple references between objects to implement such associations. But this is more of a concern
for the implementation of the DML compiler, which would need to generate the code to
implement the correct semantics (provided that we could agree on one). If we ignore the
implementation problems raised by non-binary associations, however, there are still usage problems: Traversing a binary link in a typical implementation of an object-oriented
domain model may be linguistically quite different from traversing links that relate more
than two objects.
The third pragmatical reason for limiting the DML to binary associations, was the
observation that associations among three or more entity types are seldom used in the
design or in the implementation of a domain model: Most probably because of the two
reasons above, programmers shy away from n-ary associations when they develop their
domain models.
The fourth and final reason is that limiting the DML to binary associations does not
prevent the implementation of a domain model. In the rare cases where programmers use
a non-binary association, it is possible to replace that association (often with benefits to
the design of the domain model) with an entity type and several binary associations: The
new entity type represents, as first-class entities in the domain model, the links of the
non-binary association that it replaces.
111
112
Therefore, following the fourth guiding-principle of Section 1.2.2, I considered that the
eventual benefits of supporting n-ary associations in DML were not worthy of the effort
necessary to implement them.
5.6.1
An association declaration, which adds an association to a domain specification, is a toplevel construct in the DML languagethat is, it is a construct that stands by itself, rather
than being part of, or having to appear subordinated to another construct.
This syntax for associations contrasts with how other approaches to the problem of
making associations explicit in the programming language propose to represent associations. Many of such approaches propose to represent associations with new constructs
that programmers should use in each of the associated classes. This solution, however,
splits the information about the association between the two classes, making both the
understanding and the maintenance of a domain model more difficult. Thus, I argue that
an association declaration should be a single construct, as it is in the DML language.
In Listing 5.6 on the next page, I show the grammar rules that specify the syntax of an
association declaration. This set of rules concludes the syntactic grammar of the DML
language.
The keyword association introduces an association declaration and is followed by
the name of the association, an identifier. As we shall see in the following section, the DML
compiler uses this identifier to name a static field in each of the classes that participate
in the association. Therefore, this name must be unique throughout the inheritance
hierarchy of each of those classes. To avoid name clashes, programmers should adopt a
naming convention that distinguishes the names of associations from the names of other
members of a class (e.g., inner classes).
In a relationship among several entities, each of the entities plays a role in the relationship. Thus, the DML language uses role declarations to identify the entity types that
participate in an association. As in DML all the associations are binary, an association
declaration has exactly two role declarations.
Each role declaration specifies the type of the entities that play the declared role in
the association, the name of the role, and the multiplicity of the role. Yet, both the name
and the multiplicity of the role are optional. If the name of a role is not specified, then
the association is not traversable in that directionthat is, it is not possible to reach
the entities that play that role from the entities in the other end of the association. I
shall discuss this further in the following section, where I present the semantics of an
association declaration.
If the multiplicity of a role is not specified, however, a default multiplicity of 0..1
5.6 Associations
AssociationDeclaration:
association Identifier RoleDeclarations
RoleDeclarations:
{ RoleDeclaration RoleDeclaration }
RoleDeclaration:
EntityTypeName playsRole Identifieropt RoleBody
RoleBody:
;
{ MultiplicityOption }
MultiplicityOption:
multiplicity MultiplicityValues ;
MultiplicityValues:
MultiplicityRange
MultiplicityValues , MultiplicityRange
MultiplicityRange:
MultiplicityUpperBound
MultiplicityLowerBound .. MultiplicityUpperBound
MultiplicityUpperBound:
*
DecimalNumeral
MultiplicityLowerBound:
DecimalNumeral
Listing 5.6: Grammar rules for the syntax of an association declaration. Each
association declaration has exactly two role declarations, identifying the two entity
types that participate in the relationship. The nonterminal symbol DecimalNumeral
is defined in the Java lexical grammar [Gosling et al., 2005] either as the digit 0 or as
a sequence of digits that start with a non-zero digit.
113
114
association AccountOwnership {
Client playsRole owner {
multiplicity 1; // it is equivalent to 1..1
}
ClientAccount playsRole account {
multiplicity 1..*;
}
}
association AccountGroup {
CheckingAccount playsRole checking {
multiplicity 1;
}
SavingsAccount playsRole savings {
multiplicity *; // it is equivalent to 0..*
}
}
Listing 5.7: Examples of association declarations in DML. Both associations are bidirectional one-to-many associations. Note that the role names and the multiplicities
match those specified in the class diagram shown in Figure 2.4 on page 24.
5.6.2
Like before, when I specified the semantics of an entity type declaration, I specify the
semantics of an association declaration by prescribing the minimal Java interface that a
DML compiler must generate for each association declaration.
In this case, however, the DML compiler does not need to generate any new classes to
5.6 Associations
implement an association declaration. Rather, it must generate new methods for each of
the entity types that play a role in the association.
The semantics that I specify here tries to respect the pragmatics of object-oriented
programming. Object-oriented programmers do not, as a common practice, implement
a binary association or the associations instancesthe linksas first-class objects in
the program. Instead, the object-oriented common practice is to implement a binary
association between two classes A and B, by adding to each of the classes A and B a
reference to the elements of the class in the other side of the association: In the class
A we add a reference to elements of B; in the class B we add a reference to elements of
A. These references, however, are not typically accessible by the classes clients. As we
have seen in Section 3.3.2, the usual approach is to provide methods that access these
references.
Thus, because programmers should use the public methods, rather than the private
references, the semantics that I specify here prescribes only which methods must a DML
compiler generate. It does not specify how the compiler should implement those methods.
It does not specify either, how the generated code should implement the associations
links, whether with references in each of the classes, or with any other solution.
To simplify the presentation below, given an association declaration with two role
declarations, I use the term opposite type of a role declaration to refer to the entity type
of the other role declaration. For example, given the following association declaration
association Rel {
A playsRole role1;
B playsRole role2;
}
I say that the opposite type of the first role declaration is B, and that the opposite type of
the second role declaration is A.
A compiler for the DML language must generate at most two sets of related methods for
an association declarationone set of methods for each of the role declarations that have
a role name specified. If a role declaration has no name, then the DML compiler should
not generate any methods for that role declaration. The methods generated from a role
declaration belong to the opposite type of that role declaration. They allow the traversal
from an instance of the opposite type to one or more instances of the role declarations
type.
Therefore, an association where both role declarations have a name is a bidirectional
association, whereas if only one role declaration has a name, the association is unidirectional. An association declaration where none of the role declaration have a name is
meaningless for a domain specification, because no code will be generated from it.
115
116
The signature of the methods that a DML compiler must generate for a role declaration
depends only on the properties of that role declaration:
The roles multiplicity determines the set of methods to generate. Although the
multiplicity option may have many different values, the DML compiler separates the
roles into two disjoint classes: (1) the roles that have a multiplicity upper-bound of
one; and (2) the roles that have a multiplicity upper-bound greater than one.2 The
multiplicity upper-bound of a role declaration is the maximum of the upper-bounds
of all the multiplicity ranges (there is at least one) in the role declaration. The upperbound of a multiplicity range of the form M..N is infinite, if N is the terminal symbol
*, and the value of the integer N, otherwise. I shall present below, separately, the
set of methods that must be generated for each of these cases.
The roles name determines the exact name of each method. The name of the role
with the first letter capitalized appears in all the methods names, either with a
prefix only, or with both a prefix and a suffix. Below, when I present the methods to
generate, I show in italic the part of the name of each method that must be replaced
by the roles name.
The roles type determines the type of the argument, or the return type of some of
the methods to generate.
5.6.2.1
If the multiplicity of a role declaration has an upper-bound of one, then an object of the
opposite type may refer to at most one object of the roles type. This is the simplest case
of an association, which is typically implemented with a getter and a setter method.
In Figure 5.4 on the next page, I show the signature of the methods that a DML
compiler must generate for a role declaration with a multiplicity upper-bound of one. Only
the first two methods, shown in boldface, are necessary to implement the association. The
other two methods may be trivially implemented by using the mandatory methods. Yet,
they make the class more convenient to use, and, therefore, more programmer-friendly.
2
Roles with a multiplicity upper-bound lesser than one do not make sense.
5.6 Associations
association RelAB {
A playsRole role {
multiplicity 0..1;
}
B playsRole;
}
117
BState
DML Compiler
...
setRole(A role)
getRole():A
hasRole():boolean
removeRole()
Figure 5.4: Methods that a DML compiler must generate for a role declaration with
a multiplicity upper-bound of one. The part of a methods name that depends on the
name of the role is shown in italic. The methods in boldface are the core methods.
The other methods are optional methods.
A, the method must eliminate that link before it creates the new link. Finally, the call to
the setter method with a null argument eliminates any current link that may exist for
the receiver of the method call.
The method getRole is the getter method. Given an instance of the class B, b, the
method call b.getRole() returns the instance of the class A that is related to b, if such
an instance exists, or null, otherwise. This method is the method that allows us to
traverse the association.
The method hasRole returns true if there is a link between the receiver of the method
call and an instance of class A. Otherwise, it returns false. Calling this method on an
object obj is equivalent to evaluate the Java expression obj.getRole() != null.
The method, however, may be implemented more efficiently by the DML compiler.
Finally, the method removeRole removes an existing link that may exist for the
receiver of the method call. It is equivalent to call the setter method with a null argument.
5.6.2.2
The difference between this case and the previous is that the object of the opposite type
may have multiple links, at the same time, with objects of the roles type. So, when we
create a new link, we do not remove any existing link, as in the previous case. The existing
links must be removed explicitly, by specifying which object is on the other end of the
link that should be removed. Furthermore, when we traverse the association, we may
reach multiple objects. Thus, the getter method, in this case, must return a collection of
118
AState
association RelAB {
A playsRole;
B playsRole role {
multiplicity 0..*;
}
DML Compiler
...
addRole(B role)
removeRole(B role)
getRoleSet():Set<B>
getRoleCount():int
hasAnyRole():boolean
hasRole(B b):boolean
Figure 5.5: Methods that a DML compiler must generate for a role declaration with
a multiplicity upper-bound greater than one. The part of a methods name that
depends on the name of the role is shown in italic. The methods in boldface are the
core methods. The other methods are optional methods.
objects.
In Figure 5.5, I show the signature of the methods that a DML compiler must generate
for this case. Like in the previous case, there is a set of core methods, and a set of
convenience methods.
The method addRole creates a new link between the receiver of the method and an
instance of the class B passed as argument to the method. If the argument of the method
is null, the method does nothing.
The method removeRole eliminates the link between the receiver of the method and
the instance of the class B passed as argument to the method. If no such link exists, or
if the argument is null, the method has no effect.
The method getRoleSet returns the set of instances of the class B that have a
link with the receiver of the method. The value returned by this method is always an
instance of a class that implements the java.util.Set interface, but it must satisfy
some conditions that are specified below.
Finally, the remaining three methods may be defined by the expressions to which they
are equivalent:
// the expression
obj.getRoleCount()
// is equivalent to the expression
obj.getRoleSet().size()
// the expression
obj.hasAnyRole()
// is equivalent to the expression
(! obj.getRoleSet().isEmpty())
// the expression
5.6 Associations
obj.hasRole(b)
// is equivalent to the expression
obj.getRoleSet().contains(b)
5.6.2.3
Bidirectional associations
When a new link is created for a bidirectional association, that new link must become
traversable in both directions, rather than in only one, regardless of how that link is
created. Likewise for the removal of a link.
A link is an instance of an association between two objectsthat is, it represents
that the two objects are related by the relationship that the association represents. A
link, however, is typically implemented as two separate references: Each of the objects
in the link keeps a reference to the other. Yet, when a link is created or eliminated both
references must be updated to reflect the change.
Thus, in a bidirectional association, we should be able to create the same link by
calling either one method for the object in one end of the link, or calling an equivalent
method for the object in the other end. For instance, if we merge the two declarations for
the association RelAB (see Figure 5.4 on page 117 and Figure 5.5 on the facing page), the
execution of the code a.addRole(b) should be equivalent to the execution of the code
b.setRole(a).
As a final example, consider that we have the following association declaration
association Rel {
A playsRole a { multiplicity 0..1; }
B playsRole b { multiplicity 0..1; }
}
Moreover, consider that we have two instances of the class A, a1 and a2, and two instances of the class B, b1 and b2. Between a1 and b1 there is a link, and another
link exists between a2 and b2. Thus, a1.getB() returns b1, b1.getA() returns a1,
119
120
5.6.2.4
The specification for the method getRoleSet, presented above, does not specify the exact nature of the value returned by the methodfor example, whether the value returned
is an immutable set or a mutable set. Yet, it is important to specify these details, so that
programmers know what they can and cannot do with the result returned.
In the DML language, the value returned by a call to the method getRoleSet is a
mutable set that is backed up by the association and the receiver of the method call.
This means not only that programmers may add and remove elements from the set, but
also that those changes correspond, in fact, to the creation and elimination of association
links. Furthermore, if a new link is added to or removed from the object that backs up
the set, the set reflects that change.
The advantage of having this specification is that we may use all the methods available
in the java.util.Set interface to manipulate the links of an object. For instance, we
may remove all the links of an object, o, with the code o.getRoleSet().clear(). Or,
probably more useful, we may iterate over the set and remove the elements that satisfy
some condition.
Finally, a consequence of this specification is that the method call o1.addRole(o2)
is equivalent to o1.getRoleSet().add(o2). Likewise for the method that removes a
link. Thus, the method getRoleSet is sufficient to implement a role declaration with a
multiplicity upper-bound greater than one.
5.6.2.5
Having different ways to create and to remove a link makes the domain model more
programmer-friendly, because programmers may choose what is more convenient for
them in each situation. Yet, all these equivalent approaches make the domain model
more complex, and, eventually, more confusing, also. In particular, having various entry
points for the same functionality raises the question of which method should we override
if we want to customize that functionality.
Imagine that we want to execute some code whenever a new link for the association
RelAB is created between an instance of the class A and an instance of the class B.
Should we override the method setRole in the class B? That would work for the links
created by that method. But a link may be created by calling the method addRole in
the class A, also, and we do not know whether that method calls the method setRole
or not. Yet, if we override the method addRole instead, we have similar problems. Even
if we override both methods, we are not sure to catch all the link creations because links
may be created when we add objects to a set returned by the method getRoleSet.
5.6 Associations
interface Association<C1,C2> {
void add(C1 o1, C2 o2);
void remove(C1 o1, C2 o2);
Association<C2,C1> getInverse();
void addListener(AssocListener<C1,C2> listener);
void removeListener(AssocListener<C1,C2> listener);
}
interface AssocListener<C1,C2> {
void beforeAdd(Association<C1,C2> assoc, C1 o1, C2 o2);
void afterAdd(Association<C1,C2> assoc, C1 o1, C2 o2);
void beforeRemove(Association<C1,C2> assoc, C1 o1, C2 o2);
void afterRemove(Association<C1,C2> assoc, C1 o1, C2 o2);
}
Listing 5.8: The Java generic interfaces Association and AssocListener. The
DML compiler uses objects of the Association type to implement an association in
Java. Programmers may customize the association operations by adding listeners to
an association object.
To solve this problem, I propose to add yet another way to create and to remove links
from an association: A common point in the code that must be executed whenever a link
is created or removed, regardless of how that is done.
The key idea is to have an object that represents an association. The basic operations
of that object are a method to add a new pair of objects to the association, and a method
to remove a pair of objects from the association. The first method creates a new link, the
second removes an existing link. These methods must be called to create or to remove a
link. In fact, all the methods described previously that create or remove links may call
the add or the remove operations of this new object. The semantics must be the same.
The association object gives us a central point of execution for the operations that
change an association. Now, we need some mechanism to specialize those operations.
Because each association is represented by an object, rather than by a class, we cannot
use standard class inheritance and method overriding to do that specialization. Instead,
we may use listeners to do it. In Listing 5.8, I show both the interface Association
and the interface AssocListener that will allow us to specialize the creation and the
removal of a link.
The method addListener adds a new listener to an association object, whereas
the method removeListener removes a listener. Even though these methods may be
used at runtime to change the set of listeners for an association object dynamically, the
common usage is to add one or more listeners to an association object at class-load-time
and use that set of listeners throughout the entire program.
When an association object has some listeners registered, the methods add and
121
122
association Rel {
A playsRole a {
multiplicity 0..1;
}
B playsRole b {
multiplicity 0..*;
}
}
Figure 5.6:
remove call the appropriate methods of the registered listeners, following the same order by which the listeners were added to the association object. The method add first
calls the method beforeAdd of each listener, then creates the link, and, finally, calls
the method afterAdd of each listener. Mutatis mutandis for the method remove. Note
that a listener may cancel the creation (or the removal) of a link, if its beforeAdd (or
5.7
The specification of the DML language prescribes in detail the interface of the classes
that a conforming DML compiler must generate; it does not, however, dictate how that
interface should be implemented, even though it gives, now and then, some hints about
possible implementation strategies.
Therefore, different compilers for the DML language may generate different source code
to implement a domain specification: Either because they must generate domain models
with different properties, or simply because they use different implementation strategies.
In fact, the simplicity of the DML language is a conscious design decision to promote
experimentation with different implementation strategies, which may affect significantly
the performance and the memory footprint of an application. Given the reduced number
of constructs in the language, creating a new code generator backend for a DML compiler
should be well within the reaches of any software development team.
In this section, I give an outline of one of the code-generator backends that I implemented for the DML. I will not describe how I implemented the compiler or the codegenerator.
specification.
The backend that I describe here corresponds roughly to the one used in the FnixEDU
project [FenixEDU], which we shall see in Chapter 7. The code that this backend generates tries to accomplish a good equilibrium between two different requirements for the
generated code: efficiency, and readability. Efficiency, because it is used in a large project
with many entities, where efficiency was a concern. Readability, because programmers
may need to read the code generated by the DML compiler when they are debugging the
application.
Even though many other strategies exist for implementing a domain specification, I
believe that the implementation that I outline here provides a good starting base for many
projects.
5.7.1
Throughout this dissertation, I propose to implement domain models that exhibit transactional properties. So, it comes out naturally that the implementation that I describe here
generates a transactional domain, by leveraging on the JVSTMthe Java implementation
123
124
5.7.2
The DML specification already prescribes that an entity type must compile to a pair of
classesthe state class and the behavior classwith a getter and a setter for each of the
entity types attributes.
What is missing, then, is how to implement those methods. The usual approach used
to implement the attributes of an entity is to use one field for each attribute in the class
implementing that entity type. Each field holds the value of an attribute. To make the
class transactional, however, we need to wrap each value with a VBox, so that a change
in an attribute may become part of an atomic action.
For instance, in Figure 5.7 on the facing page, I show the implementation of the first
entity type declaration from the example given in Listing 5.5 on page 108.
The methods generated for an entity type declaration do not need the annotation
Atomic. They only read or write the box that corresponds to an attribute, so they are
atomic already.
class Client {
String name;
}
String getName() {
return this.name.get();
}
DML Compiler
5.7.3
Implementing Associations
The straightforward implementation of an entity type is a consequence of the natural mapping between entity types and Java classes. Unfortunately, this ease of implementation
does not extend to the implementation of associations. Therefore, the implementation of
associations requires more work.
To implement an association, we must generate a set of methods for each of the associations roles. We need, also, to create an instance of a class that implements the interface
Association (which is shown in Listing 5.8) for each of the classes participating in the
association; as prescribed by the DML specification, these Association instances are
stored in static fields, and, so, there is not much variability here.
Where we may have different implementation strategies is on the implementation of the
Association interface and on the implementation of the roles methods, provided that
the implementation chosen satisfies the requirements of the DML specification. Namely,
that, regardless of which methods are used to create or remove links from an association,
they invoke the appropriate methods in the association listeners that were previously
added to the associations instance. In fact, the DML specification prescribes a set of
redundant ways for creating and removing links, but requires that all those redundant
ways of doing things be semantically equivalent.
The simplest way of ensuring this equivalence is to implement the creation (or removal)
of a link in a single kernel method, and make all the remaining equivalent methods call
that kernel method. The kernel method is responsible for creating (or removing) a link,
ensuring that both sides of the link are updated consistently. In my implementation, the
kernel methods are the methods add and remove from the interface Association.
125
126
association AccountOwnership {
Client playsRole owner {
multiplicity 1;
}
ClientAccount playsRole account {
multiplicity 1..*;
}
}
DML Compiler
5.7.3.1
When we create a link between two objects, we need to store the information that they
are linked somewhere. Two common choices are: (1) making the link a first-class object
and keeping a global set of links for each association, and (2) storing with each object the
object or set of objects with which it relates. The first solution may be preferable in terms
of memory consumption when relations are sparse, but incurs in a performance penalty
when we need to know which objects are related to another object. The second solution is
faster in this latter case, but may require more memory and makes other types of relation
accounting more difficult to implement.
Given the interface prescribed by the DML specification, I opted for the second solution. Thus, each role declaration will generate a new field in the roles opposite class.
The purpose of that field is to hold either a single object or a set of objects, depending
on whether the roles multiplicity upper-bound is one or greater than one, respectively.
Additionally, to ensure the proper transactional semantics, the field must be wrapped
with a VBox, if for a single object, or use a transactional set, otherwise.
In Figure 5.8, I show an example where both types of fields are generated when an
association declaration is compiled. The class AssocSet is a transactional set that
ensures the semantics required by the DML specification for the sets returned for a role
association AccountOwnership {
Client playsRole owner { multiplicity 1; }
ClientAccount playsRole account { ... }
}
DML Compiler
Figure 5.9: Implementation of the methods for a role with a multiplicity upper-bound
of one. The setter simply calls the kernel method add from the Associations
instance.
with a multiplicity upper-bound greater than one. We shall see this class in more detail
in Section 5.7.3.3.
5.7.3.2
The set of methods to generate for each role depends on the multiplicity of the role. There
are two cases: roles with a multiplicity upper-bound of one, and roles with a multiplicity
upper-bound greater than one. In either case, however, there is a set of core methods
and a set of optional methods. Here, I describe the implementation of the core methods
only; the implementation of the optional methods results trivially from the core methods.
In the first caseroles with a multiplicity upper-bound of onewe have only two
methods: a getter and setter. To implement the getter, we just have to return the value
of the VBox that is used to store the value. The setter, however, creates or removes a
link and must, therefore, call the appropriate kernel method to accomplish that. I show
in Figure 5.9 an example of the code generated in this case.
In the second caseroles with a multiplicity upper-bound greater than onethe implementation is similar, except that now we have one more method to remove a link,
whereas in the previous case the setter took care of that too. I show in Figure 5.10 an
example of the code generated in this case.
127
128
association AccountOwnership {
Client playsRole owner { ... }
ClientAccount playsRole account { multiplicity 1..*; }
}
DML Compiler
Figure 5.10: Implementation of the methods for a role with a multiplicity upperbound greater than one. The methods addAccount and removeAccount simply
call the kernel methods add and remove from the Associations instance.
5.7.3.3
According to the DML semantics, the set returned by the method getAccountSet,
shown in Figure 5.10, must be backed up by the association and the instance of the
class Client on which it was called. That is, changes in the set should give rise to
changes into the associations links and vice-versa. The class AssocSet, which I outline
in Listing 5.10, implements this semantics.
We create an instance of AssocSet, by passing it an object corresponding to an end
of a link and an instance of an Association. The set stores these objects internally,
and when an element is added to or removed from the set, it calls the kernel methods add
or remove of the association object with that element and the stored end of the link.
The association object, however, must be able to add and to remove elements of the
set, as part of the creation or removal of a link. Yet, calling again the method add (or
remove) of the set would lead to an infinite loop. Thus, the class AssocSet provides
an alternative interface for adding and removing elements: the methods justAdd and
justRemove. These methods effectively add or remove the element from the underlying
set: an instance of the class VSet, which is a transactional set provided by the JVSTM.
Obviously, these low-level methods are meant to be used only by the association class.
129
130
5.7.3.4
All the methods shown above delegate into the association object the responsibility of
actually creating and removing links, in its add and remove methods, respectively.
To create or remove a link, these methods must perform several operations. For
instance, to create a link for a bidirectional association, we have to update both of the
objects that we want to link. Furthermore, in some cases, the creation of a link may
result in the elimination of previous links.
The exact sequence of operations depends on the multiplicities at both ends of the
association. So, we need to distinguish, at least, the three following cases: one-to-one,
one-to-many, and many-to-many associations. In fact, given that we need to update each
of the two objects of a link so that they point to the other object, these three cases result
from the composition of only two distinct ways of updating an end of a link: when we
have a single object, and when we have a set of objects.
Therefore, we may implement an association class in an entirely generic way, as
shown in Listing 5.11. The DirectAssociation class delegates the work of updating
the ends of a link to objects of the Role type. Creating different instances of this class,
with appropriate instances of the Role type, allows us to implement the various types of
associations.
Before we look at the Role type, note the use of the annotation Atomic to ensure that
all the operations affecting a link are executed atomically. With this implementation, the
code of each listener executes within the atomic action, also. So, if some listener throws
an exception, either before or after creating or removing a link, the entire operation is
aborted. This gives us an increased expressive power to develop the domain model.
Finally, for completeness, I show in Listing 5.12 an outline of the implementation
of the class InverseAssociation, which is used in the implementation of the class
DirectAssociation.
5.7.3.5
The final piece to conclude the implementation of an association declaration is the implementation of the classes that update each of the associations ends. These classes must
implement the interface Role, shown in Listing 5.13.
An association object calls the method add to create a new link between o1 and o2.
The responsibility of the method add of the Role interface is to change the object o1,
only; that is, this method performs only half of the work. The assoc argument is the
association object that made the call. This object may be needed to remove an existing
131
132
interface Role<C1,C2> {
void add(C1 o1, C2 o2, Association<C1,C2> assoc);
void remove(C1 o1, C2 o2);
}
Listing 5.13:
link for the object o1, in the case when it cannot have more than one link. The case for
the method remove is similar, except that this method does not need the third argument.
We need two distinct implementations of this interface: one for when we have a role
with a multiplicity upper-bound of one, another for when the multiplicity upper-bound is
greater than one. The first class is called RoleOne, and is shown in Listing 5.14. The
second class is called RoleMany, and is shown in Listing 5.15.
These classes are abstract because, to perform the changes, they need to access either
the VBox or the AssocSet that holds the value that should be changed. The box or set to
access, however, is different from one association to another, which means that we need
a different subclass of either RoleOne or RoleMany for each role declaration. These
subclasses simply override the methods getBox or getSet to return the appropriate
object, given an object o1.
These latter subclasses are generated as anonymous inner classes for each role declaration. Note that, unlike these, none of the classes AssocSet, DirectAssociation,
133
134
5.7.3.6
In the implementation given above I have not addressed one question: How to guarantee that the multiplicities of an association are not violated? I have not addressed this
problem yet, because it is not possible to implement this requirement adequately until I
introduce the consistent predicates in the next chapter. I shall return to this topic again
in Section 6.5.
5.8
Related Work
To the extent of my knowledge, the approach that I propose in this chapter is novel, even
though there is plenty of work that addresses the same problem.
I propose a new language specifically designed for implementing a domain models
structure and to complement another general-purpose programming language. The key
idea is to allow the implementation of a domain models structure in a language that
is midway between the languages used to represent a domain model and the languages
used to implement them. This language, however, is fully operational, allowing a complete
implementation of a domain models structure via automated code generation.
In contrast with this approach, all the previous work on the subject of reducing the gap
between a domain model and its implementation fits into one of the following approaches:
Support associations directly as a first-class construct in the programming language, which is then used to implement both the structure and the behavior of a
domain model.
Facilitate the implementation of associations in a programming language by providing libraries and patterns that capture the best-practices of implementing associations in that language.
Automate the generation of the code that implements part of a domain model, starting with the domain model expressed in a modeling language such as UML.
The last approach is the one that more closely relates to the work in this dissertation.
It is, however, worthwhile to discuss all of these approaches, given that all of them are
trying to solve the same problem.
5.8.1
135
136
5.8.2
Even though, theoretically, adding support for relationships to the programming language
is the best approach, practically, these proposals are of little help if they never reach
mainstream programming languages.
Another, more practical route, is to stay within the limits of current programming
languages, and see how we can implement associations more easily. I call this approach
the patterns approach, even if not all the proposals may qualify as patterns (in the sense
of [Gamma et al., 1995]).
In [Noble, 2000], Noble proposes a set of basic relationship patterns for an objectoriented programming language. He describes several patterns to implement relationships, from the simplest Relationship As Attribute, which implements a small, simple,
one-to-one unidirectional relationship, to the more complex Mutual Friends, which implements a bidirectional relationship.
The patterns described by Noble capture the common practices of implementing relationships in object-oriented programming languages. In fact, the examples given in Section 3.3.2 follow some of these patterns. Likewise, either these patterns or some of their
variations are typically used to implement the higher level constructs provided by other
approaches. For instance, in the implementation of a DML domain specification I use
some of these patterns, as do other authors.
One of the problems of using most of the patterns described by Noble is that we lose
track of a relationship after it is implemented: Implementing a relationship, spreads the
code among several different classes, making it difficult to recover the relationship again
later [Guhneuc and Albin-Amiot, 2004; Gueheneuc and Albin-Amiot, 2003].
The pattern that resists better to this problem is the Relationship Object (or one of its
specializations, the Active Value, or the Collection Object), which uses a distinct object to
137
represent the relationship, thereby encapsulating all the code in a single place. Several
authors have proposed solutions that are, in their essence, variations of this patternfor
instance, [Noble and Grundy, 1995; Suscheck and Sandn, 2003].
A more recent approach by Pearce and Noble [2006] uses aspect-oriented programming (see [Kiczales, Lamping, Menhdhekar, Maeda, Lopes, Loingtier, and Irwin, 1997])
to implement relationships as a crosscutting concern. By leveraging on the facilities
provided by the Aspect/J language, the authors propose a Relationship Aspect Library
that allows programmers to represent relationships explicitly, separate from their participating classes. Although promising, this approach has still several shortcomings. For
instance, to have more than one relationship for a given class, the authors resorted to
the workaround of having several almost identical copies of the aspects that implement a
relationship. Obviously, this solution does not scale for any medium-sized application.
Moreover, the syntax for adding links to a relationship departs significantly from
the common practice on the object-oriented programming area. For instance, using the
example given by Pearce and Noble in their paper, programmers must write code such
as Attends.aspectOf().add(student, course) to create a new relationship link
between two objects, instead of writing the typical course.enroll(student) method
call (unless, of course, they write by hand the enroll method to call this code, which
defeats the purpose of the proposal).
This last issue is, in fact, a problem shared with many other approaches that make the
relationship a first-class object in the language. For instance, both the DSM language and
the RelJ language use this same approach to change a relationship. The DML language,
on the other hand, provides both ways of adding links to relationships. We may, thus, use
either the code Course.Attends.add(course, student) to add a new link to the
5.8.3
Given that we have well-defined patterns for implementing an association, rather than
applying those patterns ourselves, we may automate their implementation instead. In
fact, this approach has been increasing steadily in popularity.
For a comparison of 10 such tools, see [Akehurst, Howells, and McDonald-Maier, 2007].
138
Avoiding round-tripping is one of the ten objectives set up by Harrison et al. [2000],
in their work on mapping high-level UML designs to Java. Harrison and his colleagues
propose a new method for generating Java code from a high-level design model expressed
in a UML class diagram. To avoid round-tripping problems, they split the implementation
of a class into two classes plus an interface, so that one of the classes contains the code
generated and the other is for the programmer make changes. The semantics of entity
type declarations in the DML language borrow from this, even though it uses a simpler
(and better) approach as it dispenses the interface.
A significant part of [Harrison et al., 2000] is dedicated to the implementation of associations. Unlike in the DML, which strives for using common programming idioms, in this
work the authors propose the use of cursors to manipulate an association. Cursors are
similar in spirit to the association-aware set described in the semantics of associations
declarations (see Section 5.6.2.4). Yet, whereas the association-aware set of DML implements the Javas standard Set interface, cursors provide an entirely new and rather more
limited interface. Moreover, the only way to change an association is through a cursor,
which, again, clashes with the common practice in the area.
Gnova et al. [2003], on the other hand, propose also a mapping for a fragment of
the UML class diagrams, but instead of cursors, they follow a more traditional approach
of adding methods to the classes in each end of an association. Yet, it is not clear from
their paper how they distinguish among different associations for the same class. Also,
they describe the interface of the code generated for an association, but do not specify the
semantics precisely; for instance, they do not say whether the collection returned by the
getter of an association end is mutable or not. No implementation is described either.
Finally, the most complete treatment of the UML language that I am aware of was
published recently by Akehurst et al. [2007]. In this work, Akehurst and his colleagues
describe thoroughly code generation patterns for each one of the possible variations on a
UML 2.0 association. They stumbled upon some difficulties, however. For instance, how
to enforce the minimum multiplicities of a bidirectional association, for which they have
no good answer. As we shall see in Section 6.5, the DML compiler will be able to solve
this problem.
To conclude this discussion of related work, I point out that in none of these proposals
is it clear whether the programmer may customize the behavior of the code generated for
an association.
5.9
Summary
This chapter describes a new languagethe DML languagethat purports to reduce the
gap that currently exists between a domain model and that domain models implementa-
5.9 Summary
139
described in the previous chapter, and is sufficiently simple and readable to constitute
a good design pattern for implementing associations in Java, even when the DML is not
used.
Finally, the DML language, when compared to the alternatives, provides some distinctive characteristics:
140
create a valuable intermediate artifact with a precise semantics that may be used
for other purposes in the application.
Given its similarity with Java and its simplicity, the DML language is easy to learn
and to use by current Java programmers.
Chapter 6
Consistency Predicates
In the previous chapter, I proposed to separate the implementation of a rich domain model
into the implementation of the domain structure and the implementation of the domain
behavior. But, whereas to implement the structure of a domain model I proposed a new
language, to implement the domains behavior I propose that programmers continue to
use Java; the DML language proposed in the previous chapter was specially designed to
allow this separation.
Java is a widely used general purpose object-oriented programming language, with
a reasonable set of programming constructs, and, more importantly, with an immense
set of libraries, tools, and documentation that greatly simplify the task of developing new
applications. Therefore, by using Java as an implementation language, we may take
advantage of all the facilities available for it. Unfortunately, the implementation of a rich
domain model in Java is not exempt of problems, as the examples discussed in Chapter 3
illustrate.
One of the difficulties found in the implementation of a domain model was in implementing domain constraints, as the example of implementing the limit imposed on the
total balance of a banks client that was discussed in Section 3.4.2. Yet, even though
this is a single example, the problems that we found in that example are not uncommon. Quite on the contrary, that example is representative of a frequent problem in the
implementation of rich domain models: How to ensure that the domain constraints are
maintained during the execution of the program?
The common approach to this problem is to implement the methods that may change
the state of domain objects with great care, to ensure that the domain remains in a
consistent state.
142
Consistency Predicates
6.1
Domain Consistency
One of the basic rules of good object-oriented programming, taught in most books on
the subject, is that each object is responsible for maintaining the consistency of its own
state. To enforce this rule, the state of an object should not be accessible to other objects.
Rather than changing an objects state directly, other objects call the methods provided by
that object to perform the changes they need. Each of the methods provided by an object,
in turn, must ensure that it leaves the object in a consistent state after its execution.
Making an object the sole responsible for its own state is a good design rule because
it limits the places in the code that need to have knowledge about the constraints for that
object, independently of the context where the object is used. Therefore, by following this
design rule, we increase the modularity of an object-oriented program.
Yet, whereas following this rule may be possible for isolated objectswhich do not have
relationships with other objects or that have relationships only with owned objectsit is
not possible, or at least convenient, in general, for rich domain models.
Before I explain the reasons why rich domain models are special in this regard I
discuss first the case of maintaining the consistency of a single objects state.
6.1.1
Consider the typical example of a buffer with a maximum capacity. A common implementation for such a buffer object is to maintain information in the object about its maximum
capacity, the number of elements that it contains, and the contained elements. Given
this implementation, the constraints that the state of every buffer object must verify are:
(1) that the number of elements in the buffer is between zero and the buffers maximum
capacity, inclusive; and (2) that the element count is equal to the number of elements
actually contained in the buffer.
Note that the constraints for a buffer object depend only on the buffer objects state.
Thus, we can be sure that these constraints remain valid throughout the entire lifetime
1
The problem, of course, is worse when we have concurrent accesses to the object that may view that
intermediate state. Possible solutions to this problem include using locks or atomic actions, and were
already discussed in Section 3.1 and in Chapter 4my preference, obviously, going to the use of atomic
actions.
143
144
Consistency Predicates
6.1.2
A distinctive aspect of rich domain models is that many of the constraints for a rich
domain model involve the state of more than one entity, rather than the state of a single
entity as in the example of the buffer object presented above.
For instance, the constraint implied by a bidirectional association between two different entity types, A and B, is a trivial (and common) example of a constraint that involves
more than one entity: If an instance of A has a reference to an instance of B, then that
instance of B must have a reference to that same instance of A. Moreover, depending on
the multiplicities of the association, there may be further constraints for the relationship
between instances of both types.
The problem of having constraints that depend on the state of more than one object,
however, is that it is no longer easy to localize the implementation of those constraints in
one single object. If a constraint refers to the state of more than one object, then we must
ensure that, whenever a change occurs in the state of any of those objects, the new state
after the change still satisfies the constraint. Unfortunately, the approach of checking the
constraint at each method that may affect the constraints validity brings with it several
problems.
First, it means that, to implement the constraints for a rich domain model, programmers must reason about several classes simultaneously, rather than concentrating on
only one. One of the advantages of object-oriented programming is that it reduces the
complexity of the programming task by allowing programmers to concentrate on the implementation of each class in turn, ignoring the implementation details of other classes.
But, when the implementation of a constraint for a class depends on the state of objects
from other classes, programmers must take into consideration the implementation of all
those other classes, also. In particular, they may have to change the other classes methods to ensure that the constraint is not violated. Yet, having several methods to worry
about is error-prone, because programmers may easily forget about one of those methods, thereby opening the door to domain inconsistencies. Often, such errors are difficult
to detect, either because the constraint is not explicitly stated anywhere in the code, or
simply because the constraint is so complex that it is not clear which are the objects
that it depends on. Furthermore, the probability of occurring such a programming error
increases when the code needs to be changed as a result of some requirements change.
Second, this approach leads rapidly to modularity problems such as code scattering,
code tangling, and strong coupling among the domain classes. Code scattering occurs
because the implementation of each constraint is spread over several methods of possibly
different classes. Code tangling, on the other hand, occurs when different constraints
refer to the state of the same entity typein this case, the methods of the shared entity
type must have code to check each of the constraints, thereby mixing several concerns in
a single method. The strong coupling among the domain classes occurs because each of
the entities referred to by a constraint needs to be aware of the other entities, so that it
can check the constraint when its state changes. A possible solution to these modularity
problems is the systematic use of the observer pattern [Gamma et al., 1995] to register
dependencies between entities. Yet, that solution complicates the domain implementation
enormously. Thus, in this dissertation I strive for a better solution.
Finally, there is a more fundamental problema problem of compositionwith the
approach of checking the constraints for a rich domain model at each of the domain entity
types methods: In many cases, those methods are used as part of larger operations, and,
thus, they may need to break the domains constraints temporarily during the operation.
Consider, for example, the case of establishing a new bidirectional link between two
objects. After one of the objects is changed to point to the other, the domain is not
in a consistent state until the latter object is changed to point to the former object.
Therefore, the method that changes the state of the former object cannot check whether
the constraint affected by that change is violated or not; it must assume that the method
that calls it ensures the final consistency of the domain. In fact, these methods are like
the private methods of the single object case, which may break the constraints for an
object. But, whereas in the case of single objects, the callers of the private methods are
necessarily from the same class, in the case of a rich domain model, the callers of the
methods that are part of larger operations must be, often, from other classes. So, it is
much harder to ensure that all such callers ensure the domain consistency, because they
are not confined to a single class; in fact, in many cases, the callers are not limited in any
way. Moreover, to ensure that, in the end, they leave the domain in a consistent state, the
higher-level operations that call the lower-level operations must have knowledge about
the details of the objects they affect, thereby breaking the object modularity.
Therefore, the composition of two or more objects into new collaborations is difficult
with current object-oriented practices; specially when the constraints of the composed
objects may need to be violated temporarily during the execution of a composing operation.
6.2
Examples of Constraints
The problem associated with the difficulty of composing domain objects was already discussed in Section 3.4.2. To exemplify the remaining problems with concrete examples,
I shall discuss, in the following, the implementation, using a traditional approach, of
several constraints taken from the same banking domain which was introduced in Section 2.2.2. Each of these examples illustrates some of the problems with the traditional
approach of implementing constraints in a rich domain model. Then, in the next section,
I present the consistency predicates, which allow a much simpler implementation of the
same constraints.
145
146
Consistency Predicates
1..* in the role declaration for the ClientAccount is not enough to guarantee that
each client has a checking account.
Starting with the implementation of the constructor for the class Client, must a new
client, as returned by the constructor, have already a checking account? If the returned
object must have a consistent state, the answer to the previous question is yes, and we
may implement it in one of two ways:
application code the creation of an instance of the class Client without ensuring the
proper constraints for the client objects.
Either way, regardless of the solution chosen for the construction of the client object,
then we have the problem of avoiding that future changes to the state of the objects violate
the consistency of the client object. Which changes may do that? Either the removal of
an account from the set of accounts of a client, or the closing of a checking account.
Therefore, we must specialize both of these operations to ensure that the client remains in a consistent state when each of the operations is performed. If the normal
execution of an operation would cause an inconsistency, then the operation must fail
by throwing an exception. In Listing 6.1 on the following page, I show a possible implementation of this functionality. Note that, even though I created a method in the class
Client to check the constraint, that method must be called from several locations in the
code, both in the class Client and in the class CheckingAccount.
This implementation suffers from the problem of code scattering, because the code
that implements the constraint is scattered over various methods of several different
classes. Furthermore, given that the method close of the class CheckingAccount
needs to call the method checkActiveCheckingAccountExists in the class Client,
it creates a stronger coupling between these two classes.
Closed accounts
The second example is about the constraints associated with closed accounts. In the
description of the example given in Section 2.2.2, there are several functionalities related
to closed accounts: only classes with a balance of zero may be closed; closed accounts
cannot have further deposits or withdrawals; and once the balance of a savings account
reaches zero, the account must be closed.
In this case, because the constraints depend only on the state of one object (a client
account), the implementation of these requirements is relatively straightforward:
We must check in the method close of the class ClientAccount that the balance
of the account is zero, throwing an exception if not.
We must override, in the class ClientAccount, both of the methods deposit and
147
148
Consistency Predicates
SavingsAccount that implement these three changes. Unlike in the previous example,
the code that implements each constraint is localized in the appropriate class, rather than
being spread over different classes.
Yet, note that each of the methods shown in this example mixes code from different concerns, rather than having each concern implemented separately. It is, thus, an
example of code tangling.
6.3
To simplify the implementation of constraints for a rich domain model, I propose the use
of consistency predicates. The two key ideas underlying consistency predicates are the
following:
A consistency predicate is a predicate that checks whether a domain object satisfies
some particular constraint.
Consistency predicates are checked only at the end of an atomic action, rather than
149
150
Consistency Predicates
A
value:int
0..1
a
0..*
b
B
value:int
Figure 6.1: UML class diagram for two classes with a bidirectional association between them.
being checked whenever a method of a domain class is executed; the atomic action
is valid only if all the consistency predicates evaluate to true.
Even though the constraints for rich domain models may involve the state of more
than one object, I assume that there is always one object through which we can access all
the objects needed to check the validity of a constraint. Thus, we may check the validity
of all the constraints for a rich domain model with predicates for single objectsthese
predicates are the consistency predicates.
Note that, even though consistency predicates are predicates for single objects, they
may refer to other objects that are accessible through the object that is being checked by
the predicate. In fact, I propose that consistency predicates be implemented as domain
classes methods, with no restrictions other than that they must return a boolean value
and that they should have no side-effects.
To show an example of methods that may implement consistency predicates, consider
the case of two classes, A and B, with a bidirectional association between them, as depicted
in Figure 6.1. We may implement these classes in DML as follows:
shown in Listing 6.3. So, we may check whether the domain is in a consistent state by
calling each of the methods implementing consistency predicates for all the applicable
domain objects. But, when should we do this check?
If we assume that we start with a domain in a consistent statethat is, where all
the consistency predicates evaluate to truethen the only way to break the domain
consistency is by changing the state of any domain object. So, we just need to check
the consistency of the domain state when something changes in that state. Yet, as we
discussed in the previous section, it is not possible to have the domain in a consistent state
all the time because, to perform certain operations, we may need to break the consistency
temporarily, during the execution of the operation. So, I argue that the boundary for
enforcing the domains consistency should not be at the end of each method that may
change the state of a domain object. Rather, it should be at the end of each atomic
action, because they correspond to the smallest unit of work semantically meaningful in
a domain model. In fact, when using a language with atomic actions, the state of the
domain changes only as the result of some successful atomic action.
Atomic actions, as implemented, for instance, in the JVSTM, give us already the
properties of atomicity and isolation. By requiring that at the end of each atomic action
all the consistency predicates be true, we enforce also the property of consistency for
atomic actions, thereby ensuring that the domain will always remain in a consistent
state.
Thus, if at the end of an atomic action, any consistency predicate evaluates to false,
the atomic action cannot commit successfully. Instead, the atomic action must abort,
because it is not consistent. Note that consistency predicates cannot transform an incon-
151
152
Consistency Predicates
sistent action into a consistent one, because consistency predicates cannot change the
state of the domain. Consistency predicates only check the consistency of the domains
state, preventing inconsistencies by vetoing the commit of the actions that would cause
an inconsistency.
Having a complete and correct set of consistency predicates helps in the implementation of a rich domain model because any programming error in the update of the domain
state that would cause an inconsistency is detected by the consistency predicates. Detecting programming errors, however, is not the sole purpose of consistency predicates.
On the contrary, consistency predicates implement also the domain logic that is typically found in the methods that change the state of a domain objectthe domain logic
that checks whether the operation can be performed in the current state of the object
and with the given arguments. By using consistency predicates much of that code may
disappear from those methods. Instead, the methods perform the operations as usual
and if a constraint is violated, consistency predicates detect that violation and abort the
atomic action, thereby undoing all the changes that caused the inconsistency. This combination of atomic actions with consistency predicates represents a significant change
in how programmers may develop a rich domain model: Rather than using a defensive
style of programming that checks first whether the programs execution may proceed with
the changes requested, they may, instead, specify separately the constraints in consistency predicates and perform the actions without checking first, knowing, however, that
if something goes wrong all the changes will be undone.
6.4
To express consistency predicates in Java, I propose the use of a new annotation type,
@ConsistencyPredicate, that may be used to annotate the methods that check the
consistency predicates conditions. These methods must be non-static, have a return
type of boolean, and have no arguments. Also, because these methods are meant to
check that an object of their class satisfies the consistency predicate, they should be
made final so that they are not overridden in a subclass.
The code of the methods should return the boolean value true if the condition of the
consistency predicate is verified, and false otherwise. To determine its return value,
the method may execute whatever it wants, provided that it has no side-effects.
If the execution of a consistency predicate returns false, or fails in any way, the
transaction executing that consistency predicate is aborted, throwing an exception of
type ConsistencyException. The annotation ConsistencyPredicate, however,
may specify a different exception type to throw. In that case, the exception thrown by the
transaction is of the type indicated in the annotation.
In Listing 6.4, Listing 6.5, Listing 6.6, Listing 6.7, and Listing 6.8 on the next page, I
show the implementation of the constraints discussed above for the banking domain.
Contrary to the implementations discussed before, the use of consistency predicates
allows us to have all the constraints implemented in the appropriate class, in a single
code unit (a method) that deals with one and only one consistency predicate. So, we
no longer have code tangling or code scattering for the implementation of constraints
that refer to several domain entities. The coupling among the classes is reduced also,
given that only the class that wants to enforce a constraint needs to know about that
153
154
Consistency Predicates
constraint. Finally, given that the execution of the consistency predicates is not statically
determined in certain points of the code, it is now possible to compose several classes into
more coarse-grained objects with operations that orchestrate several of the finer-grained
classes operations without the need to tweak the methods being composed.
6.5
Besides all the remaining advantages described above, consistency predicates allows us,
also, an elegant solution to the much debated problem of implementing the constraints
imposed by the multiplicities of an association between two classes.
In fact, none of the work that I am aware of that tackles the problem of implementing
associationseither by extending the programming language, or by presenting patterns
on how to implement themaddresses conveniently the problem of enforcing the associations multiplicities. In may cases, this problem is simply ignored, whereas in other cases
the problem is acknowledged but no convincing solution is presented to solve it.
Likewise, when I described the implementation of a DML specification in Section 5.7, I
skipped over the implementation of multiplicities, but referred to this section as presenting a solution for it.
The difficulty in implementing the constraints imposed by the multiplicity property on
an associations role is not in implementing the code that checks whether the constraint
is verified; that is trivial, in fact. Rather, the problem is in knowing when to do that check.
Consider, for example, that we want to enforce the multiplicities for the following
association (shown previously in Figure 5.8):
association AccountOwnership {
Client playsRole owner {
multiplicity 1;
}
ClientAccount playsRole account {
multiplicity 1..*;
}
}
Checking that the multiplicities are correct for an object of any of the classes is just a
simple matter of checking that the client account has an owner, or that the client has at
least one account. Yet, where and when should that check be made? The answer to this
question proves difficult when no notion of atomic action exists.
With consistency predicates, however, we may now use the multiplicity information
specified in the DML language to enforce, at the end of each atomic action, that domain
objects have the correct number of links as prescribed by an association declaration.
In Listing 6.9, I show the consistency predicates generated by the DML compiler for
checking the multiplicity of the AccountOwnership association.
6.6
155
156
Consistency Predicates
We saw already that to use consistency predicates in a Java program we use the
method-level annotation @ConsistencyPredicate. Thus, finding all the consistency
predicates for a particular set of classes is just a matter of using the reflection capabilities
of the Java language to iterate over all the methods of all those classes and collect all
the methods found with this annotation. This, obviously, is needed only once for each
program execution, provided that we cache the result somewhere.2
Having the set of consistency predicates for a program, we need now to know when to
call them to check the consistency of the domain. Given their semantics, we know that
they must be called at transactions commit-time. Yet, not all transactions need to check
the consistency. Because only write-transactions can change the state of the domain,
only these transactions need to check the consistency predicates.
So, a first solution to the problem is to check all the consistency predicates for all the
objects belonging to classes with consistency predicates whenever a write transaction is
about to commit.
Even though this solution is simple to implement, it is unacceptably inefficient in
terms of time. We may do better in terms of execution time if we spend some memory to
keep a dependence network between the objects.
The fundamental idea is that, at the end of a transaction, we should check only the
following:
The consistency predicates of all the objects created during the transaction.
The consistency predicates that may have become invalid because of a change made
in some existing transactional locationthat is, a change in a VBox.
To be able to do this, transactions need to know when a new object is created and which
consistency predicates depend on each box.
The first part is easily implemented by adding a new method to the Transaction
interface that allows new objects to notify the transaction about their creation, which they
should do in their constructors (if we are using the DML to generate the domain classes,
this is easily automated).
To implement the second part, however, we need to keep a record of which consistency predicates depend on which boxes.
There are many other ways to implement this; for some we may even do all the work at compile time. Yet,
as this is relatively straightforward, I do not discuss all the alternatives here, so that I may concentrate on
the most relevant aspects of the implementation.
VBox
0..*
depended
0..*
dependence
DependenceRecord
dependent:Object
predicate:Method
Figure 6.2: UML class diagram showing the central elements for the implementation
of consistency predicates in the JVSTM.
checked (which is kept in the dependent attribute) and with the Javas method that
implements the consistency predicate (which is kept in the predicate attribute).
Then, to execute the consistency predicate for the new object, a new special kind of
nested transaction that knows about this dependence record is created. This special
transaction differs from a normal nested transaction in the following:
If a write to a box is attempted, the transaction throws an exception. This prevents
that consistency predicates have side-effects (at least on the transactional state).
Whenever a box is read during the execution of the consistency predicate, the transaction should record that the consistency predicate depends on the value of that
box. So, it creates a link between the dependence record and the box.
Therefore, at the end of the execution of the consistency predicate, the dependence
record is related to the set of all the boxes on which the consistency predicate validity
depends. As the association between the class DependenceRecord and the class VBox
is bidirectional, each vbox is related, also, to the set of all the dependence records that
depend on the value of the box.
When a top-level write transaction commits, it just needs to go through each of the
boxes in its write set and recheck all the dependence records that are registered with each
box. Rechecking a dependence record means reexecuting the method stored in the slot
predicate on the dependent object stored in the slot dependent. This reexecution is
similar to the first one, when the dependence record is initially created, except that we
need to clear all the existing depended values for the dependence record (which has the
side-effect of removing the dependence record from the boxes on which it depended upon,
also) before the reexecution of the consistency predicate.
To conclude this outline of the implementation of the consistency predicates in the
JVSTM, there is one final question to be answered: At which phase during the commit of
a write transaction is the consistency check performed?
To answer this question it is worth noting that the process of checking the consistency
predicates for one particular transaction may occur concurrently with a similar process
for another transaction. Moreover, during the execution of this process, some shared
data structures (namely, the links between boxes and dependence records) are changed.
So, we need to be careful in implementing this process to avoid problems with data races.
157
158
Consistency Predicates
Consider, for instance, that we have a single dependence record, DR, that depends
on two boxes, B1 and B2. Imagine now that two concurrent transactions, T1 and T2,
are committing at the same time, and that T1 changed box B1 and T2 changed box B2.
Then, both transactions should recheck the dependence record DR. If both are doing
the consistency check concurrently, however, the following series of events may happen:
transaction T1 identifies DR within the dependencies of B1 and prepares to recheck it,
clearing all the DR depended valuesas a consequence of this, box B2 is no longer related
to DR; then, transaction T2 checks box B2 to see which dependencies exist for this box
and finds no dependence; finally, transaction T1 rechecks DR and, while doing it, adds
again the dependence to box B2. This example indicates that we may fail to reexecute
some dependence records if these data races are not dealt with. Unfortunately, if that
happens, an inconsistency may easily slip through into the domain.
A safe and easy way to deal with this problem is to leverage on the support for
concurrency given by the JVSTM. If we implement the association between the class
DependenceRecord and the class VBox transactionallythat is, using versioned sets
to keep the linksthen we may add and remove links to the association within a transaction without fear of those changes being made visible to other transactions or that
concurrent accesses to those links cause an inconsistency.
The idea is that the commit of a top-level write transaction checks the consistency of
its changes by executing all the needed consistency predicates before its validation phase.
That is, the consistency check is part of the normal transaction execution, except that
it is performed immediately before the commit, after all the user code has executed (so
that we know already which boxes have changed). During this consistency checking, the
transaction may read new boxes to evaluate the consistency predicates, but it may write,
also, to the boxes that are used to keep the dependence records and the vboxes connected
to each other. Naturally, all these new reads and writes are recorded in the transaction
read set and write set, respectively.
So, if some consistency predicate fails, the transaction simply aborts and all the
changes made to the dependence records are discarded, as usual. If, on the other hand, all
the consistency predicates succeed, then the commit of the top-level transaction proceeds
with the validation phase, where it may find that a dependence record was concurrently
updated by another transaction, in which case it detects a conflict and restarts the whole
transaction. Otherwise, if the transaction is valid, the commit of the transaction will
make the changes to the dependence records permanent.
Even though it is possible to find alternative solutions to this problem that avoid some
of the conflicts caused by changes in the dependencies, this implementation has the
advantage of simplicity, given that it relies on the existing support given by the JVSTM.
So, this was the solution that I adopted to implement the consistency predicates in the
JVSTM.
6.7
Related Work
The idea that the state of an object must satisfy a set of predicates, often called invariants,
traces back to the work of Hoare [1972] on data-representation correctness. Later, Meyer
built on this idea, proposing the use of class invariants as one of the fundamental elements
of his design by contract methodology for object-oriented design [Meyer, 1988, 1992a].
The design by contract approach is a design methodology in which programmers must
specify a contract for each class. A contract is specified by a set of pre-conditions and
post-conditions for each of the public classs methods, as well as a set of class invariants.
Class invariants specified for a class C are predicates that must be true for all instances
of C that are publicly exposed to other parts of the program. They provide, thus, a set of
guarantees about the state of an instance of C.
As part of his work on the design by contract approach, Meyer built into the programming language Eiffel [Meyer, 1992b] a set of constructs to allow the specification of
contracts, including, therefore, the specification of class invariants. Having class invariants explicitly represented in a program allows the Eiffel runtime to check whether the
invariants are true during the execution of the program, thereby facilitating the detection
of errors.
Meanwhile, this design by contract approach at the programming language level has
been applied to many other languages, including, naturally, the Java programming language [Kramer, 1998; Karaorman, Hlzle, and Bruno, 1999; Bartetzko, 1999; Leavens,
Ruby, Rustan, Leino, Poll, and Jacobs, 2000; Flanagan, Leino, Lillibridge, Nelson, Saxe,
and Stata, 2002; Lackner, Krall, and Puntigam, 2002; Leavens, Cheon, Clifton, Ruby,
and Cok, 2005]. All of these extensions to Java follow, more or less closely, the semantics
of the constructs provided by Eiffel.
Even though consistency predicates resemble very much class invariants, there are
some fundamental differences between the two approaches.
First and foremost, there is a conceptual difference between the two approaches. One
of the basic tenets of all the design by contract approaches is that class invariants are
used in a program as a debugging tool only. That is, the idea is that class invariants
checking is performed during program execution only to detect programming errors that
may cause the invariants to be broken. In particular, a correct program should never
obtain a false result from the evaluation of class invariants. So, once a program has been
tested, class invariants may be disabled. In fact, Eiffel and most of the other approaches
that implement class invariants allow programs to run with all the checking disabled.
Consistency predicates, on the other hand, are not for debugging purposes, even
though they may be used like that. Rather, they implement a significant part of a domains
logic and, therefore, cannot be disabled, if the program is to work as expected. That is,
159
160
Consistency Predicates
the semantics of a program depends on the execution of consistency predicates. For this,
however, consistency predicates depend on the semantics of failure recovery given by
atomic actions, which are not generally available in the languages with support for class
invariants.
A second difference is on the expressiveness of the predicates. Class invariants as
proposed by Eiffel (and followed by all the design by contract extensions to Java) cannot
access arbitrary data nor call arbitrary methods to verify their conditions. Instead, they
must restrict themselves to access the private state of the object being verified. In particular, class invariants cannot access the state of an object referenced by the object being
checked, nor can they call a method on such an object. These restrictions impose severe limitations on the expressiveness of class invariants that preclude many interesting
invariants that depend on several objects.
In fact, several authors acknowledge this problem and propose to extend class invariants so that they may access the state of other objects [Barnett, DeLine, Fhndrich,
Rustan, Leino, and Schulte, 2004; Dietl and Mller, 2005; Mller, Poetzsch-Heffter, and
Leavens, 2006]. To accomplish that, they build on work on ownership models to limit and
to control the aliasing of objects to a set of well known places within an aggregate [Clarke,
Potter, and Noble, 1998]. Class invariants in such approaches are able to extend their
visibility to all the objects owned by the object being checked. Even though these approaches represent a step further, they are still too limited for the needs of a rich domain
model. In such a domain model, it is not practical to limit the aliasing between entity
types, given that they are often part of very intricate object graphs where there is no object
that is a natural owner of the others.
A third and final difference between the two approaches is on when the predicates
are evaluated. In Eiffel, class invariants are checked at the beginning and at the end
of the execution of each of the classs methods (except for helper methods), whereas
consistency predicates are checked only at the end of a transaction. I discussed already
in the beginning of this chapter, the differences between these two approaches.
Having atomic actions in a programming language is, actually, the key enabling element for having consistency predicates as I have proposed them in this dissertation:
Once we have atomic actions, the idea underlying consistency predicates is relatively
straightforward.
Therefore, it is no surprise that there are many things in common between the work
described in this chapter and the proposal made recently by Harris and Peyton-Jones
[2006] to add data invariants to transactional memory.
Even though Harris and Peyton-Jones are working in a different setting, with a different STM implementation made for the Haskell programming language, their implementation strategy is quite similar to mine: To reduce the number of predicates that need to
6.8 Summary
6.8
Summary
This chapter proposes to separate the implementation of domain constraints from the
implementation of the remaining aspects of a domain model. To allow this separation, it
proposes the use of consistency predicates, which are automatically executed when a toplevel write transaction commits to check whether the domain objects are in a consistent
state.
The chapter uses several of the domain constraints from the banking domain model to
compare their implementation using the current best-practices with the implementation
using consistency predicates. It describes, also, how consistency predicates may be used
to implement the enforcement of an associations multiplicities.
Finally, it describes the implementation of consistency predicates in the JVSTM and
discusses related work.
161
162
Consistency Predicates
Chapter 7
Validation
This dissertations thesis is that it is possible to simplify significantly the task of implementing an object-oriented domain model by making a small, non-disruptive, and easy to
implement set of additions to the current object-oriented programming languages. More
specifically, that we may achieve that goal by adding to an object-oriented programming
language support for atomic actions, a declarative language for specifying the structural
aspects of a domain model, and consistency predicates that validate atomic actions at
commit-time.
In the last three chapters, from Chapter 4 through Chapter 6, I made concrete proposals for each of these different additions. For each case, I identified the problems,
proposed a solution, and exemplified how that solution helps in solving the problems
identified. Moreover, I described how to implement each of the proposals, showing not
only that their implementation is feasible, but also that it is possible to do it with little
effort in a practical waythat is, without introducing major changes in the languages and
the tools used by software developers. In fact, all the work described thus far tries to
respect the guiding principles put forth in the introduction of this dissertation.
The purpose of this chapter is to complement the thesis validation that was given along
each proposal, by describing two applications of the proposals made in this dissertation.
First, I describe the application of all my proposals in the development of a large realworld web application: the Fnix system. The work described in this dissertation and its
application to the Fnix system are, actually, quite intertwined, as they have occurred in
parallel, influencing each other.
Second, I evaluate the performance of the JVSTM implementation for a more STM-like
traditional setting. Even though the JVSTM was developed with the goal of simplifying
the implementation of a domain-intensive application, in this chapter I report on how it
performs in two existing benchmarks for STMs.
164
Validation
7.1
The Fnix project is an open-source university management system that aims to incorporate all on-line campus activities and related management services [FenixEDU]. From
its initial deployment with an initial limited set of functionalities in 2001 until now (mid
2007) it has been continuously growing both in functionality and use.
In this section, I describe how the proposals made in this dissertation were employed
in the more recent development of this system.
7.1.1
Fnix History
The project has been in development at the Instituto Superior Tcnico (IST), from the
Technical University of Lisbon, since 2001.
The project started with a limited set of functionalities to support the creation and the
management of web pages for some of the ISTs courses, but has ever since expanded its
functionality to encompass most of the activities and management of the IST. Over time,
it replaced most of the schools legacy information systems that were becoming obsolete
and hard to maintain. Therefore, it went through an enormous pressure for increasing
its scope.
The Fnix system is now an indispensable element for all the ISTs activities and
users, which comprise over 12,000 students, 1,000 faculty, and 700 administrative staff.
In particular, most of the administrative staff depends on the system for their daily work.
Even though the IST continues to be the systems lead development organization, other
universities and companies joined the project and are now using it and further developing
it for deployment in other universities. Presently (mid 2007), the system is deployed in
three other universities and is being deployed in at least another three.
The initial development team at IST included only a handful of programmers but has
since expanded to include more programmers, as well as a Users Support and Requirements Team and a Web Design Team. The programmers team comprises a set of senior
members, all of them with a degree in software engineering or a related area, and a set
of graduation students that are, typically, in the last year of their degree in software
engineering at IST. The senior members work full-time on the project, whereas the graduation students work only half-time and during approximately one year as part of their
degree final project. I show in Table 7.1 the evolution in the composition of the Fnixs
programmers team over time.
I joined the Fnix team in 2004 as an external collaborator and became partly responsible for the systems architecture since 2005. Since that time, I have introduced
165
2001/02
2002/03
2003/04
2004/05
2005/06
2006/07
5
2
5
12
7
19
7
16
14
10
12
2
into the system the JVSTM, the DML, and the consistency predicates described in this
dissertation. Yet, my work was not to use them to implement the Fnix domain model.
Rather, the bulk of my work for the Fnix project has been in integrating my proposals
into the systems framework and architecture, so that the rest of the team may use them
to implement the domain model.
7.1.2
The core of the Fnix system is developed in the Java programming language and consists
of a web application that was initially developed by following the best-practices for that
software development area, as of 2001.
The system used the standard three-layered architecture of an enterprise application
that I discussed in Chapter 2 (see Figure 2.1 on page 16). Therefore, it separates the code
that implements the domain model from both the code that accesses the database and
the code that deals with the user interaction.
The implementation of the domain model, however, was separated in two distinct
elements: (1) the classes implementing the domain entities, which consisted mostly of
the entities properties, and getters and setters to access those properties; and (2) the
classes that implemented the applications services, which contained most of the domain
logic related to the behavior of the entities. This solution, in fact, is akin to the pattern
Transaction Script described by Fowler [2002].
Each service, implemented typically as a single method, used the services of the
data-source layer to access the objects representing the domain entities and operated on
those objects according to the domain models requirements. To control the concurrent
access to the domain entities that is inherent to this kind of applications, the Fnix
code resorted to the interface provided by the implementation of the ODMG 3.0 Object
Persistence API [Cattell, Barry, Berler, Eastman, Jordan, Russell, Schadow, Stanienda,
and Velez, 2000] available in the Object/Relational mapping tool that was used in the
project: the Apache DB Projects OJB [OJB].
The ODMG API includes operations for locking objects either for read or for write
before they are accessed. Unfortunately, this lock-based approach to concurrency was
166
Validation
7.1.3
Even though the first proposal that I made specifically for the Fnix system was the
versioned STM, the first of this dissertations proposals to be effectively employed in the
development of the system was the DML language proposed in Chapter 5. The Fnix
team started to use the DML language in April 2005 and the JVSTM was deployed later,
in September 2005. The consistency predicates, on the other hand, were deployed only
during 2007 with a limited implementation.
Thus, given that the experience of using consistency predicates in the Fnix system is
still very limited, in the following I shall describe only the use of the DML and the JVSTM
in the Fnix project.
7.1.3.1
The goal of introducing the DML language in the development of the Fnix domain model
was initially twofold. First, to eliminate the errors in the management of bidirectional
associations. Second, to simplify the introduction of the STM in the system.
The classes that implemented the domain entities in the Fnix system followed a common pattern: each class contained a set of attributes with a pair of getter and setter
methods for each of the attributes. Likewise, the implementation of associations consisted, in most cases, in one attribute in one (for unidirectional associations) or in both
(for bidirectional associations) of the participating classes, again with the corresponding
getter and setter methods; when the multiplicity of an associations end admitted more
than one element, the attribute used to implement that end of the association was of a
collection type.
This simple approach of implementing associations, however, had the problem that
the setters of each of the attributes that corresponded to an association did not enforce
the consistency of the association when the association was bidirectional. Thus, the
responsibility of ensuring the consistency of such associations was on the programmer
that wrote the code that needed to create or remove a link; the programmer would have
to invoke the two necessary methods to ensure that the link was consistently created or
removed. Unfortunately, this is too much error-prone, and this was one of the problems
that was causing significant troubles to the Fnix team and to the project.
On the other hand, to use an STM to control the concurrent access to the domain
entities, all the classes that implemented domain entities needed to be changed to become
transactional. Given that the number of domain classes was slightly over two hundred,
that was not an easy task to do. Moreover, back then, it was not clear whether the use
of an STM would be feasible for a system with the dimension of the Fnix system. So,
committing to that change was a risk too high for the project.
The use of the DML solved both of these problems. Having the domain models structure represented in a declarative way allows us (1) to generate the methods that implement
correctly the associations between the domain classes, and (2) to generate the domain
classes as transactional classes or as plain java classes, as we may see fitit is just a
matter of changing the DML compilers backend.
In fact, when the DML was first introduced in the development of the Fnix domain
model, in April 2005, the DML compiler generated the classes exactly as they were implemented in the system at that time. In particular, neither it generated the classes as
transactional, nor it generated the methods that implemented bidirectional associations
correctly.
The goal was to allow a gradual conversion from the Java classes to their DML substitutes, given that the large number of classes in the system would not allow a rapid
conversion. Indeed, this conversion process took place during the second quarter of
2005, from April to June, and was performed incrementally by all the team, while they
were developing new functionalities.
When finally all the classes and associations became represented in DML, we could
change the DML compilers backend to generate the code that ensured the consistency
of bidirectional associations; this occurred in July 2005. Later, with the introduction
of the JVSTM, in September 2005, the compiler was once again changed to generate
transactional classes. Given that the interface of the classes generated was always the
same, no further changes in the existing code were necessary in either case.
This use of the DML language illustrates one of its advantages that were already mentioned in Chapter 5that we may change the code generation strategy without changing
the remaining code of the system.
167
168
Validation
Apr 2005
Sep 2005
Jul 2007
64
121
112
28
18
37
5
92
107
115
33
21
136
12
195
89
238
8
1
Domain classes
DML code
(generated from the DML)
Remaining domain code (services)
Presentation layer
Persistence layer
OJB mappings
Table 7.2: Lines of code for different parts of the Fnix system.
7.1.3.2
Even though the benefits described above are sufficient to justify the use of the DML for
implementing a domain models structure, there are other more far reaching benefits as
well.
Another advantage of the DML, of course, is that it reduces significantly the amount of
work required to implement a domain model. This is shown in Table 7.2, where I present
the approximate number of lines of code (LOC) of several parts of the system at three
different instants of its development: (1) in the beginning of April 2005, before starting
to use the DML language; (2) in the beginning of September 2005, when all the domain
classes were already in the DML and the JVSTM was introduced; and (3) as of this writing,
after almost two years of use of the DML and the JVSTM in the system.
From April 2005 to September 2005, the most significant change in the values presented in the table was in the implementation of the domain classes: From an initial
value of 64 thousand LOC for implementing the domain models classes, remained only
37 thousand LOC in the end. This value, however, does not reflect the actual reduction
in size of the code that implements the domain models structure for two reasons:
During that period the Fnix team continued to develop new functionalities and
to create new domain classes; the slight increase in the number of LOC in the
presentation layer indicates this, also.
In parallel with the conversion to a DML representation of the domain models
structure, the Fnix team started a refactoring process to move part of the code that
was in the classes implementing the services to the domain classes, instead. The
decrease of 15 thousand of LOC in the code of the services, despite the increase in
the systems functionality, confirm this.
If we consider both of this factors, it becomes clear that the real reduction of code
obtained with the DML should be much higher than what is shown in the table. In fact,
the code generated by the DML compiler in September 2005 for the 5 thousand lines of
DML code was about 92 thousand LOC of Java code. This value is much higher that the
initial size of the domain classes (64 thousand LOC) for several reasons: First, because the
code that is generated by the DML compiler implements correctly the bidirectionality of
the associations, where the original code did not; second, because all the associations are
now bidirectional; and third, because the DML compiler generates also the set of optional
methods that, in most cases, did not exist in the original domain model implementation.
Furthermore, one of the differences in the numbers from September 2005 to July
2007, reveal another benefit of using the DML that was realized only later: The almost
elimination of the OJB mappings.
The OJB mappings are a description in XML of how the domain classes and associations map into the tables of a relational database. As we have the information about the
domain classes and associations in a DML domain specification, those mappings may be
dynamically created from that domain specification, thereby eliminating the need to write
those mappings manually.
Finally, this possibility of using the information about the domain model structure is
being explored in several other places in the Fnix system as well:
7.1.3.3
Unlike the introduction of the DML, which forced the refactoring of much of the domain
models code, the introduction of the JVSTM in the Fnix system was performed without
changes in the domain models code. The changes were all performed in the infrastructural code that supports the systems software architecture.1
To use the JVSTM in an application we need to do two things: (1) to implement the
mutable shared objects as transactional objects (by using the class VBox), and (2) that
we identify and annotate the atomic actions.
The first task is performed by the DML compiler, that generates the domain classes using versioned boxes to represent their attributes. The second task is easier to accomplish
in a web application, given that in most of the cases, they process each user request as
1
Actually, after the introduction of the JVSTM, the code that acquired the locks of the ODMG API was no
longer necessary, but removing that code was a simple matter of a global find and replace in the projects
source code.
169
170
Validation
Date
09/2005
12/2005
03/2006
06/2006
09/2006
12/2006
03/2007
06/2007
Classes
Associations
227
255
334
346
455
557
613
704
324
349
422
679
851
931
1011
1098
Table 7.3: Evolution of the number of classes and associations in the Fnix domain
model.
a single atomic action. So, we may wrap the entire request processing code with a transaction at the web-application-framework level, thereby removing from the programmers
hand the responsibility of doing it.
Even though I do not have data to quantify it, the perceived benefits for the Fnix
application that resulted from the combined use of the JVSTM with the DML, was an
increase in the applications performance, stability, and robustness.
The increase in performance results, on one hand, from the elimination of the overheads associated with lock acquisition and management, and, on the other hand, with
the improved utilization of the database cache, given that it is no longer necessary to pay
the penalty of a round-trip to database to ensure the consistency of the data.
The improved stability and robustness of the system results from the elimination of
inconsistencies caused by faulty lock acquisition and lack of bidirectional associations
management.
From the point of view of the software development task, the major benefit that results
from the use of the JVSTM is the simplification of the programming model. Not only
as a consequence of the simpler concurrency control approach, but also because some
architectural changes were made possible by the use of the JVSTM and the DML. For
instance, in Table 7.2 on page 168 we see a significant reduction in the LOC needed for
the persistence layer. This reduction results from an architectural change enabled both by
the JVSTM and the DML that eliminates the dependency that the domain layer had of the
persistence layer: For example, the code in the domain layer no longer needs to explicitly
store the objects in the database when they are changed; the JVSTM automatically does
that in the commit of a memory transaction, invoking the methods of the persistence layer
to persistently store the objects that were changed (provided that the memory transaction
is valid).
The simplification of the programming model allows for a faster evolution of the system,
given that it is simpler to develop new code and that programmers spend less time in
171
# associations
1098
# classes
227
0
Sep
2005
Dec
2005
Mar
2006
Jun
2006
Sep
2006
Dec
2006
Mar
2007
Jun
2007
Figure 7.1: Evolution of the number of classes and associations in the Fnix domain
model.
debugging their code. The evolution of the number of classes and associations in the
Fnix domain model, shown in Table 7.3 on the facing page and graphically depicted
in Figure 7.1, shows that the system has experienced a fast growth since the JVSTM and
the DML were introduced in the development of the system. In fact, the domain model
more than tripled its size in less than two years, even though the team has remained
more or less the same throughout that period.
7.1.4
In Section 4.2, I presented the rationale underlying the development of the JVSTM. In
particular, I made a set of assumptions regarding the typical workload of a domainintensive application:
That the number of write transactions is low when compared with the total number
of transactions; probably, less than 10%.
That most of the transactions are medium-sized; that is, that the average size of
their read sets and write sets have hundreds to thousands of objects.
That occasionally there are long-running transactions that need to access thousands to millions of objects.
That an updating transaction may read many objects, but typically changes just a
few.
172
Validation
27878965
20909223
13939482
6969741
0
Oct
2006
Nov
2006
Dec
2006
Jan
2007
Feb
2007
Mar
2007
Apr
2007
May
2007
Jun
2007
Figure 7.2: Total transactions successfully processed by the Fnix web application
from October 2006 to June 2007.
7.1.4.1
In Figure 7.2, I show the monthly total of transactions successfully committed during the
period under examination. The decrease in the number of transactions during Novem2
2665893
1999419
1332946
666473
0
1 2 3 4 5 6 7 8 9 10111213141516171819202122232425262728
Figure 7.3: Total daily transactions successfully processed by the Fnix web application on February 2007.
ber and December coincide with the end of the first semester, and with the Christmas
holidays.
On the other hand, the period from mid February to mid March corresponds to one of
the two periods of highest activity for the Fnix application that occur when all the ISTs
students (more than 12,000) enroll for all the courses and shifts for their next semester.
In fact, this year the enrollments for the second semester started on the 17th of February
2007, at 15:00, as can be seen in Figure 7.3 and in Figure 7.4.
Contrast the values shown for the enrollments day with the values shown in Figure 7.5 and in Figure 7.6, which present the daily and the hourly average of transactions
processed, respectively, over the period of nine months from October 2006 to June 2007.
Whereas on average the Fnix application processes less than one million transactions per
day (even though that value has been increasing over time), on the enrollments day the
total number of transactions exceeds two and a half millions. More dramatically, the total
number of transactions processed on the first hour after the start of the enrollments is
more than ten times the maximum number of transactions processed on average for each
hour of the day. These values show that the load of the system under normal conditions
is well below what it is able to process.
Furthermore, the values depicted in Figure 7.5 and in Figure 7.6 show that the workload of the system varies with the working patterns of the people using the system: There
are more transactions processed during the week than at weekends and the highest rate
of transactions per hour occur during the working hours, with a slight decrease around
the lunch and the dinner hours.
173
174
Validation
524157
393117
262078
131039
0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
Figure 7.4: Total hourly transactions successfully processed by the Fnix web application on the first day of enrollments, the 17th of February 2007.
825131
618848
412565
206282
0
Mon
Tue
Wed
Thu
Fri
Sat
Sun
Figure 7.5: Average daily number of transactions successfully processed by the Fnix
web application for each day of the week from October 2006 to June 2007.
50168
37626
25084
12542
0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
Figure 7.6: Average hourly number of transactions successfully processed by the
Fnix web application from October 2006 to June 2007.
7.1.4.2
The numbers shown so far show the total number of transactions successfully processed
by the system, without distinguishing between read-only and write transactions.
Yet, one of the assumptions that influenced significantly the design of the JVSTM
was that read-only transactions largely outnumber write transactions. In the Fnix web
application, that proves to be the case, as we may see in Figure 7.7. Given that this
plot is in a logarithmic scale, the monthly totals show that the number of read-only
transactions is two orders of magnitude higher than the number of write transactions.
Likewise, the number of successful write transactions is two orders of magnitude higher
than the number of conflicts.
Looking at these values monthly, however, may hide differences in these ratios, given
that the write transactions (and, therefore, the higher probability of conflicts) are more
concentrated during the working hours of the administrative staff. Thus, in Figure 7.8, I
show the hourly average for each of the three values. This plot confirms that even though
the number of write transactions and conflicts increases during the working hours, they
maintain the ratios shown before.
Finally, the stress test for these ratios is the enrollments day, where thousands of
students are rushing to get the best timetables and, therefore, contending for the same
domain objects. The hourly totals for the enrollments day are depicted in Figure 7.9,
which show a significant reduction in both ratios on the first hour after the start of the
enrollments period. Nevertheless, even in that pathological case, there is still an order of
magnitude difference between each value.
175
176
Validation
reads
100000000
10000000
1000000
writes
100000
conflicts
10000
1000
100
10
1
Oct
2006
Nov
2006
Dec
2006
Jan
2007
Feb
2007
Mar
2007
Apr
2007
May
2007
Jun
2007
Figure 7.7: Monthly total of read transactions, write transactions, and conflicts in
the Fnix web application from October 2006 to June 2007. The yy axis is in a
logarithmic scale.
100000
reads
10000
1000
writes
100
10
conflicts
1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
Figure 7.8: Hourly average of read transactions, write transactions, and conflicts
in the Fnix web application from October 2006 to June 2007. The yy axis is in a
logarithmic scale.
177
1000000
100000
reads
10000
writes
1000
100
10
conflicts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
Figure 7.9: Hourly total of read transactions, write transactions, and conflicts in the
Fnix web application on the first day of enrollments, the 17th of February 2007. The
yy axis is in a logarithmic scale.
7.1.4.3
The previous results confirm that the Fnix web application exhibits the transactional
workload, in terms of read/write ratio, for which the JVSTM was designed. Namely, that
there are many more reads that writes.
The remaining assumptions underlying the design of the JVSTM are related to the
dimension of the transactionsthat is, how many transactional reads and writes are
performed by each transaction.
To assess the dimension of the Fnix transactions, I instrumented the JVSTM used
in the Fnix servers to count the number of boxes read and written during each transaction. These values are then accumulated and written to the database along the previously
written statistics. The new statistical values collected for each period of 5 minutes are
the accumulated number of boxes read (or written) by all the transactions, and the maximum number of boxes read (or written) by a single transaction. Moreover, these values
are collected for each of the two types of transaction: read-only transactions and write
transactions.
The values that I report here were collected by the Fnix servers for a continuous
period of almost 14 days in July 2007. The total number of statistical records collected
during that period was 18847, each one corresponding to a 5 minutes period of one server.
In Table 7.4, I show both the average and the maximum number of boxes read or
written for each type of transaction during this period. The number of boxes written
on average by each transaction is, as expected, very low, even though there are some
178
Validation
Average
Maximum
Reads/read-only transaction
Reads/write transaction
Writes/write transaction
5,844
47,226
35
63,746,562
2,292,625
32,340
Table 7.4: Number of boxes accessed by each type of transaction by the Fnix web
application.
Number of periods by type of transaction
Maximum number of boxes read
Max
Max
Max
Max
>
>
>
>
10,000,000 boxes
1,000,000 boxes
500,000 boxes
100,000 boxes
Read-Only transactions
Write transactions
15
461
1,121
9,763
0
36
60
8,377
Table 7.5: Number of large transactions, for each type of transaction, in the Fnix
web application. The count in each row includes the value of the row above.
see, surprisingly, that there is almost a similar number of write transactions reading more
than 100 thousand boxes. On one hand, this result is surprising, because the overall
number of read-only transactions is much higher than the number of write transactions.
On the other hand, it is in accordance with the fact that on average, write transactions
read almost 50 thousand boxes, ten times the average of read-only transactions.
Finally, these high number of reads performed by some read-only transactions, together with the fact that most of the transactions processed by the system are read-only
transactions, justify the use of speculative read-only transactions in the Fnix application. In fact, the use of speculative transactions in the Fnix application, eliminated
some of the out-of-memory errors that were caused by the read sets of such very large
transactions. Unfortunately, for write transactions, no similar optimization exists.
7.2
JVSTM Performance
The literature on Software Transactional Memory uses, typically, a set of simple benchmarks to evaluate and to compare different STM implementations. In most cases, those
benchmarks consist of a couple of operations operating on a data structure such as a redblack tree, or a linked list. The usual measure of performance for such benchmarks is
the number of transactions per second that each STM implementation delivers when running the benchmark for a fixed amount of time. One such set of benchmarks is publicly
available with the implementation of the DSTM2 proposed by Herlihy et al. [2006].
More recently, Guerraoui, Kapalka, and Vitek [2007] proposed the STMBench7 benchmark as a more realistic example of what should an STM implementation be prepared to
find in a real-world application. Even though the STMBench7 is still very limited in size
when compared with the Fnix system, it is, however, much more realistic than the small
examples typically used in this area.
Therefore, in this section I use these two benchmarks to evaluate the performance of
the JVSTM. Even though the goal for the JVSTM was not to provide the best-performing
STM implementation, it is a stated goal of this dissertation that the implementation of the
STM proposed be suitable for practical use. This brief evaluation shall confirm that this
goal is attained.
7.2.1
All the results presented in this section were obtained in a single machine with two
dual-core AMD Opteron processors, which gives us the total of four available cores. The
machine runs Linux and all the tests were performed without any other significant process
running in the machine.
179
180
Validation
All the tests were compiled and executed with version 1.5.0_11-b03 of the Suns Java
Runtime Environment using the default options.
7.2.2
The publicly available implementation of the DSTM2 [DSTM2] includes three variations of
a benchmark that consists in the repeated insertion, removal, and search of a randomly
chosen integer in a set of integers. The set of integers is implemented either as a sorted
single linked list, a skip list, or a red-black tree.
The benchmark allows the specification of the number of threads to spawn, the percentage of updates that each thread should perform, how long should the benchmark run,
and which STM implementation should be used. The choice of the STM implementation
is made though the specification of a transactional factory that implements the interface
specified by the DSTM2 framework [Herlihy et al., 2006].
I created two factories to run these tests: one using the JVSTM, and another that uses
a single global lock to ensure exclusive access to the data structure in each operation.
Moreover, I made some minor changes to the benchmark to ensure that the sets have
a bounded size. More specifically, I limited the integers that are randomly chosen by the
benchmark to be within the range 0 to 1023. Thus, each set will have a maximum size of
1024 elements. Also, all the benchmarks start with a set that was previously initialized to
contain half of its maximum size (512 elements in this case) of randomly chosen integers.
Given that the argument of each of the three operations performed on the set is randomly
chosen with a uniform distribution, and that the inserts and the removals alternate with
each other, the set will have, on average, 512 elements. So, the probability of a search
operation returning true is 0.5. Likewise, both the insertion and the removal of an integer
have a probability of 0.5 of not changing the set. One consequence of this randomness is
that the effective number of write transactions for a requested workload of n % of updates
is in reality only half of that.
I ran each of the three benchmarks varying the following parameters:
Shadow: This is another factory provided with the DSTM2 code and described
in the paper.
JVSTM: This is the factory that uses the JVSTM, but where all the transactions
start as possible read-write transactions.
Speculative: This is a variation of the previous factory, which starts all transactions speculatively as read-only transactions and restarts them when a write
is attempted.
JVSTM-ROH: This is a variation of the JVSTM factory that follows the eventual read-only hints given when a transaction starts. The code of the DSTM2
benchmark was changed to allow the specification, by the programmer of an
atomic action, that a given transaction is read-only. In particular, all the executions of the search operation are made as read-only. This factory follows
these hints by starting the transactions marked as read-only in that mode.
Lock: This factory uses a single global exclusive lock to prevent that two concurrent threads access the same data structure.
The two STM implementations that came with the DSTM2 need a contention manager
to resolve the conflicts. Even though there are many contention management policies
proposed in the literature,3 in these tests I did not explore variations of contention management. Instead, in both cases, I used the default value configured in the DSTM2 code:
the backoff contention manager.
For each combination of the parameters, I ran 8 different executions of the benchmark
for 20 seconds. The value of transactions per second that I use in the following discussion
of the results is the average of the 8 values obtained with these runs.
In the following, I shall present the results for each of the 3 benchmarks. I present
in tables the results obtained for all the six factories. To facilitate the comparison of the
various STMs, I present the results graphically also in form of plots in a logarithmic scale.
In these plots, however, I do not show the results for all the variants of the JVSTM, to
avoid cluttering the plot. Instead, in the plots I present only the results for the Speculative
factory.
7.2.2.1
I show, in Table 7.6 through Table 7.9 the results for the List Benchmark. The plots corresponding to these values are depicted in Figure 7.10 through Figure 7.13, respectively.
In this benchmark, the JVSTM performs much better than either of the two DSTM2
implementations, but it is at least an order of magnitude worse than the simple lock
approach for write dominated loads.
3
See, for instance [Guerraoui, Herlihy, and Pochon, 2005; Scherer III and Scott, 2004, 2005].
181
182
Validation
16
32
422938
23916
23080
18074
640
552
214365
28946
31496
26164
523
379
374421
33203
34473
39042
557
372
375223
13074
14344
12218
494
342
359732
11162
12563
10061
603
390
365472
6197
5677
4451
585
380
Table 7.6: The results for the List Benchmark with 100% of updates.
16
32
414173
25577
30229
25620
834
662
207505
40761
46798
42070
712
483
337147
51786
66985
73418
741
436
351625
29286
22859
27900
765
490
336868
16180
16433
18147
875
601
342779
11146
9724
9438
930
691
Table 7.7: The results for the List Benchmark with 50% of updates.
16
32
433732
28364
39469
37935
1311
855
249314
48178
73042
73024
1362
1000
373933
72879
145393
142326
1342
1071
353379
36034
51281
47071
1402
1187
359725
38481
47992
43897
1591
1423
334260
19024
33455
30018
1706
1617
Table 7.8: The results for the List Benchmark with 10% of updates.
16
32
800430
34535
57948
57291
2649
991
374063
53970
114281
115751
3728
1910
702136
79273
236709
225987
5564
2998
705199
80951
223788
227925
5617
3020
714766
81278
219088
235643
5749
2864
688335
81497
230003
226171
5512
2743
Table 7.9: The results for the List Benchmark with 0% of updates.
183
1000000
Transactions/second
100000
Lock
10000
1000
Speculative
Shadow
100
ofree
10
1
1
16
32
Number of threads
Figure 7.10: Transactions per second processed by each method for the List Benchmark with 100% of updates.
1000000
Transactions/second
100000
Lock
10000
Speculative
1000
Shadow
100
ofree
10
1
1
16
32
Number of threads
Figure 7.11: Transactions per second processed by each method for the List Benchmark with 50% of updates.
184
Validation
1000000
Transactions/second
100000
Lock
10000
Speculative
1000
Shadow
100
ofree
10
1
1
16
32
Number of threads
Figure 7.12: Transactions per second processed by each method for the List Benchmark with 10% of updates.
1000000
Transactions/second
100000
Lock
Speculative
10000
1000
Shadow
ofree
100
10
1
1
16
32
Number of threads
Figure 7.13: Transactions per second processed by each method for the List Benchmark with 0% of updates.
185
16
32
1283214
132044
131348
58185
1186
1102
553313
160152
162268
98303
1375
211
1164354
88145
90303
111385
1367
17
1210855
92856
94012
73634
1384
14
1206412
87676
87099
64784
1369
13
1166084
83972
82766
63891
1364
45
Table 7.10: The results for the Red-Black Tree Benchmark with 100% of updates.
The exclusive lock approach, as expected, performs better with a single thread than
with more threads. The JVSTM, on the other hand, scales well, regardless of the workload
used, up to 4 threads. After that number, the performance degrades rapidly, except for
the case of 0% of updates. This sudden fall of the performance when the number of
threads is higher than the number of processors results from an increasing number of
conflicts, and, therefore, restarts, of the transactions. Given that all the transactions
must traverse the list nodes from the beginning up to the point of insertion or removal,
they have a high probability of traversing a node that is concurrently updated by another
thread. This effect is more visible when the rate of updates is highest.
The results for the DSTM2 implementations place the Shadow factory above the ofree,
which is consistent with the results presented in [Herlihy et al., 2006].
These results show, also, the overheads of generic transactions when compared to
read-only transactions. Even though the JVSTM with the speculative transactions has to
restart all the transactions erroneously assumed to be read-only, that is largely compensated by the gains obtained in all the tests, except when we have only 1 thread with 100%
updates.
Finally, an interesting result is that, in many cases, the JVSTM with speculative
transactions performs even better than the JVSTM-ROH because, in reality, only half of
the update operations are updating transactions. So, whereas the JVSTM-ROH uses a
generic read-write transaction for all the update operations, the JVSTM with speculative
transactions tries always a read-only transaction first, which proves to be true in at least
half of the cases (for a workload of 100%).
7.2.2.2
I show, in Table 7.10 through Table 7.13 the results for the Red-Black Tree Benchmark.
These values are depicted graphically, also, in Figure 7.14 through Figure 7.17.
Like in the previous case, the JVSTM performs much better than both of the DSTM2
implementations, but is much worse than the simple lock approach. In this case, however,
186
Validation
16
32
1440942
182191
200083
98344
2146
1986
685616
255390
278694
170429
2321
326
1323005
181625
182988
229593
2363
42
1320038
163095
163429
135732
2468
37
1322938
147826
151694
126116
2470
55
1323574
140170
148615
113520
2377
155
Table 7.11: The results for the Red-Black Tree Benchmark with 50% of updates.
Transactions/second by number of threads
Factory
Lock
JVSTM
JVSTM-ROH
Speculative
Shadow
ofree
16
32
1596168
304063
405617
283056
7619
6450
898924
451420
616440
469520
3005
1147
1360411
607360
898527
751179
3391
77
1465382
403839
519580
440848
5719
184
1456302
366217
465813
400654
5451
335
1470091
354499
450957
383148
5886
947
Table 7.12: The results for the Red-Black Tree Benchmark with 10% of updates.
Transactions/second by number of threads
Factory
Lock
JVSTM
JVSTM-ROH
Speculative
Shadow
ofree
16
32
1752592
400369
601922
597068
70686
29887
1180457
626557
1004470
1024964
86704
48232
1557793
860508
1576576
1563086
105488
68262
1680455
842556
1605674
1530231
83349
65963
1661551
886790
1614081
1600315
86818
62677
1627463
908179
1608275
1595123
85619
65050
Table 7.13: The results for the Red-Black Tree Benchmark with 0% of updates.
Red-Black Tree Benchmark (100% updates)
1000000
Lock
Transactions/second
100000
10000
Speculative
1000
Shadow
100
10
ofree
1
1
16
32
Number of threads
Figure 7.14: Transactions per second processed by each method for the Red-Black
Tree Benchmark with 100% of updates.
187
1000000
Lock
Transactions/second
100000
Speculative
10000
1000
Shadow
100
10
ofree
1
1
16
32
Number of threads
Figure 7.15: Transactions per second processed by each method for the Red-Black
Tree Benchmark with 50% of updates.
1000000
Lock
Transactions/second
100000
Speculative
10000
Shadow
1000
100
ofree
10
1
1
16
32
Number of threads
Figure 7.16: Transactions per second processed by each method for the Red-Black
Tree Benchmark with 10% of updates.
188
Validation
1000000
Lock
Speculative
Transactions/second
100000
Shadow
10000
ofree
1000
100
10
1
1
16
32
Number of threads
Figure 7.17: Transactions per second processed by each method for the Red-Black
Tree Benchmark with 0% of updates.
7.2.2.3
Finally, I show, in Table 7.14 through Table 7.17 the results for the Skip List Benchmark.
These values are depicted graphically, also, in Figure 7.18 through Figure 7.21.
This is the first benchmark where the JVSTM performs better than the lock-based
approach. In particular, for all the workloads except the one with 100% of updates, the
JVSTM has a higher transaction rate with 4 threads than the lock-based approach.
189
16
32
305121
68128
68905
47857
1965
1768
158993
102747
102593
80641
396
520
163357
103485
103825
128983
68
61
170373
75967
72128
68326
49
36
166545
64742
66043
55606
48
37
198497
59258
62111
53011
46
37
Table 7.14: The results for the Skip List Benchmark with 100% of updates.
16
32
318502
77654
92628
68632
3269
2896
172220
128398
158112
129393
692
1017
175241
180405
212225
215560
156
102
182282
108499
123948
114377
94
83
212797
94706
103584
94131
77
64
225285
89580
97521
85970
76
106
Table 7.15: The results for the Skip List Benchmark with 50% of updates.
16
32
359652
99094
144467
132659
9876
6975
214196
160091
271900
236497
3079
3421
202393
235781
466776
440954
635
388
215050
158337
247383
230325
347
298
229224
139398
227163
211212
384
456
255033
133906
220600
203021
500
553
Table 7.16: The results for the Skip List Benchmark with 10% of updates.
16
32
444574
138673
239763
224471
48369
18092
240094
227342
421094
438824
39520
25335
277495
307955
805865
782900
45951
35360
276243
321226
825336
773491
34333
32569
296080
319811
822072
806197
35713
31752
312701
328089
842003
798779
32099
31589
Table 7.17: The results for the Skip List Benchmark with 0% of updates.
190
Validation
1000000
Transactions/second
100000
Lock
10000
Speculative
1000
100
10
Shadow
ofree
1
1
16
32
Number of threads
Figure 7.18: Transactions per second processed by each method for the Skip List
Benchmark with 100% of updates.
1000000
Transactions/second
100000
Lock
Speculative
10000
1000
100
Shadow
ofree
10
1
1
16
32
Number of threads
Figure 7.19: Transactions per second processed by each method for the Skip List
Benchmark with 50% of updates.
191
1000000
100000
Transactions/second
Lock
Speculative
10000
1000
100
Shadow
ofree
10
1
1
16
32
Number of threads
Figure 7.20: Transactions per second processed by each method for the Skip List
Benchmark with 10% of updates.
1000000
Transactions/second
100000
Speculative
Lock
10000
Shadow
ofree
1000
100
10
1
1
16
32
Number of threads
Figure 7.21: Transactions per second processed by each method for the Skip List
Benchmark with 0% of updates.
192
Validation
7.2.3
The STMBench7 benchmark was proposed recently by Guerraoui et al. [2007] as a more
realistic benchmark for evaluating STM implementations. This benchmark is an adaptation of the OO7 benchmark [Carey, DeWitt, and Naughton, 1993]: It maintained the
underlying data structure of the original benchmark, but removed the database-related
parts and added a set of new operations that allow for a more adequate evaluation of
STMs.
7.2.3.1
The data structure of the STMBench7 benchmark consists in a large number of objects of
several different typesmodules, assemblies, composite parts, atomic parts, connections,
documents, and manualswhich are organized as follows: There is a single module
that contains a seven-level-deep tree of assemblies, where each level of the tree has tree
children; each of the leaves of this tree contains several composite parts, which, in turn,
have a document and a graph of atomic parts which are connected via connection objects.
All these objects, which are called generically design-library objects, are related with one
another either by using simple references between them, or by using collections of objects.
The benchmark starts with the creation of this data structure, using a pair of factories:
(1) a factory for creating each of the design-library objects, and (2) a factory for creating the
sets, bags, and indexes needed to relate the objects with one another. The factories are
used by the STMBench7 benchmark to allow that different STM implementations provide
their own version of the elements that compose the data structure.
Therefore, I created one implementation of each of these factories using the JVSTM.
For implementing the design-library objects, I used a vbox for each of their fields and
maintained all the remaining code, except that where a field access was made, there is
now a vbox operation. For implementing the collections, I used a simple approach: I
implemented two purely functional data structures, which are, thus, thread-safe, and
used a single vbox to hold an instance of the collection. So, any operation that changes
a collection creates a new instance of the collection and changes the vbox to that new
value.
The two purely functional data structures that I used to implement the collections of
the STMBench7 benchmark were: (1) a single linked list, and (2) a red-black tree that
follows the implementation described in [Okasaki, 1998].
7.2.3.2
7.2.3.3
The STMBench7 benchmark allows us to specify, for each run, the number of threads
to use, the type of workload pretended, which synchronization strategy to use, and for
how long should each thread run; additionally, it allows us to disable certain categories
of operations.
There are three predefined workload values in the STMBench7, corresponding to different splits between read-only and read-write operations: read-dominated (90% reads/10%
writes), read-write (60% reads/40% writes), and write-dominated (10% reads/90% writes).
The STMBench7 assigns a probability for each of its operations by taking into account
the workload type chosen and a predefined ratio assigned to each category type. This
probability is then used by each thread to choose randomly the sequence of operations to
execute.
Regarding the choice of synchronization strategy, the STMBench7 benchmark comes
already with two locking strategies implemented:
A coarse-grained locking strategy that uses a single read-write lock for the entire
data structure.
A medium-grained locking strategy that uses one read-write lock for each level of
the data structure.
193
194
Validation
The intended semantics for the synchronization strategies is that each operation executes atomically. Thus, to test the JVSTM, I implemented another synchronization strategy that wraps each operation with a JVSTM transaction. Moreover, as in the STMBench7
benchmark we know whether each operation is read-only or not, the JVSTM strategy uses
that information to create a transaction of the appropriate type.
As for the operations to use, the original version of the STMBench7 allows us to disable, independently, all the long traversals, and all the structure modification operations.
I extended it to allow us to disable, also, the read-write long traversals (but leaving the
read-only long traversals active).
Finally, there are two types of results produced at the end of a run: (1) the total
throughput of the benchmark, measured in number of operations per second; and (2) the
maximum latency for each type of operation. According to the authors of the benchmark,
there are two typical uses for it: either we run the benchmark with all the operations
enabled to measure the latency of the operations, or we run the benchmark with no long
traversals active to measure the throughput.
7.2.3.4
Experimental Setup
I ran a series of tests varying the synchronization strategy, the number of threads, the
workload type, and the mix of operations.
Given that the latency results of the benchmark may be significantly influenced by
the random execution of the JIT compiler of the Java virtual machine, I changed the
benchmark so that it runs for 60 seconds in the beginning to warm up the virtual machine;
after that initial warm up, the benchmark runs for the specified amount of time and,
obviously, the results are measured only for that part of the run. The results shown
below were obtained by running each test for 60 seconds.
I show results for each of the three workload types and synchronization strategies,
varying the number of threads through the values 1, 2, 4, 8, and 16.
The throughput results were obtained for two mixes of operations: one with all the
long traversals disabled, and another with only the read-write long traversals disabled.
The latency results were obtained with all the operations enabled.
7.2.3.5
Throughput Results
I show the results obtained for the throughput tests with all the long traversals disabled
in Table 7.18 through Table 7.20, where each table is for a particular workload type. Also,
I show these values graphically in Figure 7.22 through Figure 7.24.
195
16
32
2362
2447
1632
2948
3269
2585
3302
4189
4260
2838
4080
2297
2673
3988
1835
2637
3876
1495
Table 7.18: The results of the STMBench7 benchmark with all the long traversals
disabled and a read-dominated workload.
Operations/second by number of threads
Synchronization strategy
Coarse-grained locking
Medium-grained locking
JVSTM
16
32
1569
1528
947
1418
1698
1362
1498
1887
1453
1415
1769
1007
1459
1846
667
1489
1692
441
Table 7.19: The results of the STMBench7 benchmark with all the long traversals
disabled and a read-write workload.
Operations/second by number of threads
Synchronization strategy
Coarse-grained locking
Medium-grained locking
JVSTM
16
32
936
908
578
925
945
652
875
972
620
863
970
427
821
909
316
844
901
234
Table 7.20: The results of the STMBench7 benchmark with all the long traversals
disabled and a write-dominated workload.
No long traversals / Read-dominated
4260
Operations/second
medium
3195
coarse
2130
JVSTM
1065
0
1
16
32
Number of threads
196
Validation
1887
Operations/second
medium
coarse
1415
944
472
JVSTM
0
1
16
32
Number of threads
Figure 7.23: Operations per second processed by each synchronization strategy for
the STMBench7 benchmark with all the long traversals disabled and a read-write
workload.
972
Operations/second
medium
coarse
729
486
243
JVSTM
0
1
16
32
Number of threads
197
16
32
131
135
101
163
172
198
139
193
265
131
185
305
140
191
306
172
237
359
Table 7.21: The results of the STMBench7 benchmark with all the read-write long
traversals disabled and a read-dominated workload.
16
32
398
427
130
275
320
253
308
631
991
372
558
514
586
524
413
430
385
359
Table 7.22: The results of the STMBench7 benchmark with all the read-write long
traversals disabled and a read-write workload.
These results show that, even though the JVSTM performs worse than either of the
locking strategies with one thread, it scales much better than the locking strategies. In
fact, for a read-dominated workload and four threads the JVSTM is clearly better than the
coarse-grained locking and slightly better than the medium-grained locking. Also, from
the shape of the plot, it is reasonable to expect that this difference would become more
significant with more processors.
Unfortunately, the performance of the JVSTM decreases abruptly when we have more
threads than processors. This result is caused in part by the fact that the STMBench7
is not concurrency friendlythat is, there is lots of contention in the benchmark. Thus,
as the number of threads (and consequently the number of transactions) increases, the
probability of a conflict increases dramatically. Not only because it increases the probability that a concurrent write transaction exists, but also because the duration of each
transaction increases.
Nevertheless, these results are very encouraging for the JVSTM. Specially, when compared with the results reported by the authors of the STMBench7 benchmark in their
paper for an implementation of another STMthe ASTM. According to those results, the
ASTM cannot achieve the 10 operations per second in this benchmark, whereas the results presented for the locking strategy are very similar to those obtained by me. So, it is
reasonable to say that the JVSTM performs at least one to two orders of magnitude better
than the implementation used by the authors of the STMBench7.
In fact, whereas Guerraoui et al. report that the ASTM takes roughly half an hour to
execute one of the long traversals (namely, T1), when running with only one thread, the
JVSTM executes that operation in less than 1.4 seconds in my experiments.
198
Validation
16
32
822
803
470
844
949
543
873
923
603
859
893
427
845
837
315
839
863
229
Table 7.23: The results of the STMBench7 benchmark with all the read-write long
traversals disabled and a write-dominated workload.
JVSTM
Operations/second
359
269
medium
180
coarse
90
0
1
16
32
Number of threads
Figure 7.25: Operations per second processed by each synchronization strategy for
the STMBench7 benchmark with all the read-write long traversals disabled and a
read-dominated workload.
199
Operations/second
991
743
495
coarse
medium
JVSTM
248
0
1
16
32
Number of threads
Figure 7.26: Operations per second processed by each synchronization strategy for
the STMBench7 benchmark with all the read-write long traversals disabled and a
read-write workload.
949
Operations/second
medium
coarse
712
475
237
JVSTM
0
1
16
32
Number of threads
Figure 7.27: Operations per second processed by each synchronization strategy for
the STMBench7 benchmark with all the read-write long traversals disabled and a
write-dominated workload.
200
Validation
Read-only oper.
Coarse-grained locking
Short
Short
Short
Short
Short
Short
Medium-grained locking
JVSTM
traversals
operations
traversals
operations
traversals
operations
16
11
3
9
3
2
4
3564
4011
5891
5720
2
5
1702
2427
2500
4011
7
3
4256
4383
4256
4256
222
303
4515
4515
10960
10330
393
6068
Table 7.24: Maximum latency results for read-only short traversals and short operations with all the operations enabled and a read-dominated workload.
Read-only oper.
16
Coarse-grained locking
Short
Short
Short
Short
Short
Short
1
3
8
3
4
7
4650
3167
1194
3359
2
3
5082
5082
1557
3564
11
4
4515
4650
6068
6068
186
436
7463
7463
7463
7687
34
115
Medium-grained locking
JVSTM
traversals
operations
traversals
operations
traversals
operations
Table 7.25: Maximum latency results for read-only short traversals and short operations with all the operations enabled and a read-write workload.
Finally, I show both in Table 7.21 through Table 7.23, and in Figure 7.25 through Figure 7.27, the results obtained when only the read-write long traversals are disabled. In
this case, the results for the JVSTM are even better, as it performs significantly better
than both the locking strategies except in the case of a write-dominated workload. These
results validate the suitability of the JVSTM for workloads with long read-only operations.
7.2.3.6
Latency Results
To conclude the evaluation of the JVSTM performance, I show, in Table 7.24 through Table 7.26, the results of the maximum latency for all the read-only short traversals and
short operations, for each of the various workloads, when all the operations are enabled.
These results show that, in the JVSTM, the execution of read-only transactions is not
affected by the remaining of the system. Whereas for the locking strategies there is a
significant increase of the maximum latency when the number of threads increases, that
phenomenon does not occur with the JVSTM. The problem with the locking strategies is
that the reads must wait that a lengthy write transaction (a long write traversal, typically)
finishes before they may proceed. In the JVSTM, however, the reads never need to wait.
7.3 Summary
201
Read-only oper.
16
Coarse-grained locking
Short
Short
Short
Short
Short
Short
2
3
1
3
4
2
1652
569
489
474
0
3
5391
5720
1343
1030
1
34
5391
4011
3895
3564
6
6
4650
4515
6437
6437
4790
15
Medium-grained locking
JVSTM
traversals
operations
traversals
operations
traversals
operations
Table 7.26: Maximum latency results for read-only short traversals and short operations with all the operations enabled and a write-dominated workload.
7.3
Summary
This chapter presented the application of some of the proposals made in this dissertation
to a real-world large applicationthe Fnix systemwhich is developed by a team of
more than a dozen of programmers. The results obtained with this application show the
effectiveness of the proposals made in this dissertation, both in the reduction of problems
related to the implementation of a rich domain model, and the adequacy of the proposals
for real-world usage.
It presents, also, a brief performance evaluation of the JVSTM using more standard
benchmarks in the area of STMs. The results of this performance evaluation show that,
even though the JVSTM was not specifically designed as a high-performance STM implementation, it performs very well either in comparison with some of the best-known STM
implementations for Java, or in comparison with a baseline traditional locking strategy.
More specifically, the JVSTM is one to two orders of magnitude faster than the DSTM2
and an implementation of the ASTM, as reported in [Guerraoui et al., 2007].
202
Validation
Chapter 8
Conclusions
In this concluding chapter, I present the main contributions of this dissertation and
discuss some of the new research avenues that this work opens.
8.1
Main Contributions
The primary goal of the work presented in this dissertation was to simplify the implementation of rich object-oriented domain models. Moreover, that this should be accomplished
in a programmers friendly waythat is, in such a way that it is not only easily comprehensible by an average programmer, but also that it is practical to use in current software
development. I achieved this goal, because throughout this dissertation I identified some
of the problems with the existing approaches, I proposed solutions to those problems, and
I validated those solutions. So, the achievement of this goal is the primary contribution
of my work.
Yet, for achieving this goal, I made more specific contributions in several of the areas
addressed by this work.
In the area of Software Transactional Memory, I distinguish the following main contributions:
204
Conclusions
in the DML language, a new pattern for implementing associations in an objectoriented programming language. Compared to the existing patterns for implementing associations, my proposal has the advantage of being simpler to implement
(once we have all the supporting classes implemented). Furthermore, it provides
a friendlier interface to the programmer that is now able to create or remove association links in several different ways, including through the use of the familiar
interfaces of the Java Collection Framework. This new pattern allows, also, that
third-party programmersthat is, programmers that do not have access to the code
implementing the associationcustomize the behavior of the associations operations.
I introduced the idea of using consistency predicates to separate the implementation
of a domain models constraints from the code that updates the state of the domain
entities. By doing so, we reduce the code scattering, the code tangling, and the
strong coupling that results from the usual approach of implementing these two
concerns together. Moreover, this approach facilitates the composition of objects in
a truly object-oriented spirit, by allowing the composition of existing objects without
having to adapt them specifically to the new composition. This is made possible by
having the verification of the consistency predicates separate from the remaining
of the code, so that the composing object may control when will the consistency
predicates be evaluated, without knowledge or consent from the composed object.
I described an implementation of consistency predicates in the JVSTM that is sufficiently generic to be used without significant changes by most, if not all, other STM
implementations. This implementation does not require any significant change to
the underlying STM implementation. Instead, it leverages on the support for atomicity given by STMs to implement the evaluation of consistency predicates and the
necessary recording of dependencies.
Finally, in the area of software engineering, I contributed with a study of the typical
transactional workloads of a large real-world web applicationthe Fnix systemwhich is
representative of a large class of applications. This study was performed for an extended
period of time and gives us valuable insights about the needs of such kind of applications,
so that better solutions, suited to their specificities, may be developed.
8.2
Future Research
Even though the work described in this dissertation is self-contained, in the sense that it
may be readily used without further developments, it does not constitute, quite naturally,
the ultimate solution for any of the problems that it addresses. Rather on the contrary,
the decision to make proposals that are simple to learn and to implement makes these
205
206
Conclusions
STM, which many other STMs use; the specifics of a versioned model, however,
may bring different design decisions into this integration. Another line of research
worthy of consideration is the use of an arbitrarily fine-grained structure of nested
transactions to allow the partial reexecution of a conflicting transaction. The idea,
in this case, is to avoid the whole reexecution of a conflicting transaction by reexecuting (eventually, with help from other threads) only the parts that may have
changed because of a conflict.
The extension of the DML language to support the specification of more aspects of
a domain models structure. The DML, as presented in this dissertation, allows
the representation of only a limited set of domain modeling constructs. Therefore,
it is to be expected that in the future other constructs be added to the language.
For instance, constructs to specify other kinds of associations, such as qualified
associations, or constructs to specify the refinement of entities or associations.
The extension of the design pattern for implementing associations. The pattern
introduced in this dissertation does not address other common requirements of
associations, such as that they should be ordered, or sorted, or even indexed by a
given attribute of one of the participating objects. Thus, an interesting and useful
line of research work would be the extension of this pattern to support such things.
The interaction between consistency predicates and other programming language
constructs. The integration of consistency predicates in a programming language
interferes with several other constructs that may exist in the language. For instance,
an obvious candidate is the system of exceptions of a language, given that consistency predicates may signal their failure through exceptions. Thus, it would be
interesting to see whether the exceptions thrown by consistency predicates should
deserve some kind of special treatment by the language. Or, even if not by the language, whether the distinction between exceptions thrown by consistency predicates
and other types of exceptions would be useful for a programmer, from a pragmatical
point of view.
More elaborate studies of the transactional workloads of real-world domain-intensive
applications. The study performed in this dissertation was rather simple, and served
mostly to validate the assumptions underlying the development of the versioned
STM. Yet, knowing in more detail how do real-world large applications use transactions may provide valuable feedback into the development of transactional systems.
These topics are, by no means, an exhaustive list of all the research work that may
follow from what is described in this dissertation. Rather, it is a list of some of the
topics on which I had already some thoughts, and with which I would like to finish my
dissertation.
207
208
Conclusions
Bibliography
Akehurst, D., Howells, G., and McDonald-Maier, K. Implementing associations: UML 2.0
to Java 5. Software and Systems Modeling, volume 6(1):pages 335, 2007.
Albano, A., Ghelli, G., and Orsini, R. A relationship mechanism for a strongly typed
object-oriented database programming language. In Proceedings of the 17th International Conference on Very Large Data Bases, pages 565575. 1991.
Alur, D., Crupi, J., and Malks, D. Core J2EE Patterns: Best Practices and Design Strategies. Prentice-Hall, Inc., New Jersey, USA, 2001.
ANSI and ITIC. American National Standard for information technology: programming
language Common LISP: ANSI X3.226-1994. American National Standards Institute,
1430 Broadway, New York, NY 10018, USA, 1996.
Arnold, K., Gosling, J., and Holmes, D. The Java Programming Language. Addison-Wesley,
Reading, Massachusetts, USA, third edition, 2000.
Barnett, M., DeLine, R., Fhndrich, M., Rustan, K., Leino, M., and Schulte, W. Verification of object-oriented programs with invariants. Journal of Object Technology,
volume 3(6):pages 2756, 2004. Special issue: ECOOP 2003 workshop on FTfJP.
Bartetzko, D. Parallelitt und Vererbung beim "Programmieren mit Vertrag" Weiterentwicklung von JaWA. Masters thesis, Universitt Oldenburg, 1999.
Bass, L., Clements, P., and Kazman, R. Software Architecture in Practice. SEI Series in
Software Engineering. Addison-Wesley, Reading, Massachusetts, USA, second edition,
2003.
Bernstein, P. A. and Goodman, N. Multiversion concurrency control theory and algorithms. ACM Transactions on Database Systems, volume 8(4):pages 465483, 1983.
Bierman, G. and Wren, A. First-class relationships in an object-oriented language. In
Proceedings of the 19th European Conference on Object-Oriented Programming, volume
3586 of Lecture Notes in Computer Science, pages 262286. Springer-Verlag, 2005.
Booch, G. Object-Oriented Analysis and Design with Applications. Addison-Wesley, Reading, Massachusetts, USA, second edition, 1994.
210
BIBLIOGRAPHY
Booch, G., Rumbaugh, J., and Jacobson, I. The Unified Modeling Language User Guide.
Addison-Wesley, 1999.
Cachopo, J. and Rito-Silva, A.
BIBLIOGRAPHY
Flanagan, C., Leino, K. R. M., Lillibridge, M., Nelson, G., Saxe, J. B., and Stata, R.
Extended static checking for java. SIGPLAN Not., volume 37(5):pages 234245, 2002.
Fowler, M. Patterns of Enterprise Application Architecture. Addison-Wesley, Reading,
Massachusetts, USA, 2002.
Fowler, M., Beck, K., Brant, J., Opdyke, W., and Roberts, D. Refactoring: Improving the
Design of Existing Code. Addison-Wesley, 1999.
France, R. B., Ghosh, S., Dinh-Trong, T., and Solberg, A. Model-driven development
using uml 2.0: Promises and pitfalls. Computer, volume 39(2):pages 5966, 2006.
Gabriel, R. P., Northrop, L., Schmidt, D. C., and Sullivan, K. Ultra-large-scale systems.
In Companion to the 21st ACM SIGPLAN Conference on Object-Oriented Programming,
Systems, Languages, and Applications, SIGPLAN Notices, pages 632634. ACM Press,
2006.
Gamma, E., Helm, R., Johnson, R., and Vlissides, J. Design Patterns: Elements of
Reusable Object-Oriented Software. Addison-Wesley, Reading, Massachusetts, USA,
1995.
Gnova, G., del Castillo, C. R., and Llorens, J. Mapping UML associations into java code.
Journal of Object Technology, volume 2(5):pages 135162, 2003.
Gnova, G., Llorens, J., and Martnez, P. The meaning of multiplicity of n-ary associations
in UML. Software and Systems Modeling, volume 1(2):pages 8697, 2002.
Gosling, J., Joy, B., Steele, G., and Bracha, G. The Java Language Specification. AddisonWesley, Reading, Massachusetts, USA, third edition, 2005.
Graham, P. and Barker, K. Effective optimistic concurrency control in multiversion object
bases. In Proceedings of the International Symposium on Object-Oriented Methodologies
and Systems, volume 858, pages 313328. Springer-Verlag, 1994.
Gueheneuc, Y.-G. and Albin-Amiot, H. A pragmatic study of binary class relationships.
In Proceedings of the 18th IEEE International Conference on Automated Software Engineering, pages 277280. 2003.
Guhneuc, Y.-G. and Albin-Amiot, H. Recovering binary class relationships: putting
icing on the UML cake. In Proceedings of the 19th ACM SIGPLAN Conference on ObjectOriented Programming, Systems, Languages, and Applications, volume 39 of SIGPLAN
Notices, pages 301314. ACM Press, 2004.
Guerraoui, R., Herlihy, M., and Pochon, S. Toward a theory of transactional contention
management. In Proceedings of the 24nd Annual ACM Symposium on Principles of
Distributed Computing. ACM Press, 2005.
211
212
BIBLIOGRAPHY
Guerraoui, R., Kapalka, M., and Vitek, J. STMBench7: A benchmark for software transactional memory. In Proceedings of the Second European Systems Conference. 2007.
Hailpern, B. and Tarr, P. Model-driven development: the good, the bad, and the ugly.
IBM Systems Journal, volume 45(3):pages 451461, 2006.
Harris, T. and Fraser, K. Language support for lightweight transactions. In Proceedings
of the 18th ACM SIGPLAN Conference on Object-Oriented Programming, Systems, Languages, and Applications, volume 36 of SIGPLAN Notices, pages 388402. ACM Press,
2003.
Harris, T., Marlowe, S., Peyton-Jones, S., and Herlihy, M. Composable memory transactions. In Proceedings of the ACM SIGPLAN Symposium on Principles and Practice of
Parallel Programming. ACM Press, 2005.
Harris, T. and Peyton-Jones, S. Transactional memory with data invariants. In First ACM
SIGPLAN Workshop on Languages, Compilers, and Hardware Support for Transactional
Computing. 2006.
Harrison, W., Barton, C., and Raghavachari, M. Mapping UML designs to Java. In
Proceedings of the 15th ACM SIGPLAN Conference on Object-Oriented Programming, Systems, Languages, and Applications, volume 35 of SIGPLAN Notices, pages 178187.
ACM Press, 2000.
Herlihy, M., Luchangco, V., and Moir, M. A flexible framework for implementing software
transactional memory. In Proceedings of the 21st Annual ACM SIGPLAN Conference
on Object-Oriented Programing, Systems, Languages, and Applications, pages 253262.
ACM Press, 2006.
Herlihy, M., Luchangco, V., Moir, M., and Scherer, III., W. N. Software transactional
memory for dynamic-sized data structures. In Proceedings of the 22nd Annual ACM
Symposium on Principles of Distributed Computing, pages 92101. ACM Press, 2003.
Herlihy, M. and Moss, J. E. B. Transactional memory: Architectural support for lockfree data structures. In Proceedings of the 20th Annual International Symposium on
Computer Architecture. 1993.
Herlihy, M. P. Wait-free synchronization. ACM Transactions on Programming Languages
and Systems, volume 13(1):pages 124149, 1991.
Herlihy, M. P. and Wing, J. M. Linearizability: a correctness condition for concurrent objects. ACM Transactions on Programming Languages and Systems, volume 12(3):pages
463492, 1990.
Hoare, C. A. R. Proof of correctness of data representations. Acta Informatica, volume 1:pages 271281, 1972.
BIBLIOGRAPHY
Iscoe, N., Williams, G. B., and Arango, G. Domain modeling for software engineering. In
Proceedings of the 13th International Conference on Software Engineering, pages 340
343. IEEE Computer Society, 1991.
Joy, B., Guy L. Steele, J., Gosling, J., and Bracha, G. The Java Language Specification.
Addison-Wesley, Reading, Massachusetts, USA, second edition, 2000.
JVSTM. JVSTM. 2005. Home page at http://www.esw.inesc-id.pt/~jcachopo/
jvstm.
Karaorman, M., Hlzle, U., and Bruno, J. jContractor: A reflective java library to support
Design by Contract. In Proceedings of the 2nd International Conference on Meta-Level
Architectures and Reflection, number 1616 in Lecture Notes in Computer Science, pages
175196. Springer-Verlag, 1999.
Kiczales, G., Lamping, J., Menhdhekar, A., Maeda, C., Lopes, C., Loingtier, J.-M., and
Irwin, J. Aspect-oriented programming. In M. Aksit and S. Matsuoka, (Editors) Proceedings of the 11th European Conference on Object-Oriented Programming, volume 1241 of
Lecture Notes in Computer Science, pages 220242. Springer-Verlag, 1997.
Kramer, R. iContract the Java Design by Contract tool. In Proceedings of the TOOLS98:
Technology of Object-Oriented Languages and Systems, pages 295307. IEEE Computer
Society, 1998.
Lackner, M., Krall, A., and Puntigam, F. Supporting design by contract in java. Journal
of Object Technology, volume 1(3):pages 5776, 2002. Special issue: TOOLS USA 2002
proceedings.
Leavens, G. T., Cheon, Y., Clifton, C., Ruby, C., and Cok, D. R. How the design of JML
accommodates both runtime assertion checking and formal verification. Science of
Computer Programming, volume 55:pages 185208, 2005.
Leavens, G. T., Ruby, C., Rustan, K., Leino, M., Poll, E., and Jacobs, B. Jml (poster session): notations and tools supporting detailed design in java. In OOPSLA 00: Addendum to the 2000 proceedings of the conference on Object-oriented programming, systems,
languages, and applications (Addendum), pages 105106. ACM Press, 2000.
Manson, J., Pugh, W., and Adve, S. V. The java memory model. In Conference Record
of the 32th ACM SIGACT-SIGPLAN Symposium on Principles of Programming Languages,
pages 378391. ACM Press, 2005.
Marathe, V. J., Scherer, W. N., and Scott, M. L. Design tradeoffs in modern software
transactional memory systems. In Proceedings of the 7th Workshop on Languages,
Compilers, and Run-Time Support for Scalable Systems, pages 17. ACM Press, 2004.
Marathe, V. J. and Scott, M. L. A qualitative survey of modern software transactional
memory systems. Technical Report UR CSD;TR 839, 2004.
213
214
BIBLIOGRAPHY
Mellor, S. J., Scott, K., Uhl, A., and Weise, D. MDA Distilled. Addison-Wesley, Reading,
Massachusetts, USA, 2004.
Meyer, B. Object-Oriented Software Construction. Prentice-Hall, Inc., New Jersey, USA,
1988.
Meyer, B. Applying design by contract. Computer, volume 25(10):pages 4051, 1992a.
Meyer, B. Eiffel: The Language. Prentice-Hall, Inc., New Jersey, USA, 1992b.
Moss, J. E. B. and Hosking, A. L. Nested transactional memory: Model and preliminary architecture sketches.
Available at http://hdl.handle.
net/1802/2099.
Mller, P., Poetzsch-Heffter, A., and Leavens, G. T. Modular invariants for layered object
structures. Science of Computer Programming, volume 62(3):pages 253286, 2006.
Noble, J. Basic relationship patterns. volume 4, chapter 6, pages 7394. Addison-Wesley,
Reading, Massachusetts, USA, 2000.
Noble, J. and Grundy, J. Explicit relationships in object-oriented development. 1995.
Object Management Group. Unified modeling language: Superstructure (version 2.0).
Available at http://www.omg.org/cgi-bin/doc?formal/05-07-04, Visited in
2007.
OJB. Object/Relational Bridge OJB. Visited in 2007. Home page at http://db.
apache.org/ojb.
Okasaki, C. Purely Functional Data Structures. Cambridge University Press, Cambridge,
MA, USA, 1998.
OMG. OMG Model Driven Architecture. 2007. Home page at http://www.omg.org/
mda.
Parnas, D. L. On the criteria to be used in decomposing systems into modules. Communications of the ACM, volume 15(12):pages 10531058, 1972.
Pearce, D. J. and Noble, J. Relationship aspects. In Proceedings of the 5th International
Conference on Aspect-Oriented Software Development, pages 7586. ACM Press, 2006.
Polak, B., (Editor) Ultra-Large-Scale Systems: The Software Challenge of the Future. Software Engineering Institute, Carnegie Mellon, Pittsburgh, USA, 2006.
Available at
http://www.sei.cmu.edu/uls/.
Pugh, W. Fixing the java memory model. In Proceedings of the ACM 1999 Conference on
Java Grande, pages 8998. ACM Press, 1999.
BIBLIOGRAPHY
215
Raistrick, C., Francis, P., and Wright, J. Model Driven Architecture with Executable UML.
Cambridge University Press, Cambridge, MA, USA, 2004.
Reed, D. P. Naming and Synchronization in a Decentralized Computer System. Ph.D.
thesis, MIT, Cambridge, MA, USA, 1978.
Reed, D. P. Implementing atomic actions on decentralized data. ACM Transactions on
Computer Systems, volume 1(1):pages 323, 1983.
Riegel, T., Felber, P., and Fetzer, C. A lazy snapshot algorithm with eager validation. In
20th International Symposium on Distributed Computing (DISC). 2006a.
Riegel, T., Fetzer, C., and Felber, P. Snapshot isolation for software transactional memory.
In First ACM SIGPLAN Workshop on Languages, Compilers, and Hardware Support for
Transactional Computing. 2006b.
Riehle, D. Framework Design: A Role Modeling Approach. Ph.D. thesis, Swiss Federal
Institute of Technology Zurich, 2000.
Royce, W. W. Managing the development of large software systems: concepts and techniques. In Proceedings of the 9th International Conference on Software Engineering,
pages 328338. IEEE Computer Society, 1987.
Rumbaugh, J. Relations as semantic constructs in an object-oriented language. In Proceedings of the 2nd ACM SIGPLAN Conference on Object-Oriented Programming, Systems, Languages, and Applications, volume 22 of SIGPLAN Notices, pages 466481.
ACM Press, 1987.
Scherer III, W. N. and Scott, M. L. Contention management in dynamic software transactional memory. In Proceedings of the ACM PODC Workshop on Concurrency and
Synchronization in Java Programs. 2004.
Scherer III, W. N. and Scott, M. L. Advanced contention management for dynamic software transactional memoryy. In Proceedings of the 24nd Annual ACM Symposium on
Principles of Distributed Computing. ACM Press, 2005.
Selic, B. The pragmatics of model-driven development. IEEE Software, volume 20(5):pages
1925, 2003.
Shah, A. V., Hamel, J. H., Borsari, R. A., and Rumbaugh, J. E.
DSM: an object-
216
BIBLIOGRAPHY
Suscheck, C. A. and Sandn, B. A construct for effectively implementing semantic associations. Journal of Object Technology, volume 2(3):pages 101111, 2003.
Thomas, D. UML - Unified or Universal Modeling Language? Journal of Object Technology,
volume 2(1):pages 712, 2003.
Thomas, D. and Barry, B. M. Model driven development: the case for domain oriented
programming. In Companion to the 18th ACM SIGPLAN Conference on Object-Oriented
Programming, Systems, Languages, and Applications, SIGPLAN Notices, pages 27. ACM
Press, 2003.