javaoopinheritancedesign-patternsclean-architecture

How to properly reuse heavy calculations performed in the Parent class within the Child class?


Suppose I have a Parent class in Java:

public class Parent {
    protected int value;

    public Parent() {
        value = performHeavyCalculations();
    }

    private int performHeavyCalculations() {...}
}

Also I have a Child class:

public class Child extends Parent {
    private int newValue;
    
    public Child() {
        super();
        newValue = value + performAnotherHeavyCalculations();
    }
    
    private int performAnotherHeavyCalculations() {...}
}

Before creating a Child class I have a requirement to create Parent class. How can I properly design my program to not repeat heavy calculations in Parent class while constructing the Child class?


Solution

  • Why would creating a parent class ever be a requirement in itself? This is a design decision, and not part of the problem to solve. I would avoid it, since parent-child relationships are very rigid and tend to cause tight coupling, making re-use harder.

    A more flexible approach is to model the heavy calculations as services which can be injected where needed. The rule of thumb is to favor composition over inheritance.

    interface Calculator {     
        int calculate();
    }
    
    class HeavyCalculator implements Calculator {
        @Override
        public int calculate() { 
            // here is the code from performHeavyCalculations
        }
    }
    
    class AnotherHeavyCalculator implements Calculator {
        @Override
        public int calculate() { 
            // here is the code from performAnotherHeavyCalculations
        }
    }
    
    public class Application {
    
        private final Calculator heavyCalculator;
        private final Calculator anotherHeavyCalculator;
        
        // dependency injection via the constructor
        public Application(Calculator heavyCalculator, Calculator anotherHeavyCalculator) {
            // just initialize, don't do the work in the constructor
            this.heavyCalculator = heavyCalculator;
            this.anotherHeavyCalculator = anotherHeavyCalculator;
        }
    
        // Here the work is done. Keeping this concern separate from 
        // instance construction
        public int doWork() {
            return heavyCalculator.calculate() + anotherHeavyCalculator.calculate()
        }
    }
    

    So now you need to first create an instance, and then call doWork:

    // here the dependencies are injected manually
    // Frameworks like Spring Boot can do this for you
    Application app = new Application(new HeavyCalculator(), new AnotherHeavyCalculator());
    int value = app.doWork();
    

    Only store the value in a field if you need it in that class later on. Otherwise it might be cleaner to just return the value to the calling code as shown. But this depends on your actual use case.

    The above design is more flexible, promotes re-use and makes the code easier to test than the proposed parent-child approach.

    It is easier to test because the three classes can be unit-tested independently, since they don't know about each other's internals. For example in the code you showed, the protected value field in the Parent is directly referenced by the Child, making the code tightly coupled and harder to test separately. My proposed design uses an abstraction (the Calculator interface) to separate the concerns of the classes more clearly. Now it's easy to write unit tests for all three, and you can mock the calculators when testing the Application class:

    public ApplicationTest {
    
        @Mock
        private Calculator calculator1;
        @Mock
        private Calculator calculator2;
    
        private Application testSubject;
    
        @BeforeEach
        public void setUp() {
            testSubject = new Application(calculator1, calculator2);
        }
    
        @Test
        public void testHappyFlow() {
            when(calculator1.calculate()).thenReturn(13);
            when(calculator2.calculate()).thenReturn(29);
    
            // CALL
            int actual = testSubject.doWork();
    
            assertEquals(42, actual);
        }
    }
    
    public HeavyCalculatorTest {
      
        private HeavyCalculator testSubject;
    
        @BeforeEach
        public void setUp() {
            testSubject = new HeavyCalculator();
        }
    
        @Test
        public void testHappyFlow() {
    
            // CALL
            int actual = testSubject.calculate();
    
            // assuming the heavy calculator should always return 5
            // probably it's more complicated than that in real life
            assertEquals(5, actual);
        }
    }
    

    Now if you re-use any of these classes anywhere else, they are already unit tested and have a clear contract (the Calculator interface). Maybe it is not even necessary to look inside the source code of these classes at that point. This is a big advantage.