to allow reuse without subtyping; to avoid bloated class interfaces; to publicize the interface between a class and its superclass
Not infrequently, inheritance is used just for the sake of
reusing existing implementation. However, in Java inheritance is
linked to subtyping: a subclass not only inherits the member of
its superclass, but also its type is a subtype of that of the
superclass. This lets a Stack
inheriting from Vector
be used as if it were a Vector
,
although it is not one. Also, inheritance bloats the interface of
Stack
with many
methods from Vector
,
even though a Stack
has no use for them. In these cases, it is better to let a Stack
hold a Vector
as delegatee,
to which it can delegate the responsibility for delivering the
services it offers. The following piece of Java code gives an
example.
class
Client {
Stack s = new
Stack();
s.push(
);
if (s.size()
}
public class
Stack
extends
Vector {
public
Stack() {}
public
Object push(Object
item) {
addElement(item);
return
item;
}
}
After the refactoring, the code for Stack
looks as
follows:
public class
Stack {
protected
Vector delegatee;
public
Stack() {
delegatee = new
Vector();
}
public
Object push(Object
item) {
delegatee.addElement(item);
return
item;
}
public
int
size() {
return
delegatee.size();
}
}
The code of Vector
and all clients
of Stack
can
remain unchanged. The following figure gives an impression of the
before and the after of the refactoring.
After installation, the refactoring is ready for use. Simply open the context menu on a class (not a compilation unit) in the Package Explorer of an Eclipse workspace or in the Outline view of a compilation unit, and select Replace Inheritance With Delegation.
The refactoring then offers you a choice between forwarding and delegation (see below for an explanation) if possible, or names the reasons why one or the other refactoring (or both) cannot be performed (violated preconditions; also see below).
After having made your choice (and as is standard in Eclipse), you can have a preview or have the refactoring performed immediately.
The refactoring is actually more complicated than made believe
by the above simple example. One of the biggest problems is
presence of open recursion (or hook methods; cf. the Template
Method pattern): if a method of the superclass calls
late-bound methods on this
,
removal of inheritance means that methods of the refactored class
can no longer be called this way. This however changes semantics
of the refactored program, which is unacceptable for a
refactoring.
In the context of prototype-based
programming, a clear distinction is made between forwarding
and delegation:
under forwarding, this
always points to the receiver of the method, while under
delegation, this
always points to the delegator. The following figure illustrates
this:
In prototype-based languages (which do not posses classes), delegation plays the role of inheritance. However, class-based languages like Java support only forwarding between objects; delegation applies only to classes and cannot be utilized in object composition.
There are two ways out of this problem: either refuse to perform the refactoring, or introduce reverse delegation (should be: reverse forwarding), i.e., delegation from the (object of the) (former) superclass to the (object of the) (former) subclass to make the openly recursive calls possible. The former would make absence of open recursion a precondition, the latter either requires changes to the superclass (not desired) or subclassing by a dummy class that serves as delegatee for the class to be refactored. We opt for the latter, but hide the subclass in the inner of the class to be refactored. The exact procedure requires more than sketched here (particularly because of the possible presence of subclasses of the class to be refactored); the following figure only gives a rough impression of the result.
Absence of open recursion is one precondition of replacing inheritance with forwarding; there are several others. We list them here without explanation; some of them are checked using our type inference algorithm, which is also the basis of our Infer Type refactoring and several other tools. Note that Class refers to the class to be refactored, and Superclass to its direct superclass.
Object
.super
must be accessible from Class after subclassing
is removed.Throwable
.If the preconditions are satisfied, the refactoring guarantees the following postconditions. Note that Delegator refers to the class to be refactored and Delegatee to the former Superclass or its new subclass (in case of delegation).
this
.)super
(including those to super
in constructors) are replaced by calls on Delegatee;
in case of delegation, those for which reverse
delegations exist are replaced by calls to methods
forwarding to super
in Delegatee instead (see Postcondition 7).super
.Object
or another superclass of the refactored class (the
refactored class may receive a new superclass in certain
cases), namely if the constraints for the type of the
object being cast cannot be derived. For example, if an
instance of Sub
is stored in a container typed Object
whose
source code is unavailable, and if that instance is later
retrieved and cast to the type of Super
, a class
cast exception will occur once the subtyping of Sub
from Super
is
removed. Of course, this could be prevented by adding a
corresponding precondition, but given how many harmless
cases this would exclude (and given that unguarded
downcasts are a problem anyway), absence of this
precondition is a design trade-off rather than a bug.