Search code examples
oopdesign-patternspolymorphismaggregationdecoupling

Switching long inheritance tree for composition / agreggation


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?


Solution

  • 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:

    • ensure the required functionality is available via public API.
    • if not, you must extend the type to expose the required functionality. Then use the new subclass to compose your type.
    • create the instances that the composed type requires in constructor (Composition) or request them as constructor parameters (Aggregation). Store those instances in private fields (i.e. hide them from the public API).

    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();
      }
    }