The Replace Inheritance with Delegation Refactoring (RIWD)

Purpose

to allow reuse without subtyping; to avoid bloated class interfaces; to publicize the interface between a class and its superclass

System requirements

Rationale

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.

How to use it

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.

What it does, and why

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.

Forwarding vs. delegation

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.

Preconditions

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.

  1. Superclass must not be Object.
  2. Superclass must not be abstract.
  3. The default constructor of Superclass and the constructors called via super must be accessible from Class after subclassing is removed.
  4. No type constraint requiring that Class be a subtype of Superclass must be derivable from the program. (Usually, this is satisfied if no assignment, type cast, or type test from the type of Class or its subtypes to the type of Superclass exists in the program; yet there are other type constraints derived from generic types that have equal effect
  5. For forwarding, there must be no instances of open recursion in Superclass (see above).
  6. Clients of Class including its subclasses must not access fields inherited by Class.
  7. For forwarding, Class or its subclasses must not require access to protected members inherited from Superclass, if not located in the same package.
  8. Class must not be a subclass of Throwable.
  9. Synchronizing method calls must not be split among Class and Superclass. Since such a check is difficult to perform, we require that there is no synchronization performed on instances of Class.

Postconditions

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).

  1. Delegator does not extend Delgegatee or Superclass.
  2. Delegator has a new protected field that holds an instance of Delegatee. (This field is declared final if no constructor of Delegator calls another one via this.)
  3. Delegator contains delegating methods for all methods called by its clients or subclasses that were formerly inherited from Superclass (and therefore not implemented in Class).
  4. In case of forwarding, all calls on 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).
  5. Delegator extends former indirect superclass if corresponding type constraint has been derived from the program.
  6. Delegator implements all interfaces for which corresponding type constraints have been derived from the program.
  7. In case of delegation, Delegator has a new inner subclass of Superclass that contains the necessary reverse delegations, widening of protected members accessed via the delegatee field of Delegator, and the methods forwarding to super.
  8. All other classes and types of the program have not changed. In particular, no other class accesses the delegatee field, which is therefore completely hidden.

Limitations

Known issues

Bugs

Installation

Publications

Team