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:
d.methodNotInBaseClasses()
that calls functions from the base classesd.methodFirstDefinedInA()
that can call the same method from instances of C, B and AA a = new D()
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:
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 House
should not inherit from Basement
, Stairway
and Attic
to provide the required features.
Instead a house (House
) is usually build (composed) of a Basement
, Stairway
and an Attic
- 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:
class A {
public void playVideo(Uri videoSource) {}
}
class B {
protected void playMusic(Uri musicSource) {}
}
class C {
public playWebResource(Url webResource) {}
}
We could now create a MediaPlayer
type that extends A
, B
and C
. 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.playMusic
available, we must extend B
:
class NewB extends B {
public void playMusicSource(Uri musicSource) {
playMusic(musicSource);
}
}
Now compose the new type that is now extended with the functionality of A
, B
and C
:
class ComposedABC {
private A a;
private NewB b;
private C c;
public ComposedABC() {
this.a = new A();
this.b = new NewB();
this.c = new C();
}
// Example of using the extended functionality
public PlayMedia(MediaType mediaType, Uri mediaSource) {
switch (mediaType) {
case Video: this.a.playVideo(mediaSource); break;
case Music: this.b.playMusicSource(mediaSource); break;
case Web: this.c.playWebResource(mediaSource); break;
}
}
}
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:
class ComposedType {
private SomeType someType;
public ExecuteSomeTypeFunctionality() {
// Delegate method call
this.someType.SomeTypeFunctionality();
}
}