Concise guidelines for flattening inheritance
Follow several concise guidelines to flatten inheritance.
- For each class in the type hierarchy, rename the class from
[Name]
to[Name]Impl
. - Extract an interface from each of these classes, giving each the original names used for those classes. Include all public methods and constant static fields in each interface.
- Build a type hierarchy through the extracted interfaces that is identical to the original set of
classes. For example, if
BImpl
extendsAImpl
, thenB
extendsA
. - Copy methods and fields from each parent class to their immediate children. Repeat recursively throughout the type hierarchy.
- Resolve naming conflicts for overridden methods or shadowed fields by renaming methods and fields that were copied from the parent class.
- Replace calls to
super.method()
by directly calling the methods that were folded into the flattened class. - Resolve errors from copied super constructors similarly to steps 5 and 6.
- Remove type extensions from the implementation classes (that is, if
BImpl
extendsAImpl
, removeextends AImpl
) to eliminate inheritance. - (Optional) Delete dead code, abstract classes, or any class or method that is no longer referenced.
As a simple example of flattening inheritance, consider the following class hierarchy.
public class Bird {
private String description;
public void setDecription(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
public class Bluejay extends Bird {
public String getName() {
return "Bluejay";
}
}
public class Cardinal extends Bird {
public String getName() {
return "Cardinal";
}
}
By applying these guidelines for flattening inheritance, you yield this set of interfaces:
public interface Bird {
public void setDecription(String description);
public String getDescription();
}
public interface Bluejay extends Bird {
public String getName();
}
public interface Cardinal extends Bird {
public String getName();
}
In addition, you yield an implementation class for each of these interfaces:
public class BirdImpl implements Bird {
private String description;
public void setDecription(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
public class BluejayImpl implements Bluejay {
private String description;
public void setDecription(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
public String getName() {
return "Bluejay";
}
}
public class CardinalImpl implements Cardinal {
private String description;
public void setDecription(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
public String getName() {
return "Cardinal";
}
}
As a result, the original type hierarchy is maintained and inheritance is eliminated. This architecture allows the code generation for the refactored classes to be distributed across partitions.
As an alternative to copying methods and fields from the parent class, flattening can still be
achieved through delegation by changing the relationship of the classes from extension to
containment. For example, the BluejayImpl
and CardinalImpl
classes
would be changed to have an instance of BirdImpl
and then would delegate calls to
its methods.
public class BluejayImpl implements Bluejay {
private Bird bird = new BirdImpl();
public void setDecription(String description) {
bird.setDecription(description);
}
public String getDescription() {
return bird.getDescription();
}
public String getName() {
return "Bluejay";
}
}
public class CardinalImpl implements Cardinal {
private Bird bird = new BirdImpl();
public void setDecription(String description) {
bird.setDecription(description);
}
public String getDescription() {
return bird.getDescription();
}
public String getName() {
return "Cardinal";
}
}