Academia.eduAcademia.edu

Towards a model of encapsulation

2003

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.

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.