Towards a Model of Encapsulation
James Noble1 , Robert Biddle1 , Ewan Tempero2, Alex Potanin1 , and Dave Clarke3
1
School of Mathematical and Computing Sciences, Victoria University of Wellington
2
Department of Computer Science, The University of Auckland
3
Institute of Information and Computing Sciences, Utrecht University
Abstract. Encapsulation is a founding principle of object-oriented programming:
to this end, there have been a number of recent of proposals to increase programming languages’ support for encapsulation. While many of these proposals are
similar in concept, it is often difficult to describe their effects in practice, or to
evaluate clearly how related proposals differ from each other. We are developing a general topological model of encapsulation for object-oriented languages,
based on a program’s object graph. Using this model, we can characterise a range
of confinement, ownership, and alias protection schemes in terms of their underlying encapsulation function. This analysis should help programmers understand
the encapsulation provided by programming languages, assist students to better compare and contrast the features of different languages, and help language
designers to craft the encapsulation schemes of forthcoming programming languages.
1 Introduction
We are developing a simple topological model of encapsulation in object-oriented systems, programming languages, and more general computer systems. This model has
three parts — an access graph describing the topology of the system under consideration; an encapsulation function describing how particular parts of the system are
encapsulated; and an encapsulation constraint that, when true, captures the fact that
the encapsulation function partitions the access graph so that so part of it is actually
encapsulated.
For this position paper, we take a statement of Dan Ingalls as the defining quality of
encapsulation1:
No component in a complex system should depend on the internal details
of any other component. [14]
As far as we are aware, there is as yet no agreed formalisation of encapsulation: our
model is intended to be a step in that direction.
1
Glossing over the fact that Ingalls was defining modularity rather than encapsulation.
1.1 Access Graph
The first part of our model is an access graph. An access graph models the topology of
a system: nodes (N ) in the graph represent individual objects in the model, and edges
(E) between nodes represent accesses between objects.
Nodes and edges may (or may not) be labelled: node labels are typically given by
boolean predicates (as functions from N to ); or functions to (sets of) nodes, components, or model-specific sets; edges may be labeled similarly (e.g. i, r, w). Our formulation of encapsulation does not depend upon whether the graph is directed: in an
undirected graph we take an edge (a, b) to mean that a accesses b and b accesses a.
Some notation: we write S for all the objects in the system, that is, the set of all
nodes in the graph; {a}✄ to be the set of all other nodes that have edges beginning
from a and ✄{a} to be the set of all other nodes that have edges ending at a; {a}✄✄
and ✄✄{a} for their transitive closures (all other nodes which can be reached from a
and which can reach a respectively); a −→ b to mean that there is an edge from a to b;
paths(a, b) for the set of all paths (a set of sequences of nodes) from node a to node b;
a ⊑ b to mean that a is a dominator for b, that is, every path to b from some nominated
entry point leads through a. If edges are labelled, we may subscript graph operations
to restrict to matching edges (i.e. a✄✄i is all other nodes reachable from a along edges
labelled i).
Our access graph is unremarkable in its suburban monotony, being at heart a directed graph: our whole model of encapsulation is similarly simple-minded. The reason
for this naı̈ve formulation is that the access graph is itself an abstraction: our model
of encapsulation will work over any directed graph model that meets these criteria. In
this position paper, we will consider only object graphs, with objects as nodes and their
variables (references between objects) as edges. We may build this graph informally
based on some kind of object identifiers, use Zeller’s Memory Graphs [19] or Hoare
and Jifeng’s trace model [12], or some other formulation of an object-oriented program
that also is based on a directed graph [10]. We can decorate the graph as necessary to
distinguish between static or dynamic object accesses, the classes, packages, modules,
block structure, or files to which objects belong. We expect the ubiquity of the access
graph will allow us to address some other kinds of encapsulation in object-oriented
systems and encapsulation in non-object-oriented (and non-informatic) systems.
1.2 Encapsulation Function
The second part of the model is the encapsulation function. The encapsulation function
partitions the system (S) by taking an identifier of an encapsulated component (C) to
a three-tuple of sets of access graph nodes that respectively describe the encapsulation
boundary (B), the inside (I), and the external references (R) of the component. We will
use e.g. B to represent Bc for some given c ∈ C.
C : id
S, B, I, R, O : N
e : C → (B, I, O)
✁
The boundary of a component represents the interface that components presents to
the rest of system, while the inside is that part of the component which is encapsulated, and may itself be a network of interconnected objects. The inside of a component
can only be accessed via the component’s interface, that is, by crossing its encapsulation boundary. The external references of a component are nodes which the boundary
or inside accesses directly, but which are not contained within boundary or inside of
the component. Finally, the outside (O) of a component are all nodes which are neither inside the component nor on its boundary — including the component’s external
references.
The idea that encapsulation or confinement is fundamentally a partition of some
software space is not a new one: a range of recent encapsulation schemes have been
(formally) described in such terms [3, 4, 18]. Here, we argue that this is not accidental:
rather, encapsulation is essentially defined by such a partition.
O = S − (B ∪ I)
R⊆O
This is illustrated in figure 1 below. All the nodes in the graph S represent the whole
system: those nodes outside any subset boundary are in the outside O of the component
shown in the diagram.. The boundary B (the left subset) provide the interface to the encapsulated component: they can be accessed from anywhere — outside the component,
inside it, from other boundary nodes, or from the component’s external references. The
centre subset in the diagram represent the inside I of the component: nodes here may
only be accessed by each other or by the boundary nodes. The right-most subset are the
outside nodes referred to by the inside and boundary of the component — its external
references R.
O
I
R
B
Fig. 1. An encapsulated component
Note that we make a distinction between components (C) and nodes (N ): components are not nodes: rather a component encapsulates several nodes (via its encapsulation function e) by identifying an encapsulation boundary, the nodes inside that boundary, and any external references leaving that boundary. In an object graph, objects are
nodes, however components may be based upon a range of structures, including objects
but also packages, classes, and universes, as we will see below.
The argument to the encapsulation function — a component identifier — effectively
chooses a particular partition of the system. For that component, the function selects
those nodes that will be on the boundary, inside, external, and outside. The interpretation of the component identifier depends upon the particular scheme being modelled,
and is typically linked to the access graph proper via node labels. For example, for
package-based encapsulation (such as confined types, see section 2.6 below) the unit
of encapsulation is a package, so component identifiers model packages, and nodes
need a labelling function (say package : N → C) to identify the package to which
they belong. An encapsulation function for such a scheme takes an identifier (such as
“java.lang.util”) as its argument to select the package that is encapsulated: the
function itself will then choose objects based on their package (e.g. grouping objects
of classes in java.lang.util). By contrast, in object-based encapsulation schemes
such as islands or balloons, each node forms an encapsulated component, so we let
C = N.
1.3 Encapsulation Constraints
So far, we have an access graph as a (directed) graph; and an encapsulation function
which selects nodes from that graph. The third part of our model is the encapsulation
constraints that link the access graph and encapsulation function. These constraints
ensure that the encapsulation function does actually describe encapsulation: that inside
nodes are only accessed via the boundary, and that outside nodes are only accessed via
the external references.
More formally, the encapsulation constraints are as follows: first, a component’s
boundary, inside, and externals must be disjoint (this implies that the externals must be
outside the component).
B ∩I = B ∩R = I ∩R = {}
The second constraint is the most important one: the boundary must actually be a
boundary for the inside. To be a valid encapsuation the nodes inside an encapsulated
component can only be accessed via the component’s boundary: that is, any edges ending at inside nodes can only come from other inside nodes or boundary nodes:
✄I ⊆ (B ∪ I)
Alternatively we could state that all paths from the outside of a component into the
inside must pass through the boundary.
∀o ∈ O : ∀i ∈ I : ∀p ∈ paths(o, i) : ∃b ∈ p s.t. b ∈ B
where paths(i, o) is all paths (as a set of sequences of nodes) from i to o.
Finally we require that every outside node directly accessed by an inside node is
recorded in the component’s externals:
(B ∪ I)✄ ⊆ (B ∪ I ∪ R)
Note that this sets a lower bound on the extent of the external references — a component
may include outside objects in its external references even if it does not refer to them.
This is useful because it allows us to set R = O to indicate that an object’s external
references are unconstrained (note that the outside (O) is all nodes in the system (S)
except the boundary and interface — O = S − (B ∪ I)). In any event, one can always
define an alternative encapsulation function with a tighter external reference set.
This formal definition is designed to capture the intent of Dan Ingalls’ statement
above:
No component in a complex system should depend on the internal details
of any other component. [14]
In our model, the “external aspects” of an encapsulated component lie on its boundary, while the “internal details” are its inside. The encapsulation constraints ensure that
these internal details of a component are hidden from every external component in the
system.
1.4 Nested Encapsulation
These definitions imply that encapsulated components may be nested inside each other.
We say that c1 contains c2 (c1 ≤ c2 ) or c2 is inside c1 (c2 ≥ c1 ) if all c2 ’s boundary and
inside are part of the boundary and inside of c1 (see figure 2). That is ≤ is defined as:
c1 ≤ c2 iff Bc2 ⊆ (Bc1 ∪ Ic1 ) ∧ Ic2 ⊆ (Ic1 )
One consequence of this definition is that the external references of the inner component must be contained within the inside, boundary, or external references of the outer
component:
Rc2 ⊆ (Bc1 ∪ Ic1 ∪ Rc1 )
2 Encapsulation Schemes over Object Graphs
Having outlined our model of encapsulation, in this section we apply that model to
a range of encapsulation schemes over object graphs, beginning with simple implicit
models and progressing to more complex schemes. We describe each scheme by its
characteristic encapsulation function.
An object graph is part of a program’s operational state: the graph will evolve as
the program runs, as objects are created and deleted and as assignments change references between nodes. This is the reason that we characterise encapsulation schemes
I1
O
R1
I2
B1
B2
R2
Fig. 2. Nested Components
by encapsulation functions: these functions are implicitly parameterised by the object
graphs to which they are applied. Typically, an encapsulation scheme will mandate that
the encapsulation constraints hold at all times in all runs of all programs to which the
scheme applies. Different schemes will use a wide range of mechanisms to achieve this,
from dynamic checks at assignment or invocation, through static annotations, extended
type systems, abstract interpretation, theorem proving, or even providing no enforcement at all and simply relying on programmers’ adherence to conventions. One key
advantage of our model of encapsulation functions (and access graphs) is that they can
abstract away considerations of mechanism and provide succinct characterisations of
the encapsulation policies supported by each scheme — in particular, the topology of
encapsulation that each scheme provides.
2.1 Islands: Full Encapsulation
Hogg’s Islands [13] was the first alias protection scheme advocated for an objectoriented language. The key notion of an Island is that some classes in the program
are distinguished as bridge classes: instances of those classes are subject to a series of
syntactic restrictions, so that all objects reachable from the bridge — i.e. the objects
making up the island — are only reachable from the bridge (see figure 3).
In terms of an encapsulation function, an object o that has been identified as an
bridge (using the label bridge : N → ) has itself as a boundary, all objects reachable
via that boundary as its inside, and no external references:
B = {o}
eisland (o) = I = {o}✄✄
R = {}
where bridge(o)
Islands are one example of an alias protection scheme with a full encapsulation topology: the encapsulation functions of such schemes are characterised by having empty
Fig. 3. Full Encapsulation
external references. Almeida’s Balloon Types [2] also mandate full encapsulation, as
does Banerjee and Naumann’s original work on heap confinement [3].
Note that Island’s encapsulation function must also be interpreted to cover only
static references (from objects’ fields and global variables) but not dynamic references
(such as local variables or method arguments on the stack). Islands provides no protection for dynamic references.
2.2 Uniqueness
Islands also supports another common type of encapsulation, uniqueness. A classically
unique object has only one incoming reference (| ✄ u| = 1) and so a unique object is
always encapsulated by the sole object that refers to it. This condition produces a range
of encapsulation function.
Most generally, a unique object may refer to any other object in the system (see
figure 4):
B = ✄{u}
eunique(u) = I = {u}
R=O
where | ✄ {u}| = 1
where we say that the external references are the outside (O) of the unique object to
mean that the references are unconstrained (not that the object necessarily refers to
every outside object!). Note also that this definitions works by placing the unique object
into the inside of the encapsulation: the encapsulation boundary is the (sole) object
which refers to this object.
Clarke and Wrigstrad have recently demonstrated that a weaker formulation of
uniqueness can provide all the benefits of full uniqueness but is significantly more flexible. An externally unique object [7, 8] may have only one reference from outside, but
may have any number of other references from inside:
1
unique
Fig. 4. A Unique Object
B = {v}
eexternal-unique(u) = I = {u} ∪ {o|u ⊑ o}
R=O
where v = (✄{u} − I) and |v| = 1
1
Fig. 5. External Uniqueness
Again, the encapsulation boundary is the object that refers to the externally unique object. Here, however, an unique object contains a number of other objects but any incoming references from these objects do not count against the uniqueness of the externally
unique object — external uniqueness counts only those references from the boundary
in to the unique object, not references from the inside of the object.
2.3 Balloon Types
Alemida’s Balloon Types [2] provide full encapsulation, however, they are also constrained to be unique. Using a label (balloon : N → ) for balloon objects, we can
model this with two nested encapsulations, a inner full encapsulation
B = {b}
einner-balloon(b) = I = {b}✄✄
R = {}
where balloon(b)
and an outer unique object:
B = ✄{b}
eouter-balloon(b) = I = {b}
R = {b}✄✄
where balloon(b) ∧ | ✄ {b}| = 1
or as a single encapsulation function encompassing both the balloon object and the
contents of the balloon, and having as its boundary the (sharable) object holding the
single reference to balloon object proper:
B = ✄{b}
eballoon (b) = I = {b} ∪ {b}✄✄
R = {}
where balloon(b) ∧ | ✄ {b}| = 1
These are then nested, so that eballoon ≤ einner-balloon and eballoon ≤ eouter-balloon .
I outer
Bouter
inner
balloon
R outer
I inner
Rinner
balloon
Binner
Fig. 6. Balloon Types
2.4 Flexible Alias Protection
Flexible Alias Protection [16] was proposed to resolve problems with full static alias
protection schemes such as Islands and Balloons: these schemes were simultaneously
too lax, in that dynamic references could penetrate Islands and plain balloons, and too
strict, as no external references were permitted. Flexible Alias Protection divided the
objects within an “alias-protected container” into two main categories: representation
objects which were private, and the arguments objects which could be shared. Flexible alias protection used “modes” annotating static types to track which objects were
representation and which arguments: we can model this here with a pair of labelling
functions (rep, arg : N → N ). Unlike Islands, Flexible Alias Protection maintained
the same encapsulation on dynamic references as static references — representation
objects could never be accessed externally.
To a first approximation, the encapsulation function for Flexible Alias Protection is:
B = {o}
eflexible (o) = I = rep(o)
R = arg(o)
✁
with an object’s representation being its inside, and arguments its external references
(see figure 7 compare with 3). Flexible Alias Protection’s restrictions on the use of
modes (types of different mode are not assignment compatible; representation modes
cannot appear in a protected container’s interface, and so on) ensure that the labelling
actually resulted in an encapsulation.
Fig. 7. Flexible Encapsulation Topology
2.5 Ownership Types
Ownership Types [9] were first designed to formalise the topological restrictions of
Flexible Alias Protection, although they have since found many difference applications. The key idea behind ownership types is that a type system statically enforces
topological restrictions based on an ownership relation between objects: we can model
this relationship by labelling the basic object graph so that every node has an owner
(owner : N → N ) which is another node. We write i ownedby o for the transitive
closure of the owner function, and also o owns i for the inverse. The ownership relation
is constrained to form a convergent tree at some nominated root (owner(root) = root)
and the type system then enforces a containment invariant:
s −→ t ⇒ s ownedby owner(t)
that is, the source of an edge must be owned by the owner of the target of an edge. This
containment invariant ensures that ownership partitions the graph into a series of nested
encapsulations based on each object owning its inside (see figure 8).
Fig. 8. Ownership Types
Using this ownership graph as the access graph, the most basic encapsulation function for ownership types have the form of:
B = {o}
esimple-ownership (o) = I = {i|o owns i}
R = {r|o ownedby owner(r)}
The external reference expression for ownership types can be simplified if (the types
of) objects are explicitly parameterised with the ownership of the objects to which they
may refer (params : N − > N ):
B = {o}
eparam-ownership(o) = I = {i|o owns i}
R = {r|owner(r) ∈ params(o)}
where ∀p ∈ params(o) : o ownedby p
✁
These ownership type systems are known as owners-as-dominators models, because
the containment invariant ensures that every object is dominated by their sole owner
(owner(o) ⊑ o). The encapsulation function shows how this forms an encapsulation,
furthermore, the tree of owners gives a matching tree of nested encapsulations:
o1 owns o2 ⇒ e(o1 ) ≤ e(o2 )
Because the owners-as-dominators versions of ownership types limits a component’s boundary to be a single object, these simple ownership types restrict some programming idioms. Recently, Boyapati and Liskov have extended ownership types to
allow multiple objects in a component’s boundary, where the extra objects are Java (or
BETA) style inner objects nested within the main object [6]. This changes the encapsulation function as follows:
B = {o} ∪ {b|outer(b) = o}
einner-ownership (o) = I = {i|o owns i}
R = {r|o ownedby owner(r)}
where outer : N → N gives the outer object within which a (non-static) inner class instance is nested (see figure 9). Banerjee and Naumann’s later work on heap confinement
also supports this topology [4].
Fig. 9. Extended Ownership Types
2.6 Confined Types
There are a range of other type-based encapsulation schemes. Confined types [5, 11] are
a package-wide type discipline: some classes are marked as confined, and instances of
these classes can only be accessed by instances from the same package. Objects that are
not confined within the package provide the interface by which their confined types are
manipulated (figure 10). We model this by decorating every object with the package to
which their class belongs (package : N → C) and a bit to indicate whether or not that
class is confined2 (confined : N → ):
2
We could add another layer of indirection here to treat classes explicitly, but this would needlessly complicate the model.
B = {o|package(o) = p ∧ ¬confined(o)}
econfined (p) = I = {o|package(o) = p ∧ confined(o)}
R=O
✂✂ ☎✄
✂✂ ☎☎✂✂
☎✄
☎✄
☎✄
✂☎✂✄
✂☎✄
☎✄
✂☎✄
✂ ☎✂☎✂☎✂
☎✂✄☎✄
✆✆ ✝✄
✆✆ ✝✝✆✆
✝✄
✝✄
✝✄
✆✝✆✄
✆✝✄
✝✄
✆✝✄
✆ ✝✆✝✆✝✆
✝✆✄✝✄
✞✞✄
✞✞✄
✟✄
✟✞✞ ✟✄
✟✞✞ ✟✞✞✟✟✞✞
✟✄
✟✄
✟✄
✞ ✟✟✞
✟✞✄✟✄
✟✄
☛✄
☛✄☞✄☞✄☛ ☛☞☞☛
☞✄
☛☞✄
☞✄
☛☞✄
☞✄☛ ☞☛
☛
✠
✠
✡✠✄
✄
✡
✡
☛
✠✡✄
✠✡✄
✠✡✠ ☞✄☛☞✄☛☞✄☛☞☛☞
✄
✄
✡
✡
✠✡✄
✠
✠✡✄
✠✡✄
✠✡ ✡✄
✠ ✡✠✡✠
Fig. 10. Confined Types
2.7 Universes
Universes [15] are an alternative to ownership types, based on read-only references
rather than ownership parameterisation. In particular, objects are encapsulated only
with respect to read-write references: read-only references are unencapsulated (see figure 11). A universe plays a similar role to an owner in ownership types (every object
belongs to a universe — universe : N → C), however as well as encapsulation based
on objects (called object universes), universes also supports wider encapsulation (called
type universes).
Considering only read-write references, then, every object has one object universe
(objectu : N → C, one-to-one) but may be associated with a set of type universes
(typeu : N → ✁ C). An object that uses only its own object universe encapsulates all
the objects in that universe:
B = {o|objectu(o) = u}
eobject-universe (u) = I = {i|universe(i) = u}
R=O
where u is the unique object universe of o
as shown in figure 11. Alternatively, we can consider the encapsulation afforded solely
by a type universe:
r
r
r
r
Fig. 11. Universes
B = {o|typeu(o) ∋ u}
etype-universe (u) = I = {i|universe(i) = u}
R=O
where u is a type universe
such a component’s boundary contains all the objects which share that type universe.
Note that both these encapsulation functions do not restrict the outgoing references in
any way, and may apply to both read-only and read-write references.
Determining encapsulation based on objects (rather than based on universes) is
rather more difficult. There are two issues here. First, object universes must be transitive
(rather like ownership); because an object owns all the objects in its object universe, it
also effectively owns all objects in those objects’ object universes, and so on (again, we
can write “o owns i” mean i is (transitively) in o’s object universe). On the other hand,
any of these objects may use a type universe, meaning they may access any object transitively owned by that universe, but they must share these objects with other objects that
also use the same type universe.
The resulting encapsulation function has the advantage that is offers full encapsulation, that is, in this case, that there are no outgoing read-write references.
B = {o} ∪ {b|typeu(b) ∩ T 6= { }}
euniverses (o) = I = {i|o owns i} ∪ {t|universe(t) ∈ T }
R = {}
where T is the set of all type universes transitively reachable from o
3 Conclusion
In this position paper we have outlined our generic model of encapsulation, and applied
that to a number of alias protection schemes over object graphs. In the future, we plan
to apply this model to more object-graph schemes [1, 17]; to model finer grained access graphs for programming languages; and to encapsulation in non-object oriented
languages and in computer systems more generally.
References
1. J. Aldrich, V. Kostadinov, and C. Chambers. Alias annotations for program understanding.
In OOPSLA Proceedings, November 2002.
2. Paulo Sérgio Almeida. Balloon Types: Controlling sharing of state in data types. In ECOOP
Proceedings, June 1997.
3. Anindya Banerjee and Dave Naumann. Representation independence, confinement, and access control. In Proceedings of ACM Principles of Programming Languages (POPL), pages
166–177, 2002.
4. Anindya Banerjee and Dave Naumann. Ownership: transfer, sharing, and encapsulation. In
Dave Clarke, Sophia Drossopoulou, and James Noble, editors, Proceedings of IWAOOS’03:
The ECOOP 2003 International Workshop on Aliasing, Confinement, and Ownership, July
2003.
5. Boris Bokowski and Jan Vitek. Confined types. In OOPSLA Proceedings, 1999.
6. Chandrasekhar Boyapati, Barbara Liskov, and Liuba Shrira. Ownership types for object encapsulation. In ACM Symposium on Principles of Programming Languages (POPL), January
2003.
7. Dave Clarke and Tobias Wrigstrad. External uniqueness. In Foundations of Object-Oriented
Languages (FOOL), 2003.
8. Dave Clarke and Tobias Wrigstrad. External uniqueness is unique enough. In ECOOP
Proceedings, 2003.
9. David Clarke, John Potter, and James Noble. Ownership types for flexible alias protection.
In OOPSLA Proceedings, 1998.
10. Peter Grogono and Mark Gargul. A graph model for object oriented programming. SIGPLAN
Notices, pages 21–28, July 1994.
11. Christian Grothoff, Jens Palsberg, and Jan Vitek. Encapsulating objects with confined types.
In OOPSLA Proceedings, 2001.
12. C.A.R. Hoare and He Jifeng. A trace model of pointers and objects. In ECOOP Proceedings,
1999.
13. John Hogg. Islands: Aliasing protection in object-oriented languages. In OOPSLA Proceedings, November 1991.
14. Dan H. H. Ingalls. Design principles behind Smalltalk. BYTE, pages 286–298, August 1981.
15. P. Müller and A. Poetzsch-Heffter. A type system for controlling representation exposure in
Java. In ECOOP Workshop on Formal Techniques for Java Programs, number TR 269 in
Technical Report. Fernuniversität Hagen, 2000.
16. James Noble, Jan Vitek, and John Potter. Flexible alias protection. In ECOOP Proceedings,
1998.
17. Alan Cameron Wills. Formal Methods applied to Object-Oriented Programming. PhD thesis,
University of Manchester, 1992.
18. Tian Zhao, Jens Palsberg, and Jan Vitek. Lightweight confinement for featherweight java. In
OOPSLA Proceedings, November 2003. A preliminary version appeared in the Proceedings
of IWAOOS’03: The ECOOP 2003 International Workshop on Aliasing, Confinement, and
Ownership.
19. Thomas Zimmermann and Andreas Zeller. Visualizing Memory Graphs. In Proceedings of
Dagstuhl Seminar on Software Visualization, 2001, pages 191–204. Springer-Verlag, May
2001.