Ist das Überschreiben konkreter Methoden ein Codegeruch?


31

Stimmt es, dass das Überschreiben konkreter Methoden ein Codegeruch ist? Weil ich denke, wenn Sie konkrete Methoden überschreiben müssen:

public class A{
    public void a(){
    }
}

public class B extends A{
    @Override
    public void a(){
    }
}

es kann umgeschrieben werden als

public interface A{
    public void a();
}

public class ConcreteA implements A{
    public void a();
}

public class B implements A{
    public void a(){
    }
}

und wenn B ein () in A wiederverwenden möchte, kann es wie folgt umgeschrieben werden:

public class B implements A{
    public ConcreteA concreteA;
    public void a(){
        concreteA.a();
    }
}

Wofür ist keine Vererbung erforderlich, um die Methode zu überschreiben?



4
Abgestimmt, weil (in den Kommentaren unten) Telastyn und Doc Brown sich über das Überschreiben einig sind, aber die Überschriftenfrage mit Ja / Nein beantworten, da sie sich nicht über die Bedeutung von "Codegeruch" einig sind. Eine Frage, die sich mit Code-Design befassen sollte, war also eng mit einem Slang verbunden. Und das halte ich für unnötig, da es sich speziell um eine Technik gegen eine andere handelt.
Steve Jessop

5
Die Frage ist nicht mit Java markiert, aber der Code scheint Java zu sein. In C # (oder C ++) müssten Sie das virtualSchlüsselwort verwenden, um Überschreibungen zu unterstützen. Das Problem wird also durch die Einzelheiten der Sprache mehr (oder weniger) mehrdeutig.
Erik Eidt

1
Es scheint, als würde fast jedes Programmiersprachen-Feature nach genug Jahren unweigerlich als "Code-Geruch" eingestuft.
Brandon

2
Ich habe das Gefühl, dass der finalModifikator genau für solche Situationen existiert. Wenn das Verhalten der Methode so ist, dass sie nicht überschrieben werden soll, markieren Sie sie als final. Erwarten Sie andernfalls, dass Entwickler es möglicherweise außer Kraft setzen.
Martin Tuskevicius

Antworten:


34

Nein, es ist kein Codegeruch.

  • Wenn eine Klasse nicht endgültig ist, kann sie in Unterklassen unterteilt werden.
  • Wenn eine Methode nicht endgültig ist, kann sie überschrieben werden.

Es liegt in der Verantwortung jeder Klasse, sorgfältig zu prüfen, ob eine Unterklasse angemessen ist und welche Methoden möglicherweise überschrieben werden .

Die Klasse kann sich selbst oder eine Methode als endgültig definieren oder Einschränkungen (Sichtbarkeitsmodifikatoren, verfügbare Konstruktoren) dahingehend festlegen, wie und wo sie Unterklassen aufweist.

Der übliche Fall für das Überschreiben von Methoden ist eine Standardimplementierung in der Basisklasse, die in der Unterklasse angepasst oder optimiert werden kann (insbesondere in Java 8 mit dem Aufkommen von Standardmethoden in Schnittstellen).

class A {
    public String getDescription(Element e) {
        // return default description for element
    }
}

class B extends A {
    public String getDescription(Element e) {
        // return customized description for element
    }
}

Eine Alternative zum Überschreiben von Verhalten ist das Strategy Pattern , bei dem das Verhalten als Schnittstelle abstrahiert wird und die Implementierung in der Klasse festgelegt werden kann.

interface DescriptionProvider {
    String getDescription(Element e);
}

class A {
    private DescriptionProvider provider=new DefaultDescriptionProvider();

    public final String getDescription(Element e) {
       return provider.getDescription(e);
    }

    public final void setDescriptionProvider(@NotNull DescriptionProvider provider) {
        this.provider=provider;
    }
}

class B extends A {
    public B() {
        setDescriptionProvider(new CustomDescriptionProvider());
    }
}

Ich verstehe, dass in der realen Welt das Überschreiben einer konkreten Methode für manche ein Codegeruch sein kann. Aber ich mag diese Antwort, weil sie mir spezieller erscheint.
Darkwater23

