Code-Duplikation in Klassenhierarchie vermeiden

Angenommen, ich habe eine Klassenhierarchie

public class A {
    public void actionA1(){ ... }
    public void actionA2(){ ... }
} 

public class B extends A {
    public void actionB1(){ ... }
    public void actionB2(){ ... }
} 

public class C extends B {
    public void actionC1(){ ... }
} 

Angenommen, ich will nun alle Aktionen ausführen, die direkt in der Klasse (und nicht in Elternklassen) definiert sind. Nichts leichter als das, ich schreibe eine Methode actions(), die z.B. in Klasse B die Methoden actionB1() und actionB2() ausführt.

Wenn ich jetzt noch zusätzlich will, dass ich alle definierten Aktionen, auch die der Elternklassen, ausführe, schreibe ich eine analoge Methode allActions(), mit dem einzigen Unterschied, dass diese auch noch einen Aufruf an super.allActions() enthält (außer natürlich in A).

Jetzt ist mir ums Verrecken kein Weg eingefallen, die Code-Verdopplung in den beiden Methoden zu vermeiden, da ich z.B. nicht actions() in allActions() aufrufen kann, da ja Java (in C+±Sprech) nur virtuelle Methoden hat. Sehe ich das richtig, dass es hierfür keine elegante Lösung gibt, oder stehe ich nur auf dem Schlauch?

1 „Gefällt mir“

Ich gehe mal davon aus, dass Reflection nicht zur Debatte steht?

Für mich stellen sich die Frage, ob Vererbung überhaupt das richtige Konstrukt oder wäre oder eine Komposition hier nicht besser passt?

Entscheidet der Anwender auf dem selben Typ (C) ob er die einzelnen Methoden oder alle aufrufen will?

Kennt der anwender an der stelle, wo dr Aufruf statt finden soll überhaupt alle Klassen? Also: muss die allActions() Methode überhaupt extra in A und B existieren?

Oder kann man das dem Kontext entsprechend hinter einem Interface verstecken?

1 „Gefällt mir“

Würde ich gern vermeiden.

Der Anwender soll die Wahl haben, ob er alle Methoden oder nur die Methoden der aktuellen Klasse aufrufen soll.

Ich würde actions() and allActions() möglichst in ein Interface rausfaktorieren wollen. Aber beim Brainstorming will ich natürlich nicht suggestiv Dinge vorzeichnen, die eventuell einer Lösung entgegenstehen.

Pfuh, hat einen Moment gedauert, bis ich überhaupt wußte, was du meinst (mag auch an der Uhrzeit liegen). Aber erstmal: So direkt geht das nicht (was dir vermutlich klar ist).

Hab’ dann ein bißchen rumprobiert, und bei dem hier…

public class AvoidDuplication
{
    public static void main(String[] args)
    {
        C c = new C();
        c.allActions();
    }
}

class A
{
    public void actionA1()
    {
        System.out.println("A1");
    }

    public void actionA2()
    {
        System.out.println("A2");

    }

    void actions()
    {
        actionA1();
        actionA2();
    }

    void allActions()
    {
        actions();
    }
}

class B extends A
{
    public void actionB1()
    {
        System.out.println("B1");

    }

    public void actionB2()
    {
        System.out.println("B1");
    }

    @Override
    void actions()
    {
        actionB1();
        actionB2();
    }

    @Override
    void allActions()
    {
        super.allActions();
        super.actions();
    }

}

class C extends B
{
    public void actionC1()
    {
        System.out.println("C1");
    }

    @Override
    void actions()
    {
        actionC1();
    }

    @Override
    void allActions()
    {
        super.allActions();
        super.actions();
    }
}

musste ich dann selbst erstmal im debugger schauen, wie denn die Ausgabe

C1
A1
A2
B1
B1

zustande kommt :thinking: (Wär’ eigentlich was für’s Java-Quiz :smiley: )

Ich dachte dann noch, dass man den Spieß umdrehen und die unerwünschte virtuelle Vererbung für sich arbeiten lassen und die allActions nur in A definieren könnte…:

import java.util.stream.Stream;

