Composable Encapsulation Policies⋆
Nathanael Schärli1 , Stéphane Ducasse, Oscar Nierstrasz, and Roel Wuyts2
1
2
Software Composition Group
University of Bern
www.iam.unibe.ch/∼scg
Lab for Software Composition and Decomposition
Université Libre de Bruxelles
http://homepages.ulb.ac.be/∼rowuyts/
Abstract. Given the importance of encapsulation to object-oriented
programming, it is surprising to note that mainstream object-oriented
languages offer only limited and fixed ways of encapsulating methods.
Typically one may only address two categories of clients, users and heirs,
and one must bind visibility and access rights at an early stage. This can
lead to inflexible and fragile code as well as clumsy workarounds. We
propose a simple and general solution to this problem in which encapsulation policies can be specified separately from implementations. As such
they become composable entities that can be reused by different classes.
We present a detailed analysis of the problem with encapsulation and
visibility mechanisms in mainstream OO languages, we introduce our
approach in terms of a simple model, and we evaluate how our approach
compares with existing approaches. We also assess the impact of incorporating encapsulation policies into Smalltalk.
1
Introduction
Encapsulation is widely acknowledged as being one of the cornerstones of objectoriented programming [Nie89]. Nevertheless, the term encapsulation is often used
in inconsistent ways.
At the very least, encapsulation refers to the bundling together of data and
the operations that manipulate them. That is, information hiding is not necessarily an essential component of encapsulation. At the same time, the terms
encapsulation boundary and violation of encapsulation suggest that information
hiding is typically, albeit not necessarily, implied by encapsulation. In practice,
depending on the programming language in use, or the programming conventions
being applied, different policies concerning encapsulation may be in effect.
Snyder, in a classic paper [Sny86], defines encapsulation as follows.
Encapsulation is a technique for minimizing interdependencies among separately-written modules by defining strict external interfaces. The external interface
of a module serves as a contract between the module and its clients, and thus
between the designer of the module and other designers.
⋆
In Proceedings ECOOP 2004, LNCS 3086, pp. 248–274, Springer Verlag, 2004
2
Nathanael Schärli, Stéphane Ducasse, Oscar Nierstrasz, and Roel Wuyts
We feel this definition captures the essence of encapsulation, but we observe
(as did Snyder) that present day object-oriented programming languages are
surprisingly weak in terms of the mechanisms they offer programmers to establish
the encapsulation policies that a class offers to its clients. In particular, we
identify the following three weaknesses as being endemic to OO languages:
1. Access rights are inseparable from classes: access rights to methods are specified as part of their implementation. As a consequence, it is neither possible
to apply the same policies in a reusable way to different classes, nor is it
possible to apply different policies to the same class.
2. Client categories are fixed: existing languages offer only the possibility to
specify access rights for a fixed set of client categories, typically users and
heirs (i.e., instances using a fixed, public interface, and subclasses).
3. Access rights are not customizable: the onus is on the provider to specify
the access rights. It can be hard or impossible for a client to adapt certain
encapsulation decisions once they are fixed by the providing class.
We propose to address these problems by turning encapsulation policies into
separate entities that can be composed. We characterize our approach as follows:
– A class consists of an implementation and a number of encapsulation policies.
– An encapsulation policy is a mapping from method signatures to access
rights.
– The set of access rights may be language-specific, but will typically express
whether a method may be called, implemented or overridden.
– Encapsulation policies can be composed. One may merge available policies,
thus combining their access rights, or refine a policy to obtain a more restrictive one.
– A client can use a class through a default encapsulation policy, explicitly
select one of the available policies, or a specify a customized policy.
We claim that this simple model of composable encapsulation policies addresses the weaknesses we have identified above in a fundamental way. Encapsulation policies not only give the programmer freedom to specify multiple usage
contracts for different classes of clients, but they allow certain critical decisions
to be delayed until the client is ready to bind them. Furthermore, encapsulation policies subsume other, less general, mechanisms, such as interfaces, and
visibility mechanisms.
The contributions of this paper include:
– an analysis of the weaknesses in the encapsulation mechanisms of mainstream
OO languages,
– a proposal for a new encapsulation mechanism based on composable encapsulation policies,
– a simple formalization of this mechanism,
– a detailed discussion of how we applied this model to Smalltalk,
– an evaluation of the proposed mechanism, including a comparison with mainstream OO languages.
Composable Encapsulation Policies
3
This paper is structured as follows: In section 2 we motivate this work by
presenting a detailed analysis of the shortcomings of present encapsulation mechanisms. In section 3 we propose encapsulation policies by means of a simple,
set-theoretic model. In section 4 we present how this model can be applied to
Smalltalk based on the experiences with our prototype implementation. We then
evaluate our proposal with respect to the identified problems and provide some
discussion in section 5. We review related work in section 6, and we conclude in
section 7 with some remarks on future and ongoing work.
2
Problem Statement
In this section, we motivate our work by analyzing the limitations of the encapsulation mechanisms offered by mainstream object-oriented programming languages such as Java, C++, C#, and Eiffel.
2.1
Access Rights are Inseparable from Classes
In most object-oriented languages, the access rights to classes are tightly bound
to their implementation. In languages like Java, C++, and C#, methods may
be annotated with certain access rights by using keywords such as public, private
or protected. Since the access rights are inseparable from the methods they are
applied to, they cannot be reused independently. It is consequently impossible
to express, for example, that methods >, <, <=, etc. should be public in all classes
that implement the magnitude protocol. Instead, the programmer has to express this information on a per-method basis and duplicate it in each class that
implements these methods.
Illustration. As a concrete example, consider the two classes Collection and Path
that each implement a collection protocol which typically consists of a few dozen
methods such as add:, addAll:, remove:, do:, and select:. Path inherits from GraphicalObject and Collection inherits from Object, so they are not related by inheritance. In current languages, both of these classes must individually specify their
encapsulation attributes for these methods (i.e., which should be public, private,
or protected). It is not possible to express these attributes in a sharable way.
The situation is worse if there are subclasses of Collection and Path in which
only a subset (or a superset) of these methods should be accessible to a client,
because the programmer is again forced to specify the new access rights on a
per-method basis and duplicate this information to make it available in both
subclasses.
2.2
Client Categories are Fixed
Most object-oriented languages such as Java and C# offer a set of keywords
(i.e., private, public, and protected) that essentially allow the designer of a class
to assign encapsulation policies for just two fixed categories of clients (i.e., users
4
Nathanael Schärli, Stéphane Ducasse, Oscar Nierstrasz, and Roel Wuyts
and heirs) corresponding to two different modes of use (i.e., instantiation and
inheritance) [Sny86].
This approach restricts modularity because it does not take into account that
different clients within the same category may need to access a class in different
ways [Bra92]. By forcing the designer of a component to fix the encapsulation
policy for each category of client, one takes away the freedom of the client to
choose which mode of use is more appropriate, and one loses the ability to
distinguish between different needs of clients within such a category.
Illustration. The class Morph is the root of all the graphical objects in the
user interface framework of Squeak [IKM+ 97] and implements several hundred
methods, most of which are internal auxiliary methods. Since this framework
is designed to be extended by inheritance, Morph has many subclasses with a
variety of different needs for encapsulation.
The vast majority of subclasses, exemplified by SketchMorph, specialize how
the Morph is drawn on a graphical canvas. This means that they typically override
only a very small set of designated hook methods such as drawOn: and drawPostscriptOn:, which are then called by other methods such as fullDrawOn: and
refreshOn: that are part of the “drawing protocol” of Morph. However, there
are also other kinds of subclasses that override more than these hook methods.
The class PluggableListMorph, for example, specializes other drawing methods to
implement smooth scrolling and overrides the submorph management methods
since it uses a list as a model and therefore does not need to explicitly store
submorphs.
Several other kinds of subclasses of Morph exist, and each needs to customize
certain methods of Morph. But unfortunately, encapsulation models like the one
of Java are not expressive enough to address the needs of these different categories of subclasses. Instead, the designer of the class Morph has to declare
practically all internal methods as public or as protected, in order not to restrict the most demanding clients from specializing the functionality of Morph
according to their needs.
However, such a “one-size-fits-all” encapsulation policy is not appropriate
for the majority of subclasses that just want to customize some designated hook
methods or add some special purpose methods. This is because it forces all
these subclasses to access the class through an extremely wide and error prone
interface that unnecessarily restricts their freedom of choosing names for local
auxiliary methods and makes them unnecessarily fragile with respect to changes
in Morph (e.g., introducing a new protected method in Morph will break any
subclass that incidentally uses the same name for an own internal method).
This fragility is unnecessary because most subclasses neither need nor want
to override any of these protected methods in Morph, but because the same
encapsulation policy must be shared by all subclasses, they cannot explicitly
declare this.
Existing Solutions. Eiffel addresses this problem by allowing the designer of a
class to declare the classes that are allowed to access a certain method. However,
Composable Encapsulation Policies
5
this solution is very limited, because the designer has to take an up-front decision
on the clients that will have access. Clients that are not known when the class is
written (and are not subclasses of known clients) cannot be taken into account
and so can never have access. Furthermore, it cannot be used to discriminate
between different heirs, which means that all heirs access the class through a
completely unrestricted interface.
C++ addresses this problems with the friend construct that allows a class to
grant other functions or classes access to its internal members. Like the Eiffel
approach, this is a very limited solution because the clients have to be known
upfront. Furthermore, it is not fine-grained enough because a friend is always
allowed to access all the otherwise private methods and fields, without distinction. Similarly, private and protected inheritance do not generally solve these
problems: they allow a programmer to make either all or none of the methods
available in a subclass, but are not fine-grained enough to address the precise
needs of different subclasses.
In Java and C#, methods and fields can be defined to be accessible within the
current package and the current assembly, respectively. However, this approach
is also not flexible enough because it allows programmers to establish only one
additional category of clients, which is given by the physical organization of
classes and underlies therefore many constrictions. For example, each Java class
can only be part of exactly one package.
2.3
Access Rights are not Customizable
It is clear that it is primarily the responsibility of the designer to define how
a class should be encapsulated. But even if a language allowed the designer to
specify an arbitrary number of encapsulation policies, it would neither be possible
nor reasonable for the designer to provide a policy that precisely addresses the
individual needs of each client. Therefore, a language should allow a client to
customize the encapsulation policy according to its individual needs as long as
it does not violate the restrictions defined by the designer of the component.
This means that a client should be allowed to make the interface granted by an
encapsulation policy smaller, but not larger.
Unfortunately, current languages offer at best only limited support for such
customization. Java, for example, does not allow the client of a class to customize
the encapsulation policy specified by its designer, whereas C++ offers only very
coarse-grained and limited mechanisms (i.e., public, private, and protected inheritance). This not only prevents unanticipated reuse, it also prevents a client from
using a class through a customized encapsulation policy that minimizes the risk
of inappropriate method accesses and reduces fragility with respect to changes
in the used class.
To avoid unnecessarily fragile class hierarchies, the programmer of a subclass
should, for example, have the means to decline the override right for all methods
but the ones that effectively need to be overridden. This makes the new subclass
invulnerable to the problem of unintended name clashes that can occur when
the implementor of a superclass changes its internal implementation and adds
6
Nathanael Schärli, Stéphane Ducasse, Oscar Nierstrasz, and Roel Wuyts
some new auxiliary methods. Even if one of the new methods incidentally has a
signature that is already used in the subclass, the absence of the override access
right guarantees that the methods do not interfere.
Illustration. Consider a C++ class Point where the method == is implemented
by comparing the two coordinates x and y. Furthermore consider a method
moveTo(Point) that uses == and is implemented as follows:
void moveTo(Point other) {
if (this == other) return;
x = other.x;
y = other.y;
coordinatesChanged(); // Notify my clients
}
To allow another programmer to override the method == in a specialized
subclass such as LargeIntegerPoint, == is declared as virtual, which grants subclasses the right to override this method. But since this encapsulation policy
cannot be adapted by the subclass, it does not only allow a subclass to override
this method, but it also prevents subclasses from accessing the method in another
way; once the designer of Point has decided that this method will be dynamically
bound, heirs can no longer customize this decision and make it statically bound.
In particular, this means that it is not possible for a client to implement a
new method == without having all the calls to == in Point be bound to this new
method. As a consequence, this encapsulation decision significantly restricts the
freedom of all direct and indirect subclasses of Point because it does not allow
them to use the method == in a way that does not fully conform to the original
implementation.
It is for example not possible to implement a subclass ColoredPoint of Point
where == takes into account both the color and the coordinates without breaking moveTo and all the other methods in Point that call == and expect that it
just compares the coordinates.
Existing Solutions. Both Eiffel and C# address this problem and allow a subclass
to resolve such unintended name captures. In Eiffel this is done by allowing a
subclass to consistently rename arbitrary methods of the superclass. C# allows
the programmer to assign the keyword new (rather than override) to a method to
declare that it is used for a different concept than in the superclass and that all
calls in the superclass should therefore be statically bound to the local method.
However, these solutions are not as flexible as they should be. Eiffel only
allows the subclass to resolve unintended name captures that are apparent when
the subclass is written, but it does not allow the subclass to protect itself from
unintended name clashes that may occur later, for instance when the superclass
is modified and new methods are added. This is because only existing superclass
methods can be renamed in a subclass.
In C#, this limitation is avoided because the keywords new and override can
also be used for methods that do not (yet) have a corresponding method in
Composable Encapsulation Policies
7
the superclass3 . However, the C# approach suffers from the same limitations
as described in section 2.1, which means that the only way to protect internal
methods from such unintended name clashes is to explicitly assign the keyword
new to the implementation of each of these methods. It is not possible for a
programmer to declare a reusable policy that declines the override right for
all but the methods where this right is effectively needed and then share this
policy among a family of subclasses that need to override the same superclass
methods but may use different internal methods that should all be protected
from unintended name clashes that can arise when the superclass is modified.
Another limitation is that these solutions only allow a subclass to decline
the right to override a method, but they do not allow any client to decline any
access right that is granted by an encapsulation policy. Thus, it is for instance
not possible for a subclass to declare that certain internal superclass methods
cannot be called because they are inappropriate in a specific usage scenario.
3
Encapsulation Policies
In this section we present a new model for specifying encapsulation policies
for object-oriented programming languages. We use a simple, set-theoretic approach to describe the model in a language-independent way. For concreteness,
we use the terminology of class-based languages with inheritance and instantiation as the only two modes of use for a class. Note however that the concept of
composable encapsulation policies is very general and could also be applied to
prototype-based languages as well as languages that support non-standard composition mechanisms such as automated delegation [VRB00] or trait composition
[SDNB03].
3.1
Design Rationale and Overview
As we have seen in section 2, most of the weaknesses in present encapsulation
mechanisms arise from the fact that encapsulation policies are inseparable from
the implementation. We propose to tackle this problem essentially by introducing
encapsulation policies as separate entities, which can be individually selected,
composed and applied. We apply the following principles:
– An encapsulation policy expresses how a client can access the methods of
a class, independent of the particular mode of use (i.e., instantiation or
subclassing).
– The designer can associate an arbitrary number of encapsulation policies to a
class. Each policy represents a set of encapsulation decisions that correspond
to a certain usage scenario.
– The client can independently decide which encapsulation policy to apply and
in which way the class will be used. The chosen policy may be one that is
provided by the class, or one that is stricter than a provided one.
3
This causes a compiler warning but not an error.
8
Nathanael Schärli, Stéphane Ducasse, Oscar Nierstrasz, and Roel Wuyts
Note that we only consider methods in the encapsulation policy, since we
assume that instance variables are never accessible from the outside of an object.
3.2
Modelling Encapsulation Policies
We now present a simple model of encapsulation policies.
An encapsulation policy P : S 7→ 2A is a mapping from method signatures to
potentially empty sets of access attributes. P represents a contract between the
class and its client. This means that a client accessing a class through P may
only access a method m with signature s according to the set of attributes P (s).
A signature s ∈ S identifies a method provided by the class. This may simply
represent a method name, for dynamically typed languages like Smalltalk, or
might include type information for statically typed languages with overloading,
like Java and C++.
The access attributes represent the policy in effect that constrains how clients
may use the method. The actual set of available access attributes may depend
on the particular programming language. For the purpose of illustration, we
will consider three kinds of access attributes, namely c, r and o, which specify, respectively, that the associated method may be called, reimplemented or
overridden.
A word of explanation may be in order. We draw an important distinction
between reimplementing and overriding a method in a subclass. If a subclass
overrides a method, this means that all existing calls to this method in the
superclass are dynamically bound to the overriding method. If a subclass does
not override but only reimplements a method, existing calls in the superclass
continue to be statically bound to the old version of the method. In Java, for
example, a subclass may reimplement a method that has been declared as private
in its superclass, but one cannot override it — the new method is not visible
from the context of the superclass and all the calls remain statically bound to
the local version of the method. By contrast, a subclass can neither reimplement
nor override a method that is declared as final in its superclass.
Access attributes express rights that are conceptually orthogonal. We consider
the access rights c, r and o to be orthogonal since each can logically occur
in isolation independently of the others, whether or not all combinations are
sensible or desirable. In most programming languages, only certain combinations
may make sense, or might be expressible using the mechanisms available. For
example, protected in Java corresponds to the rights {c, o} — a heir may call
protected methods and may override them, but may not simply reimplement
them.
3.3
Composing Encapsulation Policies
We now define operators and relations over encapsulation policies that enable
us to compose them and express constraints on their composition.
Suppose that P and Q are arbitrary encapsulation policies and s is an arbitrary method signature. Then we define the following:
Composable Encapsulation Policies
9
– The policy P + Q is the merge of P and Q:
def
(P + Q)(s) = P (s) ∪ Q(s)
– The policy P ∗ Q is the intersection of P and Q:
def
(P ∗ Q)(s) = P (s) ∩ Q(s)
– The policy P − Q is the reduction of P by Q:
def
(P − Q)(s) = P (s) − Q(s)
– For a set of selectors S ⊆ S, the policy P |S is the restriction of P to S:
P (s) if s ∈ S
def
(P |S)(s) =
∅
otherwise
– P is stricter than Q if all rights granted by P are also granted by Q:
def
P ≤ Q ⇔ P (s) ⊆ Q(s), ∀s ∈ S
– P [a] is the set of method signatures for which right a ∈ A is granted:
def
P [a] = {s ∈ S | a ∈ P (s)}
– The policy P \ A is the result of removing the access rights A ⊆ A from P :
def
(P \ A)(s) = P (s) − A
3.4
Encapsulation Constraints
In class-based languages, clients use classes via two kinds of operations: inheritance and instantiation. With our approach, both of these operations are parameterized with an encapsulation policy that imposes certain constraints on
the client.
Inheritance. Consider a chain of subclasses C0 , C1 , . . . , Cn where C0 is the root
of the class hierarchy, Cn is a concrete class, and the class Ci is defined as the
subclass of Ci−1 using the encapsulation policy Pi , for all i ∈ {1, . . . , n}. For
any class C, the term pol (C) denotes the set of encapsulation policies offered by
C, and meth(C) denotes the set of methods implemented in C. Furthermore, we
use sig(C) to denote the signatures of the methods in meth(C), we use self(C)
to denote the set of signatures that are sent to self in any of the methods in
meth(C), and we use super(C) to denote the set of signatures that are sent to
super in any of the methods in meth(C).
For the concrete class Cn to be valid, the following encapsulation constraints
must be fulfilled for all k ∈ {1, . . . , n}.
10
Nathanael Schärli, Stéphane Ducasse, Oscar Nierstrasz, and Roel Wuyts
∃Q ∈ polS(Ck−1 ) : Pk ≤ Q
(1)
Pk [r] ∪ Pk [o]
(2)
sig(Ck ) ∩ Si<k sig(Ci ) ⊆ S
self(Ck ) ∩ i<k sig(Ci ) ⊆ n≥i≥k sig(Ci ) ∪ P [c] (3)
super(Ck ) ⊆ P
(4)
P[c]
(5)
∀Q ∈ pol (Ck ) : Q|(S − sig(Ck )) ≤ Q′ ∈pol(Ck−1 ) Q′
The first constraint guarantees that the policy Pk through which the client
Ck uses the class Ck−1 can only grant access rights that are also granted by a
certain encapsulation policy Q offered by Ck−1 . The second constraint makes
sure that the class Ck only implements methods with signatures that are not
defined in any of its superclasses, are allowed to be reimplemented by Pk , or
are allowed to be overridden by Pk . The third constraint ensures that there are
only self-sends to methods that are inherited from Pk−1 if they are declared as
callable by the policy P . Note that self-sends to methods implemented in Ck or
one of its subclasses are always allowed, even if they have the same signature
as a method implemented in a superclass of Ck . The fourth constraint ensures
that there are only super-sends to methods that are declared as callable by the
policy Pk .
Finally, the fifth constraint guarantees that for all the method signatures that
are not implemented in Ck , the encapsulation policies pol (Ck ) offered by Ck can
only grant access rights that are also granted by at least one of the policies
pol (Ck−1 ) of the superclass Ck−1 . This is important because it guarantees that
the encapsulation restrictions that the designer of the class Ck−1 assigned to its
methods cannot be bypassed in indirect subclasses. Note that the subclass Ck
is free to grant arbitrary access rights for all the methods meth(Ck ) that are
implemented locally.
Note that these constraints do not prevent a subclass Ck from offering its
clients a policy that grants more rights on the methods obtained from Ck−1 than
the policy Pk , through which the class Ck inherits from Ck−1 . This is important
because it allows a class Ck to access a class Ck−1 through a minimal policy Pk
without preventing its future clients from accessing the methods obtained from
Ck−1 through a policy that grants more rights. Also note that these constraints
do not guarantee that the class Ck is correct (i.e., that there are no calls to
methods that are not available) nor are they concerned with issues related to
subtyping (see section 5.4). Instead, they only ensure that the encapsulation
restrictions are not violated.
Instantiation. Instantiation is also parameterized with an encapsulation policy,
which means that each new instance o of the concrete class Cn is created through
an encapsulation policy P . For such an instantiation and subsequent calls on o
to the selector s to be valid, the following encapsulation constraints must be
fulfilled.
∃Q ∈ pol (Cn ) : P ≤ Q (1)
s ∈ P [c] (2)
Composable Encapsulation Policies
MyCollection
add:
do:
remove:
11
PCollection
appendOnly
PAccess
at:
at:put:
collection
PAppend
add:
addAll:
PRemove
remove:
removeAll:
all
appendOnly
internalAdd:
internalAt:
collection
Transaction
Protocol
transactions
PEnumerate
do:
collect:
collection
appendOnly
MySet
Legend
MySet
PCollection
collection
class offers an encapsulation policy
named collection
Fig. 1. Encapsulation policies at work
The first constraint is the same as for inheritance and it says that the policy P
can only grant access rights that are also granted by a certain policy Q offered
by Cn . The second constraint says that the only method signatures that are
allowed to be called on the instance o are the ones that are marked as callable
in P .
3.5
Example
In the example shown in figure 1, the class MyCollection offers three different
encapsulation policies that are available under the names appendOnly, collection
and all. Note that each of these three policies is composed from several stricter
policies, some of which are shared.
The class MyCollection has two clients. One client is the subclass MySet, which
inherits from MyCollection through the encapsulation policy collection and in turn
offers the same policy as well as the policy appendOnly to its clients. The other
client is the class TransactionProtocol, which uses an instance of MyCollection
through the encapsulation policy appendOnly to store its transactions. To be
usable, the class TransactionProtocol must also offer at least one encapsulation
policy, but this is not shown in the figure.
12
4
Nathanael Schärli, Stéphane Ducasse, Oscar Nierstrasz, and Roel Wuyts
Encapsulation Policies in Smalltalk
In the previous section we have introduced the model of encapsulation policies
in a language independent way. Now we show how this model can be applied
to Smalltalk. This section is based on our proof of concept implementation in
the Smalltalk dialect Squeak [IKM+ 97]. However, we will present the examples
in a somewhat simplified syntax to ease the reading of the paper, especially for
readers who are not familiar with Smalltalk. In particular, we use bold face for
symbols rather than the prefix #.
4.1
Representing Encapsulation Policies
Following the Smalltalk tradition of making everything an object, encapsulation
policies are instances of the class Policy. Each policy object can contain some local
definitions, which are represented as a dictionary of associations from method
selectors (i.e., symbols) to access attributes, and can refer to other encapsulation
policies which it is composed from.
Creating Encapsulation Policies. In the previous section, we have pointed out
that the actual set of access attributes may depend on the programming language. Since Smalltalk is dynamically typed and inheritance is often used for
sharing implementation in unanticipated ways, we believe that it does not make
much sense to declare a method that cannot be reimplemented in a subclass.
Therefore, we define that a method can always be reimplemented, and consequently, our Smalltalk encapsulation policies only manage the access attributes
callable (c) and overridable (o).
For convenience, we provide a literal way of creating encapsulation policies.
This is done by putting the selectors between brackets [] and prefixing them
with either ↑ or ↓ to indicate the associated access rights. The meaning of such
a literal policy is defined as follows:
–
–
–
–
No prefix means that the selector is fully accessible ({c, o})
The prefix ↑ means that the selector is callable but not overridable ({c})
The prefix ↓ means that the selector overridable but not callable ({o})
All selectors that do not appear in the policy definition are neither callable
nor overridable ({})
As an example, the expression [foo ↑bar ↓check] returns a policy that allows
full access to the selector foo, allows the selector bar to be called but not overridden, and allows the selector check to be overridden but not called. All the
other selectors are neither allowed to be called nor overridden. All selectors are
allowed to be reimplemented.
Manipulating Encapsulation Policies. Since encapsulation policies are first-class
objects, they can be manipulated by messages. If p and q are arbitrary encapsulation policies, the most common messages and their semantics are as follows:
Composable Encapsulation Policies
13
– + is the merge operator, which means that the expression p + q returns a
new policy that grants all the access rights granted by either p or q.
– * is the intersection operator, which means that the expression p * q returns
a new policy that grants only the access rights that are granted by both P
and Q.
– - is the reduction operator, which means that the expression p - q returns
a new policy that grants the access rights granted by p without the rights
granted by q.
– The expression p noOverride returns a new policy that is the same as p except
that no selector can be overridden:
p noOverride ≡ p − {o}
4.2
Associating Encapsulation Policies with Classes
A key feature of the model is that a programmer can associate an arbitrary
number of encapsulation policies with a class. In our Smalltalk implementation,
this is done by sending the message policyAt:put: to a class. This message takes
a symbol and a policy as an argument and then associates the policy with the
class under the identifier represented by the symbol. All the identifiers associated
with encapsulation policies are local to the class, and they allow a client to refer
to a certain encapsulation policy that is offered by the class. As an example, we
can define a collection class OrderedCollection as follows:
(Collection subclass: OrderedCollection)
instanceVariableNames: ’array offset’;
policyAt: basicUse put: [add: addAll: removeAt: ...];
policyAt: basicExtend put: basicUse + [growBy: compact ...]
This creates the class OrderedCollection as a subclass of Collection, defines
the two instance variables array and offset, and associates two encapsulation
policies with the identifiers basicUse and basicExtend. Note that it is possible
to define a policy that refers to another policy associated with the class by using
its identifier in the policy definition. In our example, the expression basicUse +
[growBy: compact ...] refers to the policy basicUse and merges it with the policy
[growBy: compact ...].
The order of the policyAt:put: messages is only relevant as far as later messages override policies that have been bound to the same identifier by earlier
messages. However, the order is not relevant for the meaning of the policy definition (i.e., the second argument). This is because references to other policies are
not evaluated when the expression is executed. Instead, these references remain
part of the policy definition, which means that the relationship between the different policies remains valid even if one of the involved policies gets modified.
Note that circular references in policy definitions are not allowed and result in
an error.
14
4.3
Nathanael Schärli, Stéphane Ducasse, Oscar Nierstrasz, and Roel Wuyts
Using Encapsulation Policies
When creating a subclass or an instance of class, the programmer can select
which of the encapsulation policies offered by the class should be applied. This
is done by passing the symbol selecting the encapsulation policy as an additional
argument to the message that creates the new subclass or instance. The following
code illustrates how the message newWithPolicy: is used to create a new instance
of the class OrderedCollection using the encapsulation policy basicUse:
Morph>>initialize
super initialize.
submorphs := OrderedCollection newWithPolicy: basicUse.
...
As a consequence, the ordered collection submorphs responds only to the
messages that are declared as callable by the policy basicUse in the class OrderedCollection. Sending any other messages leads to a runtime error.
Default Policies. To improve ease of use without sacrificing the flexibility of
having multiple encapsulation policies, our implementation features the concept
of default policies. The designer of a class can specify two default policies for a
class by associating ordinary encapsulation policies with the identifiers basicUse
and basicExtend. When a client uses this class and does not explicitly specify
another policy, these default policies are then used for creating new instances
and subclasses, respectively. This means that in the previous example, we could
have used the simpler expression OrderedCollection new to create an instance
that implicitly uses the default policy basicUse.
4.4
Sharing Encapsulation Policies
Although it is possible to define anonymous policies using the [] notation, it is
often more appropriate to declare named policies and then share them among
different classes. Because of the lack of namespace facilities in Squeak, we store
policies in the same namespace as classes and just use the convention that we
prefix policy names with the letter P.
In the following example, we first define encapsulation policies named PEnumeration, PAppend, and PRemove. Then we merge these policies to define a policy
named PCollection, which is then shared between the classes Collection and Path
described in section 2.1.
Policy named: PEnumeration
is: [do: select: detect: collect: reject: ...].
Policy named: PAppend
is: [add: addAll: ...].
Policy named: PRemove
is: [remove: removeAll: ...].
Policy named: PCollection
is: PEnumeration + PAppend + PRemove.
Composable Encapsulation Policies
15
(Object subclass: Collection)
instanceVariableNames: ”;
policyAt: basicUse put: PCollection
(GraphicalObject subclass: Path)
instanceVariableNames: ’points’;
policyAt: basicUse put: PCollection + [draw drawOn: length segmentCount ...]
Note that neither classes Path nor Collection specify a policy to access their
respective superclass, which means that the default policy basicExtend is applied.
Special Policies. The policy PProtoObject is defined so that it allows all the
methods that are implemented in the class ProtoObject to be called. To guarantee
that every object responds at least to this common set of system messages such
as == and isNil, this policy is implicitly added to any policy that is assigned
to a class (e.g., using the message policyAt:put:). This means that the policy of
the class Collection in the above example is in fact equivalent to PCollection +
PProtoObject.
Traditionally, all methods in Smalltalk are public, and therefore many Smalltalk programmers enjoy the freedom of not having to deal with encapsulation
decisions if they do not want to. We support this style of programming with a
policy PAll, which allows full access to any valid method selector. As a consequence, it is possible to make a class fully accessible from the outside by simply
associating the policy PAll to the default identifiers basicUse and basicExtend.
4.5
Encapsulation Policies in Subclasses
In section 3.4, we have formally defined the constraint that applies to encapsulation policies offered by subclasses (constraint 5). In our implementation, we
ensure this by implicitly restricting each policy that is assigned to a class (e.g.,
using the message policyAt:put:) so that it does not grant any access right for
inherited methods that are not also granted by the union of the policies offered
by the superclass.
To allow a programmer to create subclasses that have less encapsulation
policies than their superclass, policies that are assigned to a class are not automatically available in subclasses. However, a programmer can “inherit” the
policies offered by a superclass by sending the message addSuperPolicies to the
newly created class. Note that these inherited policies are overridden by equivalently named policies that are explicitly assigned to the class using the message
policyAt:put:.
Furthermore, a programmer can use the keyword super to refer to the superclass policy from within the definition of a policy that is assigned to the subclass
using the message policyAt:put:. Note that super always refers to the superclass
policy with the name of the newly added policy (i.e., the first argument to the
message policyAt:put:).
16
Nathanael Schärli, Stéphane Ducasse, Oscar Nierstrasz, and Roel Wuyts
As an example, consider the class Morph and its subclass SketchMorph described in section 2.2. Since SketchMorph needs to override only the two drawing
methods, it uses its superclass through the encapsulation policy drawingHooks.
However, it still offers all the encapsulation policies specified by Morph so that
it does not unnecessarily restrict its clients. This is done by using the message
addSuperPolicies. In addition, SketchMorph overrides the inherited policy drawingHooks so that it also contains the method selector drawPDF:.
(Object subclass: Morph)
instanceVariableNames: ’submorphs owner color bounds’;
policyAt: basicUse put: PMorph;
policyAt: basicExtend put: PAll;
policyAt: drawingHooks put: PAll noOverride + [drawOn: drawPostscriptOn:];
policyAt: symbsubmorphManagement put: PAll noOverride + [addFront: addBack: ...]
(Morph subclass: SketchMorph withPolicy: drawingHooks)
instanceVariableNames: ’form’;
addSuperPolicies;
policyAt: drawingHooks put: super + [drawPDF:]
4.6
Customizing Encapsulation Policies
An important feature of encapsulation policies is that the client is not only
allowed to select a policy offered by a class but can also customize the selected
policy according to its needs. In our implementation, this is done by sending the
messages -, *, and noOverride to the symbol corresponding the selected policy.
As an example, assume that the class Point described in section 2.3 is defined as
follows:
(Object subclass: Point)
instanceVariableNames: ’x y’;
policyAt: basicUse put: [x y moveTo: radius degrees dotProduct: = ...];
policyAt: basicExtend put: PAll
Although both encapsulation policies offered by this class declare the method
= as overridable, we can still define a subclass ColoredPoint where implementing
the method = does not override the superclass method. We do this by first
selecting the policy basicUse and then customizing it so that it allows the method
= to be called but not to be overridden.
((Point subclass: ColoredPoint withPolicy: basicUse - [↓=])
instanceVariableNames: ’rgb’;
addSuperPolicies
Note that in our implementation, the programmer of the subclass decides
whether the method = should override or simply reimplement the superclass
method by means of specifying the encapsulation policy: the method = implemented in the subclass overrides the superclass method if and only if the encapsulation policy allows it. This means that the encapsulation policy not only
specifies whether a method can override the superclass version but also whether
it will override the superclass version.
Composable Encapsulation Policies
5
17
Evaluation and Discussion
In section 2 we identified a set of limitations that are caused by the encapsulation models of state of the art object-oriented programming languages. In the
following, we present a point-by-point evaluation of how composable encapsulation policies solve these problems in a simple and elegant way. Furthermore,
we briefly discuss additional constraints for defining encapsulation policies in
subclasses and compare encapsulation policies to Java interfaces.
5.1
Access Rights are Inseparable from Classes
Encapsulation policies are separate and independent from the implementation
of a class. This allows us to express encapsulation policies in a reusable way and
share them between arbitrary classes. Since these policies are not only sharable
but also composable, it is possible to define new policies by combining, modifying
and extending existing policies.
This significantly raises the level of abstraction because a programmer does
not have to explicitly associate encapsulation attributes with the implementation
of every single method. Furthermore, it reduces implementation and maintenance
overhead because encapsulation decisions do not have to be duplicated in the
first place and are therefore much easier to adapt if necessary.
Illustration. Let us reconsider the example of section 2.1. When we implement
the classes Collection and Path with this approach, we do not have to assign
any encapsulation attributes to the implementation of their methods. Instead,
we can create a named encapsulation policy PCollection that contains all the
accessible collection methods as well as the corresponding access rights and then
use it as an encapsulation policy for both Collection and Path. In our Smalltalk
implementation, this could be done as shown in section 4.4.
Besides the fact that we do not have to duplicate the encapsulation decisions for the collection protocol, this example illustrates also other advantages
of encapsulation policies:
– Since encapsulation policies specify only accessible methods, the classes Path
and Collection can use different internal method names (e.g., internalAt: vs.
basicAt: and unsafeAdd: vs. privateAdd:) and still use the same encapsulation
policy. Furthermore, a programmer can add, remove or rename such internal
methods in either class without having to change the encapsulation policy.
– The encapsulation policy PCollection can also be shared if a certain method,
for example removeAll:, should only be accessible for clients of Collection but
not of Path. This can be done by using the policy PCollection - [removeAll:]
in Path.
– The encapsulation policy PCollection can be used in any class that provides
the collection protocol. This means that independent of how such a class
is implemented, the programmer does not have to deal with encapsulation
on a per-method level and can instead just reuse the policy PCollection.
18
Nathanael Schärli, Stéphane Ducasse, Oscar Nierstrasz, and Roel Wuyts
This stands in contrast to the interface-based approaches of languages like
Java and C#, which do not support reuse of encapsulation decisions even
if multiple classes implement the same interface (see section 5.5 and the
discussion of C#’s CAS in section 6).
5.2
Client Categories are Fixed
We avoid this problem by allowing the designer to specify an arbitrary number
of independent encapsulation policies for a given class. This allows a designer to
implement a class with multiple usage scenarios in mind and to explicitly specify
and document this by giving each of these scenarios a descriptive name and
assigning it to an encapsulation policy. Another programmer can immediately
see which usage scenarios a given class supports and then select the encapsulation
policy corresponding to the usage scenario that is most appropriate.
In contrast to existing approaches, the designer can specify these encapsulation policies in a way that is independent of a particular mode of use. This raises
the level of abstraction because it allows the programmer to think in a conceptual rather than an operational way. It is based on the realization that as long
as it is not possible to sidestep such a conceptual policy, it is not relevant for the
designer of a class whether a client uses the class by inheritance, instantiation,
or any other mode of use such as automated delegation.
The fact that a particular encapsulation policy can be applied for both inheritance and instantiation gives a client the freedom to choose the mode of use
that is most appropriate for its needs. In particular, it avoids those situations
where a programmer is forced to inherit from a class just because this is the
only way to obtain certain access rights, even if this is from a design point of
view not appropriate (e.g., when Stack inherits from OrderedCollection just to
be able to access some internal methods of the collection) or not possible (e.g.,
in a language that does not offer multiple inheritance).
Illustration. With our approach the limitation of a fixed set of categories can
be avoided by associating different encapsulation policies with the class Morph.
In our Smalltalk implementation, the class Morph and its subclass SketchMorph
could be defined as shown in section section 4.5.
Another scenario in which it is useful to be able to assign multiple encapsulation policies to a class is when changes to the implementation of a class should
be accessible for new clients without breaking existing ones. As an example, suppose that a vendor of a graphics framework ships a class GraphicalObject that is
extensively subclassed by its customers. At a later point, the vendor would like
to add the capability of alpha-blending to this class and therefore needs to add
a few more internal methods such as transformAlpha:.
With traditional encapsulation approaches, these methods would be declared
as protected since it should be possible to override them in new subclasses.
However, doing this can break existing subclasses [SLMD96] because they may
have introduced the same method name for another purpose! In our model, this
dilemma can be solved by leaving the existing encapsulation policies as they are
Composable Encapsulation Policies
19
and instead assigning the class a new encapsulation policy (e.g., under the name
withAlphaBlending) that contains these new methods. As a consequence, the new
methods are completely invisible to all the existing subclasses that use the class
through an old policy, whereas they are available for new clients that want to
take advantage of them.
5.3
Access Rights are not Customizable
We allow a client not only to select the most appropriate reuse policy, but also
to customize this policy so that it best matches its individual needs as long as
it does not violate the restrictions defined by the designer of the class.
This allows one to reuse a class in a way that may not have been anticipated
by the designer. Furthermore, it allows a client to specify a customized encapsulation policy that contains only the access rights that are effectively necessary,
and it therefore minimizes the interdependencies between the class and its client.
Minimizing these interdependencies is important because each access right that
is granted by a policy comes together with a risk (e.g., to accidentally and inappropriately call or override a method), restricts the freedom of the client (e.g.,
in Java, a subclass cannot use the name of a protected superclass method for
a method that represents a different concept), and makes the code more fragile
with respect to changes in the used class (e.g., unintended name clashes that
can occur when a new internal method is added to the used class).
Illustration. In section 4.6, we have already shown how customizing encapsulation policies allow a programmer to solve the problem introduced in section 2.3,
even if the designer of the class Point did not anticipate a client such as ColoredPoint that needs to implement a method == that is not compatible to the
original implementation.
As another illustration, consider the class Morph introduced in section 2.2
and assume that its designer only provided the encapsulation policy PAll, which
exposes the complete interface to its clients. Now suppose that another programmer would like to make a subclass TurtleMorph that overrides the method
drawOn: and implements the turtle-specific methods go: and pointNorth using
the methods position: and rotate:. To minimize the interdependencies to Morph,
the programmer of TurtleMorph can still access Morph through a minimal policy
that grants only the rights that are needed. This is done by using the intersection
of the policy basicExtend and the policy that only allows the selectors position:
and rotate: to be called and gives full access to the selector drawOn:.
((Morph subclass: TurtleMorph withPolicy: basicExtend * [↑position: ↑rotate: drawOn:])
instanceVariables: ”;
...
5.4
Constraints for Encapsulation Policies in Subclasses
In section 3.4, we formally stated the constraints that encapsulation policies
impose on the clients of a class, and we have pointed out that these constraints
20
Nathanael Schärli, Stéphane Ducasse, Oscar Nierstrasz, and Roel Wuyts
guarantee only that the encapsulation restrictions expressed by the designer of
a class can never be violated in a direct or indirect client.
This has the advantage that a subclass can always assign fewer access rights
to inherited methods, which is very useful in the dynamically typed language
Smalltalk where implementation inheritance is a common practice [Tai96] and
every method is traditionally fully accessible. For instance, it allows a programmer to create a class that can only be accessed through a restricted encapsulation
policy even if the superclass (which may have been designed by another programmer) declares all the methods as fully accessible (e.g., by using the policy PAll).
However, the price for this expressiveness is that it sacrifices substitutability
of subclasses for superclasses. This means that if a superclass C offers a policy
under the name p that grants full access rights to the method signature s, it
may be that the policy that is offered by the subclass D under the same name
does not grant any access rights for s. In fact, it may even be that the subclass
D does not even provide a policy under the name p!
In a language like Java where subclassing implies subtyping, it may therefore
be more appropriate to introduce additional constraints for the encapsulation
policies offered by a subclass. For example, we could define that encapsulation
policies are “inherited” and that a subclass cannot make these inherited policies
stricter. This guarantees substitutability of subclasses for superclasses because
every method signature that can be accessed through a policy named p in a class
C can also be accessed through the policy p in all its subclasses.
Note that this additional constraint does not affect the ability to freely choose
and customize a policy when creating a subclass nor does it prevent the designer
of a subclass from offering additional policies (i.e., policies that were not inherited from the superclass) that do not underly this constraint. Therefore, the
programmer still enjoys all the conceptual benefits of encapsulation policies that
are described in this paper.
5.5
Comparison to Java Interfaces
At a first glance, our notion of encapsulation policies resembles Java interfaces as
they both specify a set of callable method signatures. However, aside from this
structural resemblance, they are used for quite different purposes. Whereas the
primary purpose of encapsulation policies is to express a usage contract between
a component and its client, the purpose of Java interfaces is to declare subtype
relationships in a way that is independent from subclassing.
As a consequence, Java interfaces are neither designed nor able to capture
the encapsulation aspects of a class and separate them from the implementation.
Instead, all the access attributes (e.g., public, private, and protected) of methods
are still declared together with their implementation. This means that regarding
encapsulation, the information provided by interfaces is redundant because the
definition of the methods in the class already defines all the encapsulation-related
information.
Nevertheless, it may seem that Java interfaces offer a way for clients to reuse
a class through different encapsulation policies by simply creating an instance
Composable Encapsulation Policies
21
of the class and then using type casts to restrict access to this instance to an
interface that is associated with the class. However, this sort of “policy” is not
comparable to the one expressed with our approach because of the following
limitations:
1. Policies cannot be enforced. Even if a client reuses a class C through an
instance that has been type casted to the interface I, there is no way to
enforce that this instance is not accessed through the complete interface
defined by C. This is because it is always possible to use a downcast to
convert the instance back to the type C.
2. Policies can only be defined for one category of clients. The policies defined by Java interfaces are only available to instances but not to subclasses.
This means that all subclasses always have to reuse the class through the
unchangeable policy that is defined by the access attributes in the implementation of the class.
3. Policies cannot be defined independently. The policies defined by Java interfaces are interdependent with the encapsulation decisions specified within
the implementation of the class. This is because an interface can only be
applied to a class that declares all the method specified by the interface as
public, but this in turn makes it impossible to provide another policy that
does not allow full access to these methods.
Realizing that Java interfaces are not expressive enough to be used as encapsulation policies, the other interesting question is whether encapsulation policies
are expressive enough to be used as types, i.e., whether encapsulation policies
subsume interfaces. Even though we have neither formalized nor implemented
a type system based on encapsulation policies, we strongly believe that this is
possible.
Our belief that this is possible stems from the fact that encapsulation policies
contain a superset of the information expressed by interfaces. In fact, both types
of entities define a set of method signatures that are allowed to be called. The
only difference is that encapsulation policies also capture all the other encapsulation aspects such as which of these methods are allowed to be overridden
and reimplemented. Also the relationship of encapsulation policies and classes is
quite similar to the relationship of interfaces and classes. In fact, a programmer
can associate several possibly nested encapsulation policies to a class to express
that the class conforms to the “interface” that is expressed by such a policy.
6
Related Work
We have already discussed the encapsulation mechanisms of the languages Java,
C++, C#, and Eiffel in section 2. In this Section, we briefly discuss encapsulation
mechanisms of other languages as well as some related research.
The encapsulation model of CLOS and Dylan follow the tradition of Lispbased object-oriented languages such as Flavors (with the notable exception
of CommonObjects [Sny86]). There is no direct access to slots as in Java or
22
Nathanael Schärli, Stéphane Ducasse, Oscar Nierstrasz, and Roel Wuyts
Smalltalk. The access is always performed via accessors that can be generated
automatically from the class description. However, it is always possible to access
a slot value using the function slot-value4 . There is no encapsulation of methods,
which means that they are all public and late-bound.
Ada [Ame83] uses packages as a module system. These packages have a separated definition and body. Besides importing and exporting definitions, Ada
9X [Taf93] also allows sharing of packages. This is used for constructing hierarchical libraries, and solves the problem of private types only providing coarse
control of visibility and the inability to extend packages without recompiling
them. Hierarchical libraries are built by adding child packages to existing packages. The child packages can add definitions to their parent package and can see
the internal body of their parent. Child packages can be made private, which
means that they are only visible within the subtree of the hierarchy whose root
is its parent. Moreover, within that tree, a private child package is not visible
to the specifications of any non-private sibling (although it is visible to their
bodies).
Ada also has protected types that are used for concurrent tasks. Protected
types consist of a specification, where the access protocol is specified, and a
body, where the implementation details are provided. Protected types contain a
notion of visibility (clients can only use the procedures as defined in the access
protocol), but they also control the access to the data these procedures work
with: calls from clients to subprograms within a protected body are mutually
exclusive.
Modula-3 is a statically typed, object-oriented language with single inheritance, and modules that consist of separate interface and implementation files.
Modula-3 sports partial revelation [Fre95], which is a technique that allows inheriting from a class without making all its features visible. This is done by
dividing class definitions across multiple files, each of them specifying a partial
type for the class. Partial revelation allows a program unit to import only the
relevant aspects of a class by selecting the corresponding type; the other aspects
of the class are still available and can be revealed elsewhere in the program.
Similar to encapsulation policies, this addresses the problem that different kinds
of subclasses need to access a class in different ways.
However, there are many conceptual differences between the two approaches.
For example, the Modula-3 approach does not model different access attributes:
a feature is either visible (i.e., fully accessible) or hidden; there is no mechanism for a more fine-grained distinction between allowing a feature to be called,
overridden, and reimplemented. Furthermore, the Modula-3 types are not composable: it is only possible to use partial and full revelations to define a new
type that reveals more features of an object type. Also, it is not possible for a
subclass to customize the partial types that are available for a class.
4
In CLOS, slot-value is in fact calling slot-value-using-class, which is an entry point of
the MOP that allows controlling of slot accesses. Therefore it is possible to define a
different encapsulation mechanism than the default one.
Composable Encapsulation Policies
23
The object-oriented programming language Beta [MMPN93] is a block-scoped
language where the visibility is given by the nesting of the blocks. There are no
explicit visibility attributes that can be granted beyond that. However, Beta
allows one to declare virtual patterns that can be extended in subpatterns. Furthermore, virtual patterns can be finalized (i.e., made non-virtual) in a subpattern. For programming in the large, Beta also has a hierarchical module system
to declare interface modules and implementation modules. The module system
allows different implementation modules to be associated with the same interface
module (so-called variants). Module visibility is also controlled by nesting.
C# and other .NET languages allow one to place CAS attributes on methods
of interfaces and thereby reuse the constraint attributes across all classes implementing these interfaces. However, the security constraints that are expressed by
CAS attributes and then checked at runtime by the .NET CLR are conceptually
different from the encapsulation constraints that are expressed by our approach
and C#’s access modifiers.
Wolczko also argues that existing class-based languages do not provide sufficient support for encapsulation [Wol92]. This is addressed by a Smalltalk-based
research language called MUST, which offers additional features such as two
types of self-sends and super-sends. This allows a programmer to express additional encapsulation issues in a very fine-grained way, by extending the language
with additional mechanisms. In contrast, our approach is of a conceptual and
language independent nature: it does not specify exactly what encapsulation issues (i.e., access attributes) should be modelled, but it suggests to separate these
encapsulation issues from the implementation and to make them first class.
The Jigsaw modularity framework, developed by Bracha in his doctoral dissertation [Bra92], defines module composition operators hide, show, and freeze
to control how attributes of a module are encapsulated. Whereas hide eliminates
the argument attributes from the interface of a module, show eliminates everything but the argument attributes from the interface of a module. The operator
freeze allows one to control how attributes of a module are bound. It takes an
attribute as an argument and produces a new module in which all references to
the argument attribute are statically bound.
Altogether, these operators give a programmer fine-grained control over how
a module should be encapsulated. Similar to our approach, they also allow the
client of a module to decline access rights by hiding or statically binding attributes. However, the Jigsaw framework cannot capture such encapsulation decisions as separate, reusable entities, associate them to modules and apply them
when a module is used.
Caesar’s collaboration interfaces extend the concept of interfaces to include
the declaration of expected methods, i.e., the methods that a class must provide when bound to an interface [MO02]. However, they do not address the
encapsulation problems that are addressed in this paper.
Sadeh and Ducasse present the introduction of dynamic interfaces in Smalltalk
[SD02]. These interfaces represent a list of message selectors which are causally
connected to the class that implements them. The system can be dynamically
24
Nathanael Schärli, Stéphane Ducasse, Oscar Nierstrasz, and Roel Wuyts
queried to get the classes implementing a given interface. Dynamic interfaces can
be derived from other interfaces or included in other interfaces. As Smalltalk is
dynamically typed, dynamic interfaces mainly serve as documentation purpose.
Contrary to encapsulation policies, dynamic interfaces do not deal with encapsulation aspects.
To avoid the fragile base class problem [MS98], researchers developed better
ways to describe the contract between a class and its subclasses. Lamping proposes a limited specialization interface that expresses the calling relationships
between the methods in the superclass [Lam93]. Reuse Contracts [SLMD96]
bring the idea a step further by proposing a model in which the operations of
class evolution are analyzed in the context of the calling dependencies in the superclass. Hence the evolution problems are categorized and detected with finer
precision.
7
Conclusion and Future Work
In this paper we have proposed composable encapsulation policies as a way to
improve the flexibility and expressiveness of object-oriented programming languages and to reduce the fragility of the resulting programs. Explicit encapsulation policies enable the expression of different usage scenarios for different
classes of clients, they enable reuse of policies, and they enable client-specific
customizations in a straightforward way.
We have outlined a general, language-independent model of encapsulation
policies, and we have described a proof-of-concept prototype for Smalltalk that
demonstrates the feasibility of the idea. Encapsulation policies can be incorporated into a language in such a way that there is an additional syntactic burden
only when one wishes to make use of the feature. In other cases, default policies
mimic the conventional approach offered by the language.
We are working on extending our proof-of-concept prototype to a full implementation of encapsulation policies in Smalltalk as well as Smalltalk with Traits.
This will serve as the basis for a more detailed evaluation of the advantages of
our approach in languages that feature non-standard composition mechanisms
such as trait composition or automated delegation. Furthermore, we plan to
investigate the impact of replacing the traditional encapsulation mechanisms of
languages like Java with encapsulation policies. In particular, it seems that there
could be interesting synergies if the notion of encapsulation policies would also
be used as a type and could hence replace the notion of interfaces.
Acknowledgments
We gratefully acknowledge the financial support of the Swiss National Science
Foundation for the projects “Tools and Techniques for Decomposing and Composing Software” (SNF Project No. 2000-067855.02, Oct. 2002 - Sept. 2004)
and “RECAST: Evolution of Object-Oriented Applications” (SNF Project No.
620-066077, Sept. 2002 - Aug. 2006).
Composable Encapsulation Policies
25
References
[Ame83]
American National Standards Institute, Inc. The Programming Language
Ada Reference Manual, volume 155 of LNCS. Springer-Verlag, 1983.
[Bra92]
Gilad Bracha. The Programming Language Jigsaw: Mixins, Modularity and
Multiple Inheritance. Ph.D. thesis, Dept. of Computer Science, University
of Utah, March 1992.
[Fre95]
Steve Freeman. Partial revelation and Modula-3. Dr. Dobb’s Journal,
20(10):36–42, October 1995.
[IKM+ 97] Dan Ingalls, Ted Kaehler, John Maloney, Scott Wallace, and Alan Kay.
Back to the future: The story of Squeak, A practical Smalltalk written in
itself. In Proceedings OOPSLA ’97, pages 318–326. ACM Press, November
1997.
[Lam93]
John Lamping. Typing the specialization interface. In Proceedings OOPSLA ’93, ACM SIGPLAN Notices, volume 28, pages 201–214, October
1993.
[MMPN93] Ole Lehrmann Madsen, Birger Moller-Pedersen, and Kristen Nygaard.
Object-Oriented Programming in the Beta Programming Language. Addison Wesley, Reading, Mass., 1993.
[MO02]
Mira Mezini and Klaus Ostermann. Integrating independent components
with on-demand remodularization. In Proceedings OOPSLA 2002, pages
52–67, November 2002.
[MS98]
Leonid Mikhajlov and Emil Sekerinski. A study of the fragile base class
problem. In Proceedings of ECOOP’98, number 1445 in Lecture Notes in
Computer Science, pages 355–383, 1998.
[Nie89]
Oscar Nierstrasz. A survey of object-oriented concepts. In W. Kim and
F. Lochovsky, editors, Object-Oriented Concepts, Databases and Applications, pages 3–21. ACM Press and Addison Wesley, Reading, Mass., 1989.
[SD02]
Benny Sadeh and Stéphane Ducasse.
Adding dynamic interface to
Smalltalk. Journal of Object Technology, 1(1), 2002.
[SDNB03] Nathanael Schärli, Stéphane Ducasse, Oscar Nierstrasz, and Andrew Black.
Traits: Composable units of behavior. In Proceedings ECOOP 2003, volume
2743 of LNCS, pages 248–274. Springer Verlag, July 2003.
[SLMD96] Patrick Steyaert, Carine Lucas, Kim Mens, and Theo D’Hondt. Reuse
contracts: Managing the evolution of reusable assets. In Proceedings of
OOPSLA ’96 Conference, pages 268–285. ACM Press, 1996.
[Sny86]
Alan Snyder. Encapsulation and inheritance in object-oriented programming languages. In Proceedings OOPSLA ’86, ACM SIGPLAN Notices,
volume 21, pages 38–45, November 1986.
[Taf93]
S. Tucker Taft. Ada 9x: From abstraction-oriented to object-oriented. In
Proceedings OOPSLA ’93, volume 28, pages 127–143, October 1993.
[Tai96]
Antero Taivalsaari. On the notion of inheritance. ACM Computing Surveys,
28(3):438–479, September 1996.
[VRB00] John Viega, Paul Reynolds, and Reimer Behrends. Automating delegation
in class-based languages. In Proceedings of TOOLS 34’00, pages 171–182,
July 2000.
[Wol92]
Mario Wolczko. Encapsulation, delegation and inheritance in objectoriented languages. IEEE Software Engineering Journal, 7(2):95–102,
March 1992.