In Ihrem letzten Beispiel sollten nicht Aimplementiert werden DescriptionProvider?
Gepunktet

@Spotted: A könnte implementieren DescriptionProviderund somit der Standardbeschreibungsanbieter sein. Die Idee hier ist jedoch die Trennung von Bedenken: ABenötigt die Beschreibung (einige andere Klassen könnten dies auch tun), während andere Implementierungen sie bereitstellen.
Peter Walser

2
Ok, klingt eher nach dem Strategiemuster .
Gepunktet

@Spotted: Sie haben Recht, das Beispiel veranschaulicht das Strategiemuster und nicht das Dekorationsmuster (ein Dekorationsprogramm würde einer Komponente Verhalten hinzufügen). Ich werde meine Antwort korrigieren, danke.
Peter Walser

25

Stimmt es, dass das Überschreiben konkreter Methoden ein Codegeruch ist?

Ja, im Allgemeinen ist das Überschreiben konkreter Methoden ein Codegeruch.

Da mit der Basismethode ein Verhalten verbunden ist, das Entwickler normalerweise respektieren, führt eine Änderung zu Fehlern, wenn Ihre Implementierung etwas anderes ausführt. Schlimmer noch, wenn sie das Verhalten ändern, kann Ihre zuvor korrekte Implementierung das Problem verschlimmern. Und im Allgemeinen ist es ziemlich schwierig, das Liskov-Substitutionsprinzip für nicht-triviale konkrete Methoden zu respektieren .

Jetzt gibt es Fälle, in denen dies möglich und sinnvoll ist. Semi-triviale Methoden können einigermaßen sicher übersteuert werden. Basismethoden , die nur als „gesund default“ Art von Verhalten gibt , die ist gemeint sein übergangen haben einige gute Anwendungen.

Daher scheint mir das ein Geruch zu sein - manchmal gut, normalerweise schlecht, schauen Sie, um sicherzugehen.


29
Ihre Erklärung ist in Ordnung, aber ich komme genau den entgegengesetzten Schluss: „Nein, Methoden konkrete zwingende ist nicht ein Code Geruch im Allgemeinen“ - es ist nur ein Code Geruch , wenn man überschreiben Methoden , die nicht beabsichtigt waren außer Kraft gesetzt zu sein. ;-)
Doc Brown

2
@ DocBrown genau! Ich sehe auch, dass die Erklärung (und insbesondere die Schlussfolgerung) der ursprünglichen Aussage widerspricht.
Tersosauros

1
@Spotted: Das einfachste Gegenbeispiel ist eine NumberKlasse mit einer increment()Methode, die den Wert auf den nächsten gültigen Wert setzt. Wenn Sie es in Unterklasse , EvenNumberwo increment()zwei fügt anstelle eines (und die Setter erzwingt Gleichmäßigkeit), der erste Vorschlag der OP erfordert doppelte Implementierungen von jeder Methode in der Klasse und seine zweite erfordert das Schreiben Wrapper - Methoden , die Methoden in dem Element zu nennen. Ein Teil des Vererbungszwecks besteht darin, dass untergeordnete Klassen deutlich machen, wie sie sich von den Eltern unterscheiden, indem sie nur die Methoden bereitstellen, die unterschiedlich sein müssen.
Blrfl

5
@DocBrown: Ein Codegeruch zu sein, bedeutet nicht, dass etwas unbedingt schlecht ist, nur, dass es wahrscheinlich ist .
mikołak

3
@docbrown - für mich fällt dies neben "Komposition vor Vererbung bevorzugen". Wenn Sie sich über konkrete Methoden hinwegsetzen, befinden Sie sich bereits in einem Land mit winzigem Geruch, in dem es um Vererbung geht. Wie groß ist die Wahrscheinlichkeit, dass Sie etwas überschreiten, das Sie nicht haben sollten? Aufgrund meiner Erfahrung halte ich es für wahrscheinlich. YMMV.
Telastyn

7

Wenn Sie konkrete Methoden überschreiben müssen, [...] kann dies als "" umgeschrieben werden

Wenn es auf diese Weise umgeschrieben werden kann, haben Sie die Kontrolle über beide Klassen. Aber dann wissen Sie, ob Sie Klasse A so entworfen haben und wenn ja, ist es kein Codegeruch.

Wenn Sie andererseits keine Kontrolle über die Basisklasse haben, können Sie nicht auf diese Weise neu schreiben. Entweder wurde die Klasse so entworfen, dass sie auf diese Weise abgeleitet wird. In diesem Fall handelt es sich nicht um einen Codegeruch. In diesem Fall haben Sie zwei Möglichkeiten: Suchen Sie nach einer anderen Lösung oder gehen Sie trotzdem vor , weil das Arbeiten die Reinheit des Designs übertrumpft.


Wäre es nicht sinnvoller, eine abstrakte Methode zu haben, um die Absichten klar auszudrücken, wenn Klasse A abgeleitet werden sollte? Wie kann derjenige, der die Methode überschreibt, wissen, dass die Methode auf diese Weise entworfen wurde?
Gepunktet

@Spotted, es gibt viele Fälle, in denen Standardimplementierungen bereitgestellt werden und jede Spezialisierung nur einige davon außer Kraft setzen muss, um die Funktionalität zu erweitern oder die Effizienz zu erhöhen. Ein extremes Beispiel sind Besucher. Der Benutzer weiß, wie die Methoden anhand der Dokumentation entworfen wurden. es muss da sein, um zu erklären, was von der Außerkraftsetzung erwartet wird.
Jan Hudec

Ich verstehe Ihren Standpunkt, dennoch halte ich es für gefährlich, dass ein Entwickler eine Methode nur dann überschreiben kann, wenn er das Dokument liest, weil: 1) wenn sie überschrieben werden muss, kann ich nicht garantieren, dass dies der Fall ist getan werden. 2) Wenn es nicht überschrieben werden darf, wird es aus praktischen Gründen von jemandem ausgeführt. 3) Nicht jeder liest das Dokument. Außerdem war ich nie mit einem Fall konfrontiert, in dem ich eine ganze Methode außer Kraft setzen musste, sondern nur Teile (und ich endete mit einem Schablonenmuster).
Gepunktet

@Spotted, 1) wenn es überschrieben werden muss , sollte es sein abstract; aber es gibt viele Fälle, in denen es nicht sein muss. 2) Wenn es nicht überschrieben werden darf, sollte es final(nicht virtuell) sein. Aber es gibt immer noch viele Fälle , in denen Methoden können außer Kraft gesetzt werden. 3) Wenn der nachgeschaltete Entwickler die Dokumente nicht liest, schießt er sich in den Fuß, tut dies jedoch unabhängig von den von Ihnen ergriffenen Maßnahmen.
Jan Hudec

Ok, unsere Sichtweise divergiert nur in einem Wort. Sie sagen , dass jede Methode sollten abstrakt sein oder endgültig , während ich sagen , dass jede Methode muss abstrakt sein oder endgültig. :) In welchen Fällen ist das Überschreiben einer Methode erforderlich (und sicher)?
Gepunktet

3

Lassen Sie mich zunächst darauf hinweisen, dass diese Frage nur in bestimmten Schreibsystemen anwendbar ist. Zum Beispiel in Structural Typing- und Duck-Typing- Systemen - eine Klasse mit einer Methode mit demselben Namen und derselben Signatur würde bedeuten, dass die Klasse typkompatibel ist (zumindest für diese bestimmte Methode im Fall von Duck-Typing).

Dies unterscheidet sich (offensichtlich) stark vom Bereich statisch typisierter Sprachen wie Java, C ++ usw. sowie von der Art der starken Typisierung , wie sie sowohl in Java und C ++ als auch in C # und anderen verwendet wird.


Ich vermute, Sie kommen aus einem Java-Hintergrund, in dem Methoden explizit als endgültig markiert werden müssen, wenn der Vertragsgestalter beabsichtigt, dass sie nicht überschrieben werden. In Sprachen wie C # ist jedoch das Gegenteil der Standard. Methoden werden (ohne Dekoration, spezielle Schlüsselwörter) " versiegelt " (C # -Version von final) und müssen explizit deklariert werden, als virtualob sie überschrieben werden dürfen .

Wenn eine API oder Bibliothek in diesen Sprachen mit einer konkreten Methode entworfen wurde, die überschrieben werden kann, muss dies (zumindest in einigen Fällen) sinnvoll sein, damit sie überschrieben wird. Daher ist es kein Codegeruch, eine nicht versiegelte / virtuelle / nicht finalkonkrete Methode zu überschreiben .     (Dies setzt natürlich voraus, dass die Designer der API Konventionen befolgen und entweder als virtual(C #) oder als final(Java) die geeigneten Methoden für das, was sie entwerfen , markieren .)


Siehe auch


Reichen beim Duck-Typing zwei Klassen mit derselben Methodensignatur aus, um typkompatibel zu sein, oder ist es auch erforderlich, dass die Methoden dieselbe Semantik haben, sodass sie für eine implizite Basisklasse durch Liskov ersetzt werden können?
Wayne Conrad

2

Ja, das liegt daran, dass es zu einem Super- Anti-Pattern kommen kann. Es kann auch den ursprünglichen Zweck der Methode denaturieren, Nebenwirkungen hervorrufen (=> Bugs) und Tests zum Scheitern bringen. Würden Sie eine Klasse verwenden, deren Unit-Tests rot (failling) sind? Werde ich nicht.

Ich gehe noch weiter und sage, der anfängliche Codegeruch ist, wenn eine Methode weder abstractnoch ist final. Weil es erlaubt, das Verhalten einer konstruierten Entität (durch Überschreiben) zu ändern (dieses Verhalten kann verloren gehen oder geändert werden). Mit abstractund finaldie Absicht ist eindeutig:

  • Abstract: Sie müssen ein Verhalten angeben
  • Final: Sie dürfen das Verhalten nicht ändern

Der sicherste Weg, einer vorhandenen Klasse Verhalten hinzuzufügen, ist das, was Ihr letztes Beispiel zeigt: das Dekorationsmuster verwenden.

public interface A{
    void a();
}

public final class ConcreteA implements A{
    public void a() {
        ...
    }
}

public final class B implements A{
    private final ConcreteA concreteA;
    public void a(){
        //Additionnal behaviour
        concreteA.a();
    }
}

9
Dieser Schwarz-Weiß- Ansatz ist eigentlich gar nicht so nützlich. Denken Sie Object.toString()an Java ... Wenn dies der abstractFall wäre, würden viele Standardfunktionen nicht funktionieren und der Code würde nicht einmal kompiliert, wenn jemand die toString()Methode nicht überschrieben hätte ! Wenn finaldies der Fall wäre, wären die Dinge noch schlimmer - es gibt keine Möglichkeit, Objekte in Zeichenfolgen umzuwandeln, mit Ausnahme der Java-Methode. Als @ Peter Walser ‚s Antwort spricht, Standardimplementierungen (wie Object.toString()) sind wichtig. Besonders in Java 8 Standardmethoden.
Tersosauros

@Tersosauros Du hast recht, wenn toString()entweder abstractoder finales wäre ein Schmerz. Meine persönliche Meinung ist, dass diese Methode kaputt ist und es besser gewesen wäre, sie überhaupt nicht zu haben (wie Equals und HashCode ). Der ursprüngliche Zweck von Java-Standardeinstellungen besteht darin, API-Evolution mit Retrokompatibilität zu ermöglichen.
Gepunktet

Das Wort "Retrokompatibilität" hat eine Menge Silben.
Craig

@Spotted Ich bin sehr enttäuscht über die allgemeine Akzeptanz der konkreten Vererbung auf dieser Website, ich habe es besser erwartet: / Ihre Antwort sollte viel mehr positive Stimmen haben
TheCatWhisperer

1
@TheCatWhisperer Ich stimme zu, aber andererseits habe ich so lange gebraucht, um die subtilen Vorteile der Verwendung von Dekoration gegenüber konkreter Vererbung herauszufinden (tatsächlich wurde klar, als ich anfing, Tests zu schreiben), dass Sie niemandem die Schuld dafür geben können . Am besten versuchen Sie, die anderen zu erziehen, bis sie die Wahrheit (den Witz) entdecken . :)
Gepunktet am
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.