public class AvoidDuplication
{
    public static void main(String[] args)
    {
        System.out.println("\nA:");
        A a = new A();
        a.allActions();
        
        System.out.println("\nB:");
        B b = new B();
        b.allActions();

        System.out.println("\nC:");
        C c = new C();
        c.allActions();
        
    }
}

class A
{
    public void actionA1()
    {
        System.out.println("A1");
    }

    public void actionA2()
    {
        System.out.println("A2");
    }
    
    Stream>Runnable> collect()
    {
        return Stream.of(this::actionA1, this::actionA2);
    }

    void actions()
    {
        actionA1();
        actionA2();
    }

    void allActions()
    {
        collect().forEach(r -> r.run());
    }
}

class B extends A
{
    public void actionB1()
    {
        System.out.println("B1");
    }

    public void actionB2()
    {
        System.out.println("B1");
    }

    @Override
    Stream>Runnable> collect()
    {
        return Stream.concat(super.collect(), 
            Stream.of(this::actionB1, this::actionB2));
    }
    
    @Override
    void actions()
    {
        actionB1();
        actionB2();
    }
}

class C extends B
{
    public void actionC1()
    {
        System.out.println("C1");
    }

    @Override
    Stream>Runnable> collect()
    {
        return Stream.concat(super.collect(), 
            Stream.of(this::actionC1));
    }
    
    @Override
    void actions()
    {
        actionC1();
    }
}

Aber wirklich „schön“ ist das auch nicht, und versteckt eigentlich die Duplikation nur in ein paar kryptischen Streams…

Hm. Sonst. Spontan keine Idee.

Wie ist denn das mit der Reihenfolge? Sollen von C erst die eigenen Aktionen und dann alle Aktionen der erweiterten Klassen aufgerufen werden oder umgekehrt, oder ist das egal?

@Marco13 Bei deinem Code sieht Klasse B falsch aus, sie würde sie nur zweimal die Aktionen von A ausführen. Ist das ein Typo?

@Falk Reihenfolge ist mir egal

Ich habe mich inzwischen entschlossen, bei meiner “richtigen” Implementierung nur die “alles”-Variante anzubieten, weil das die nützlichere ist, und alles andere wahrscheinlich mehr verwirrt als hilft. Trotzdem finde ich das Problem an sich interessant, weil es so einfach erscheint, aber keine offensichtliche Lösung in Java hat.

Kein „Typo“ sondern schlicht falcsh (also nicht „falsch“, weil das wäre ja richtig :smiley: )

Auch wenn ich davon ausgehe, dass du einen Grund hast, das über Vererbung abzubilden, finde ich auch, dass es aussieht, als könnte man das anders geschickter machen. Im speziellen könnte man (ggf. gestelzt auf „Separation Of Concerns“ verweisend) das Bereitstellen und das Sammeln der Aktionen als getrennte Dinge ansehen. Aber solche Worte wie ActionProvider (oder RunnableProvider) und CompoundRunnable schwirr(t)en vermutlich auch in deinem Kopf rum.

@Marco13 ist dein erstes Beispiel explizit so kompliziert geplant?
warum sollte jede allActions()-Methode super.actions() aufrufen, aber nicht die eigenen Aktionen?

nur durch allActions() von der Oberklasse mit Aufruf actions() wird dann von C was ausgegeben…


eine naheliegende Variante wurde erstaunlicherweise noch nicht genannt:
private void actionsIntern() {
actionB1();
actionB2();
}

@Override
void actions()  {
    actionsIntern();
}

@Override
void allActions()  {
    super.allActions();
    actionsIntern();
}

actions() und allActions() in allen Unterklassen gleich, etwas boilerplate,
dazu eine zusätzliche Methode actionsIntern(),
aber der wichtige Code zumindest, die Auflistung der Methoden, nicht doppelt

doppelten Code zu vermeiden ist das A und O, insbesondere bei fachlich wichtigen Code :wink:

Das erste Beispiel war kein “echter Lösungsversuch”. Ich war nur so irritiert von der Ausgabe (in Abhängigkeit von dem super. vor den action()-Aufrufen, dass ich mich an ein OCJP-Examen oder das Java-Quiz erinnert sah: Er hüpft da bei den Aufrufen so verwirrend durch die Klassen, dass es, selbst wenn es funktionieren würde, eine schlechte (weil schwer nachvollziehbare) Lösung wäre.