I have a program in Java which uses a lot of inheritance, and it's making it troublesome to modify and test the final derived classes
So I'm refactoring, attempting to switch from inheritance to aggregation / composition. The design in question is similar to A isInheritedBy -> B isInheritedBy -> C isInheritedBy -> D
My problems are:
- Calling functions from derived classes instances that the base classes don't have, while still having access to the base classes functions. There is often a need to call things like
d.methodNotInBaseClasses()that calls functions from the base classes - Calling overridden functions through the derived class instances, so calling
d.methodFirstDefinedInA()that can call the same method from instances of C, B and A - I need to maintain polymorphic behavior to minimize the required changes to the rest of the code. Currently there is a lot of
A a = new D() - I need to have loose coupling between base and derived classes
The best design I've thought of for class creation is:
Assume that classes A,B,C and D exist, where B to D are coupled to the previous class by constructor dependency injection
class Builder {
public A createInstanceOfA(){
return new A();
}
public B createInstanceOfB(){
return new B(createInstanceOfA());
}
public C createInstanceOfC(){
return new C(createInstanceOfB());
}
public D createInstanceOfD(){
return new D(createInstanceOfC());
}
}
This seems fine for me. However for polymorphic behavior behavior from base classes I'm defining interfaces that each derived class implements for its base classes:
interface InterfaceForA {
void doSomething();
}
interface InterfaceForB extends InterfaceForA {
void doMore();
}
interface InterfaceForC extends InterfaceForB {
void doEvenMore();
}
interface InterfaceForD extends InterfaceForC {
void doTheMost();
}
And now you can see the problems of duplicated functions:
Sidenote: I'm aware @Override is not required for interface implementations, I added those for clarity
class A implements InterfaceForA {
public void doSomething(){
System.out.println("A did something");
}
}
class B implements InterfaceForB {
private A a;
B(A a){
this.a = a;
}
@Override
public void doSomething(){
a.doSomething();
}
public void doMore(){
a.doSomething();
System.out.println("B did more");
}
}
class C implements InterfaceForC {
private B b;
C(B b){
this.b = b;
}
@Override
public void doSomething(){
b.doSomething();
}
@Override
public void doMore(){
b.doMore();
}
public void doEvenMore(){
b.doMore();
System.out.println("C did even more");
}
}
class D implements InterfaceForD {
private C c;
D(C c){
this.c = c;
}
@Override
public void doSomething(){
c.doSomething();
System.out.println("D did something");
}
@Override
public void doMore(){
c.doMore();
}
@Override
public void doEvenMore(){
c.doEvenMore();
}
public void doTheMost(){
c.doEvenMore();
System.out.println("D did the most");
}
}
All for being able to do these:
class App {
public static void main (String[] args) {
Builder builder = new Builder();
D d = builder.createInstanceOfD();
// Allows functions the base classes don't have, yet have access to the base classes functions
d.doTheMost();
System.out.println("---");
// Allows calling overriden functions of the base classes directly
d.doSomething();
System.out.println("---");
// Allows calling overriden function from base classes interfaces
InterfaceForA a = d;
a.doSomething();
}
}
If this already seems too complex, consider also that the entire dependency tree consists of about 90 classes, with the deepest ones maybe with 6-8 base classes
I also tried the Strategy pattern, with dependencies inverted like A dependsOn InterfaceForB, B dependsOn InterfaceForC, C dependsOn InterfaceForD and D having no dependencies, but I couldn't wrap my head around it. I had to make Builder more complicated, I could only call A methods from D by making D depend on A, and to either:
- Keep references to the instance of each base class and call each instance function separatedly. The client code from the 13 lines in main() grew to 28
- Use getters in base clases to get the interface to their strategy. The client code grew from 13 to 20, and the entire code grew from 129 to 148
Thoughts?
It looks like your understanding of Composition is wrong. Unless you want to implement something like the Facade pattern you don't want to mimic all the types that a type is composed of.
Just rewind the reel: the general idea is to reuse functionality that some type has already implemented.
One solution is to use inheritance. Inheritance has some drawbacks (e.g. limited extensibility/testability or the fact that multi-inheritance is usually not supported by OO languages - for good reasons).
A second solution is to implement interfaces. The drawback is that you have to reimplement every functionality. Whle this solution enables polymorphism it does not help much when the goal is to reuse existing features. A third and most famous solution is to use Composition/Aggregation in order to get access to the functionality of those other types.
Composition is also perceived as the more natural class design that helps to avoid design mistakes. For example a
Houseshould not inherit fromBasement,StairwayandAtticto provide the required features.Instead a house (
House) is usually build (composed) of aBasement,Stairwayand anAttic- that enables a more intuitive class design and usage.This example makes it also clear why the composed type should not implement all the interfaces that define the owned types (like in your original example): because a house is not a basement. A house is not a stairway. And a house is not an attic. A house is a complex entity composed of many components. Some of those components are even aggregated and can be reused once the house is destroyed.
The difference:
When using inheritance the subclass owns the inherited members. It can change the inherited behavior by overriding the virtual members.
When using Composition/Aggregation the class does not own any members and can't modify their behavior. Instead it owns a reference to the type that implements the required functionality and the access is restricted to the public class API (opposed to protected members when using inheritance).
To convert inheritance to composition is very simple:
Example:
We could now create a
MediaPlayertype that extendsA,BandC. Because we want to improve our class design and therefore avoid a terrible inheritance tree and other design flaws we decide to use Composition.Now to make the protected
B.playMusicavailable, we must extendB:Now compose the new type that is now extended with the functionality of
A,BandC:The key is that the internal types that the composition type is composed of are usually inaccessible for the public (private). The client of the composed type's API must not now what types are internally used to provide a functionality (information hiding). If you need to expose the functionality directly, always implement a public member and use delegation: