lundi 24 juillet 2023

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

interface InterfaceForC {
    void doEvenMore();
}

interface InterfaceForD {
    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, InterfaceForA {
    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, InterfaceForB, InterfaceForA {
    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, InterfaceForC, InterfaceForB, InterfaceForA {
    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?

Aucun commentaire:

Enregistrer un commentaire