How to force derived class to call super class method at multiple layers?
Asked Answered
M

4

12

I am trying to find the most elegant way to allow a child and parent to react to an event initiated by the grandparent. Here's a naive solution to this:

abstract class A {
    final public void foo() {
        // Some stuff here
        onFoo();
    }

    protected abstract void onFoo();
}

abstract class B extends A {
    @Override
    final protected void onFoo() {
        // More stuff here
        onOnFoo();
    }

    protected abstract void onOnFoo();
}

class C extends B {
    @Override
    protected void onOnFoo() {
        // Even more stuff here
    }
}

So basically, I'm trying to find the best way to allow all related classes to perform some logic when foo() is called. For stability and simplicity purposes I prefer if it is all done in order, although it's not a requirement.

One other solution I found involves storing all the event handlers as some form of Runnable:

abstract class A {
    private ArrayList<Runnable> fooHandlers = new ArrayList<>();

    final public void foo() {
        // Some stuff here
        for(Runnable handler : fooHandlers) handler.run();
    }

    final protected void addFooHandler(Runnable handler) {
        fooHandlers.add(handler);
    }
}

abstract class B extends A {
    public B() {
        addFooHandler(this::onFoo);
    }

    private void onFoo() {
        // Stuff
    }
}

class C extends B {
    public C() {
        addFooHandler(this::onFoo);
    }

    private void onFoo() {
        // More stuff
    }
}

This method is certainly preferable to the first. However I am still curious if there is a better option.

Mariellamarielle answered 1/8, 2019 at 0:22 Comment(6)
The alternate approach is to make A#foo() final and provide an override hook that it calls. (This is precisely what should have been done in Android.)Holms
oops, it's actually supposed to be final already. The child class will see it has been called by overriding A#onFoo() instead.Mariellamarielle
Why not having onFoo() in all 3 classes in the hierarchy and chain the method call using super.onFoo() from C and B. Note that even though foo() is defined in A the concrete instance exists for C only and in that case onFoo() execution will start from C, then go to B and then A. There is no need to make onFoo() abstract in A. If A and B can be concrete classes then the scenario would be different.Metamorphose
By the way, the accumulative steps sounds like this is a misdesign; it might not be, but perhaps the whole thing is better off as a Chain of Responsibility or Composite.Holms
Calling super.onFoo() in a chain (or probably get rid of it and just use super.foo()) would work, however it would require that someone looks at the documentation or they may find an error without an exception. It's for sure the cleanest way to do it, but it seems like it may cause some invisible errors without some way to check if the parent functions have been called. Also, in my case A and B cannot be concrete classes.Mariellamarielle
@chrylis-cautiouslyoptimistic- inheritance hierarchy deeper than 1 level is not that rare and certainly not something bad per se. As the example given in the OP is intentionally abstract, it is impossible to say whether a composite or chain would be better. There certainly are situations where inheritance of more than 1 level is the right choice and since java does not have a built-in mechanism to ensure super invocation, accumulating handlers is IMO quite an elegant way to ensure no logic gets lost in action.Seedling
L
0

What about this by calling the super method?

class A {
  void foo() {
    System.out.println("Some stuff here");
  }
}

class B extends A {
  @Override
  void foo() {
    super.foo();
    System.out.println("More stuff here");
  }
}

class C extends B {
  @Override
  void foo() {
    super.foo();
    System.out.println("Even more stuff here");
  }
}
Luettaluevano answered 2/8, 2019 at 22:21 Comment(0)
K
0

Have you considered the Template Method pattern? It works well to define a high level method that delegates to derived types to fill-in the gaps.

Karbala answered 18/5, 2020 at 8:14 Comment(2)
While that pattern may be useful for operations that can be split into well-defined substeps that child classes can provide different implementations for, the problem as originally stated seems to involve child classes performing additional tasks (likely things entirely unknown to the superclass) in addition to whatever the superclass is doing. As such, there are likely no predetermined substeps from which to define a template method (without also leaking implementation-specific details from the child classes).Donny
@Smallhacker Doesn't the event raised by the base class serve as a well-defined 'substep'?Karbala
S
0

I really like the idea of accumulating handlers from the OP!
My approach so far was to define a public inner class with a private constructor in my given base class and then use it as a return type of the method that was supposed to be overridden in subclasses:

public class Base {

    public static class SuperEnforcer {
        private SuperEnforcer() {}
    }

    @Nonnull
    protected SuperEnforcer methodToOverrideInSubclasses() {
        // do Base stuff here...
        return new SuperEnforcer();
    }
}

This way regardless how many levels of inheritance there are, each subsequent subclass must call super.methodToOverrideInSubclasses() to obtain an instance of SuperEnforcer to return it at the end. This also gives subclasses a freedom to decide at which point of their implementation to call super.methodToOverrideInSubclasses() (with "handler" approach this can be also achieved by providing addHeadFooHandler(fooHandler) and addTailFooHandler(fooHandler)).
Unfortunately, it is still possible to return null, but this can now be statically checked with @Nonnull.

Seedling answered 22/8, 2023 at 12:56 Comment(0)
H
0

Inspired by the Decorator pattern.

The advantage is that

  1. Every child class must call super class's constructor and decide which class I'm going to decorate.
  2. Every child class must override afterFoo() method, because it's abstract.
  3. Every child class only need to focuse on the same method which is afterFoo(), not afterAfterFoo(), afterAfterAfterFoo(), ...
class A {
    protected A() {}
    public void foo() {
        // Some stuff here
    }
}

abstract class ADecorator extends A {
    protected final A a;
    public ADecorator(A a) {
        this.a = a;
    }
    @Override
    public final void foo() {
        a.foo();
        afterFoo();
    }

    protected abstract void afterFoo();
}

class B extends ADecorator {
    protected B() {
        super(new A());
    }
    @Override
    protected void afterFoo() {
        // More stuff here
    }
}

class C extends ADecorator {
    public C() {
        super(new B());
    }
    @Override
    protected void afterFoo() {
        // Even more stuff here
    }
}
Homorganic answered 6/12, 2023 at 11:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.