Wann sollte ich eine Java-Swing-Klasse erweitern?

35

Nach meinem derzeitigen Verständnis der Vererbungsimplementierung sollte eine Klasse nur erweitert werden, wenn eine IS-A- Beziehung vorhanden ist. Wenn die übergeordnete Klasse über spezifischere untergeordnete Typen mit unterschiedlicher Funktionalität verfügen kann, jedoch gemeinsame, im übergeordneten Element abstrahierte Elemente aufweist.

Ich stelle dieses Verständnis in Frage, weil mein Java-Professor uns empfiehlt, dies zu tun. Er hat empfohlen, dass JSwingwir für eine Anwendung im Unterricht bauen

Man sollte alle erweitern JSwingKlassen ( JFrame, JButton, JTextBox, usw.) in separate benutzerdefinierte Klassen und geben Sie GUI ähnliche Anpassung in ihnen (wie die Bauteilgröße, Komponentenbezeichnung usw.)

So weit so gut, aber er rät weiter, dass jeder JButton eine eigene benutzerdefinierte erweiterte Klasse haben sollte, obwohl der einzige Unterscheidungsfaktor das Etikett ist.

Zum Beispiel, wenn die GUI über zwei Schaltflächen OK und Abbrechen verfügt . Er empfiehlt, sie wie folgt zu erweitern:

class OkayButton extends JButton{
    MainUI mui;
    public OkayButton(MainUI mui) {
        setSize(80,60);
        setText("Okay");
        this.mui = mui;
        mui.add(this);        
    }
}

class CancelButton extends JButton{
    MainUI mui;
    public CancelButton(MainUI mui) {
        setSize(80,60);
        setText("Cancel");
        this.mui = mui;
        mui.add(this);        
    }
}

Wie Sie sehen, liegt der einzige Unterschied in der setTextFunktion.

Ist das also die Standardpraxis?

Übrigens wird der Kurs, in dem dies besprochen wurde, als Best Programming Practices in Java bezeichnet

[Antwort von Prof]

Deshalb habe ich das Problem mit dem Professor besprochen und alle in den Antworten genannten Punkte angesprochen.

Seine Rechtfertigung ist, dass Unterklassen wiederverwendbaren Code bereitstellen, während sie den Standards für das GUI-Design folgen. Wenn der Entwickler beispielsweise benutzerdefinierte Schaltflächen Okayund CancelSchaltflächen in einem Fenster verwendet hat, ist es einfacher, dieselben Schaltflächen auch in anderen Fenstern zu platzieren.

Ich verstehe den Grund, den ich nehme an, aber es nutzt nur die Vererbung und macht den Code zerbrechlich.

Später könnte jeder Entwickler versehentlich die Schaltfläche setTexton aufrufen Okayund sie ändern. Die Unterklasse wird in diesem Fall nur zum Ärgernis.

Paras
quelle
3
Warum JButtonöffentliche Methoden in ihrem Konstruktor erweitern und aufrufen, wenn Sie JButtondiese öffentlichen Methoden einfach außerhalb der Klasse erstellen und aufrufen können?
17
halte dich von diesem Professor fern, wenn du kannst. Dies ist weit davon entfernt, Best Practices zu sein . Code-Vervielfältigung, wie Sie es dargestellt haben, ist ein sehr übler Geruch.
njzk2
7
Fragen Sie ihn einfach warum, zögern Sie nicht, hier seine Motivation mitzuteilen.
Alex
9
Ich möchte abstimmen, weil das schrecklicher Code ist, aber ich möchte auch abstimmen, weil es toll ist, dass Sie Fragen stellen, anstatt blindlings zu akzeptieren, was ein schlechter Professor sagt.
Nic Hartley
1
@ Alex Ich habe die Frage mit der Begründung des Prof aktualisiert
Paras

Antworten:

21

Dies verstößt gegen das Liskov-Substitutionsprinzip, da a an OkayButtonkeiner Stelle substituiert werden kann, an der a Buttonerwartet wird. Sie können beispielsweise die Beschriftung einer beliebigen Schaltfläche nach Ihren Wünschen ändern. Aber das mit einer OkayButtonVerletzung seiner internen Invarianten zu tun .

Dies ist ein klassischer Missbrauch der Vererbung zur Wiederverwendung von Code. Verwenden Sie stattdessen eine Hilfsmethode.

Ein weiterer Grund, dies nicht zu tun, ist, dass dies nur eine verschlungene Methode ist, um dasselbe zu erreichen, was linearer Code tun würde.

usr
quelle
Dies verstößt nicht gegen LSP, es sei denn, der OkayButtonhat die Invariante, an die Sie denken. Der OkayButtonBenutzer kann behaupten, keine zusätzlichen Invarianten zu haben, sondern lediglich den Text als Standard zu betrachten und beabsichtigt, Änderungen am Text vollständig zu unterstützen. Immer noch keine gute Idee, aber nicht aus diesem Grund.
HDV
Es verletzt es nicht, weil du kannst. Dh, wenn eine Methode activateButton(Button btn)eine Schaltfläche erwartet, können Sie ihr leicht eine Instanz von geben OkayButton. Das Prinzip bedeutet nicht, dass es genau die gleiche Funktionalität haben muss, da dies die Vererbung so gut wie unbrauchbar machen würde.
Sebb
1
@ Sebb, aber Sie können es nicht mit einer Funktion verwenden, setText(button, "x")da dies die (angenommene) Invariante verletzt. Es ist wahr, dass dieser bestimmte OkayButton keine interessante Invariante hat, also habe ich wohl ein schlechtes Beispiel ausgewählt. Aber es könnte sein. Was ist zum Beispiel, wenn der Click-Handler sagt if (myText == "OK") ProcessOK(); else ProcessCancel();? Dann ist der Text Teil der Invariante.
usr
@usr Ich habe nicht daran gedacht, dass der Text Teil der Invariante ist. Du hast recht, es ist dann verletzt.
Sebb
50

Es ist in jeder Hinsicht schrecklich. Bei den meisten , verwenden Sie eine Fabrik - Funktion JButtons zu produzieren. Sie sollten nur dann von ihnen erben, wenn Sie ernsthafte Erweiterungsbedürfnisse haben.

DeadMG
quelle
20
+1. Weitere Informationen finden Sie in der Swing-Klassenhierarchie von AbstractButton . Dann schauen Sie sich die Hierarchie von JToggleButton an . Die Vererbung wird verwendet, um verschiedene Arten von Schaltflächen zu konzipieren. Es wird jedoch nicht zur Unterscheidung von Geschäftskonzepten verwendet ( Ja, Nein, Fortfahren, Abbrechen ... ).
Laiv
2
@Laiv: Sogar unterschiedliche Typen für z. B. zu JButtonhaben JCheckBox, JRadioButtonist nur ein Zugeständnis für Entwickler, die mit anderen Toolkits vertraut sind , z. B. dem einfachen AWT, bei dem solche Typen der Standard sind. Grundsätzlich können sie alle von einer einzigen Button-Klasse behandelt werden. Es ist die Kombination aus Modell und Benutzeroberflächendelegierter, die den eigentlichen Unterschied ausmacht.
Holger
Ja, sie könnten mit einem einzigen Knopf bedient werden. Die tatsächliche Hierarchie kann jedoch als bewährte Praktiken herausgestellt werden. Das Erstellen eines neuen Buttons oder nur das Delegieren der Kontrolle von Ereignissen und Verhalten auf andere Komponenten ist eine Frage der Präferenzen. Wenn ich es wäre, würde ich die Swing-Komponenten erweitern, um meinen Owm-Button zu modellieren und seine Verhaltensweisen, Aktionen usw. zusammenzufassen. Nur um die Implementierung im Projekt zu vereinfachen und es für Junioren einfacher zu machen. In Web-Umgebungen bevorzuge ich Web-Komponenten gegenüber zu viel Event-Handling. Sowieso. Sie haben Recht, wir können die Benutzeroberfläche von Behaivors entkoppeln.
Laiv
12

Dies ist eine sehr ungewöhnliche Art, Swing-Code zu schreiben. Normalerweise erstellen Sie nur selten Unterklassen von Swing-UI-Komponenten, in der Regel JFrame (um untergeordnete Fenster und Ereignishandler einzurichten), aber selbst diese Unterklassen sind unnötig und werden von vielen davon abgehalten. Die Anpassung von Text an Schaltflächen usw. erfolgt normalerweise durch Erweitern der AbstractAction-Klasse (oder der von ihr bereitgestellten Aktionsschnittstelle ). Dies kann Text, Symbole und andere erforderliche visuelle Anpassungen bereitstellen und diese mit dem tatsächlichen Code verknüpfen, den sie darstellen. Dies ist eine weitaus bessere Methode zum Schreiben von UI-Code als die von Ihnen gezeigten Beispiele.

(Übrigens, Google Scholar hat noch nie von dem Artikel gehört, den Sie zitieren. Haben Sie eine genauere Referenz?)

Jules
quelle
Was genau ist der "Standardweg"? Ich bin damit einverstanden, dass das Schreiben einer Unterklasse für jede Komponente wahrscheinlich eine schlechte Idee ist, aber die offiziellen Swing-Tutorials befürworten diese Praxis immer noch. Ich gehe davon aus, dass der Professor nur dem Stil folgt, der in der offiziellen Dokumentation des Frameworks gezeigt wird.
KOMMEN SIE VOM
@COMEFROM Ich gebe zu, dass ich nicht das gesamte Swing-Tutorial gelesen habe, also habe ich vielleicht verpasst, wo dieser Stil verwendet wird, aber die Seiten, die ich mir angesehen habe, tendieren dazu, die Basisklassen und keine Laufwerksunterklassen zu verwenden, z. B. docs.oracle / javase / tutorial / uiswing / components / button.html
Jules
@Jules Entschuldigung für die Verwirrung, ich meinte, der Kurs / das Referat, das der Professor unterrichtet, heißt Best Practices in Java
Paras
1
Generell wahr, aber es gibt eine andere große Ausnahme - JPanel. Es ist tatsächlich sinnvoller, dies zu unterordnen als JFrame, da ein Panel nur ein abstrakter Container ist.
Ordentliche
10

IMHO, Best Programming Practices in Java, werden von Joshua Blochs Buch "Effective Java" definiert. Es ist großartig, dass Ihr Lehrer Ihnen OOP-Übungen gibt, und es ist wichtig zu lernen, die Programmierstile anderer Leute zu lesen und zu schreiben. Aber außerhalb von Josh Blochs Buch gehen die Meinungen über Best Practices ziemlich weit auseinander.

Wenn Sie diese Klasse erweitern möchten, können Sie auch die Vererbung nutzen. Erstellen Sie eine MyButton-Klasse, um den allgemeinen Code zu verwalten, und unterteilen Sie ihn für die variablen Teile:

class MyButton extends JButton{
    protected final MainUI mui;
    public MyButton(MainUI mui, String text) {
        setSize(80,60);
        setText(text);
        this.mui = mui;
        mui.add(this);        
    }
}

class OkayButton extends MyButton{
    public OkayButton(MainUI mui) {
        super(mui, "Okay");
    }
}

class CancelButton extends MyButton{
    public CancelButton(MainUI mui) {
        super(mui, "Cancel");
    }
}

Wann ist das eine gute Idee? Wenn Sie die Typen verwenden, die Sie erstellt haben! Wenn Sie beispielsweise eine Funktion zum Erstellen eines Popup-Fensters haben und die Signatur lautet:

public void showPopUp(String text, JButton ok, JButton cancel)

Diese Typen, die Sie gerade erstellt haben, sind überhaupt nicht gut. Aber:

public void showPopUp(String text, OkButton ok, CancelButton cancel)

Jetzt haben Sie etwas Nützliches erstellt.

  1. Der Compiler überprüft, ob showPopUp einen OkButton und einen CancelButton akzeptiert. Jemand , der den Code liest , weiß, wie diese Funktion verwendet werden soll, da diese Art von Dokumentation einen Fehler beim Kompilieren verursacht, wenn sie veraltet ist. Dies ist ein großer Vorteil. Die 1 oder 2 empirischen Studien zu den Vorteilen der Typensicherheit ergaben, dass das Verständnis des menschlichen Codes der einzige quantifizierbare Vorteil ist.

  2. Dies verhindert auch Fehler, bei denen Sie die Reihenfolge der an die Funktion übergebenen Schaltflächen umkehren. Diese Fehler sind schwer zu erkennen, aber sie sind auch ziemlich selten, so dass dies ein kleiner Vorteil ist. Dies wurde verwendet, um Typensicherheit zu verkaufen, hat sich jedoch empirisch nicht als besonders nützlich erwiesen.

  3. Die erste Form der Funktion ist flexibler, da zwei beliebige Schaltflächen angezeigt werden. Manchmal ist das besser, als die Art der Tasten einzuschränken. Denken Sie jedoch daran, dass Sie die Funktion any-button unter mehreren Umständen testen müssen. Wenn sie nur dann funktioniert, wenn Sie genau die richtige Art von Tasten übergeben, tun Sie niemandem einen Gefallen, indem Sie so tun, als würde sie eine beliebige Art von Taste annehmen.

Das Problem bei der objektorientierten Programmierung ist, dass der Wagen vor das Pferd gestellt wird. Sie müssen die Funktion zuerst schreiben, um zu wissen, was die Signatur ist, wenn es sich lohnt, Typen dafür zu erstellen. In Java müssen Sie jedoch zuerst Ihre Typen erstellen, damit Sie die Funktionssignatur schreiben können.

Aus diesem Grund möchten Sie vielleicht etwas Clojure-Code schreiben, um zu sehen, wie großartig es sein kann, zuerst Ihre Funktionen zu schreiben. Sie können schnell programmieren und dabei so viel wie möglich an asymptotische Komplexität denken. Die Unveränderlichkeitsannahme von Clojure verhindert Fehler, wie dies bei Java-Typen der Fall ist.

Ich bin immer noch ein Fan von statischen Typen für große Projekte und verwende jetzt einige funktionale Dienstprogramme, mit denen ich Funktionen zuerst schreiben und meine Typen später in Java benennen kann . Nur ein Gedanke.

PS Ich habe den muiZeiger endgültig gemacht - er muss nicht veränderbar sein.

GlenPeterson
quelle
6
Von Ihren 3 Punkten können 1 und 3 genauso gut mit generischen JButtons und einem geeigneten Benennungsschema für Eingabeparameter behandelt werden. Punkt 2 ist tatsächlich ein Nachteil, da Ihr Code dadurch enger miteinander verbunden wird: Was ist, wenn Sie möchten, dass die Tastenreihenfolge umgekehrt wird? Was ist, wenn Sie anstelle der Schaltflächen OK / Abbrechen die Schaltflächen Ja / Nein verwenden möchten? Wirst du eine völlig neue showPopup-Methode erstellen, die einen YesButton und einen Nobutton akzeptiert? Wenn Sie nur Standard-JButtons verwenden, müssen Sie Ihre Typen nicht zuerst erstellen, da sie bereits vorhanden sind.
Nzall
Eine moderne IDE erweitert die Aussage von @NateKerkhofs und zeigt Ihnen die formalen Argumentnamen, während Sie die tatsächlichen Ausdrücke eingeben.
Solomon Slow
8

Ich würde wetten, dass Ihr Lehrer nicht wirklich glaubt, dass Sie immer eine Swing-Komponente erweitern sollten, um sie zu verwenden. Ich wette, sie verwenden dies nur als Beispiel, um dich zu zwingen, das Erweitern von Klassen zu üben. Ich würde mir noch nicht allzu viele Sorgen um die Best Practices der Praxis machen.

In der realen Welt bevorzugen wir jedoch die Komposition gegenüber der Vererbung .

Ihre Regel "Eine Klasse sollte nur erweitert werden, wenn eine IS-A-Beziehung vorliegt" ist unvollständig. Es sollte mit "... und wir müssen das Standardverhalten der Klasse ändern" in großen, fetten Buchstaben enden .

Ihre Beispiele entsprechen nicht diesen Kriterien. Sie müssen extendder JButtonKlasse nicht nur den Text zuweisen. Sie müssen nicht in extenddie JFrameKlasse, nur um Komponenten hinzuzufügen. Sie können diese Aufgaben problemlos mit den Standardimplementierungen ausführen, sodass das Hinzufügen von Vererbung nur unnötige Komplikationen mit sich bringt.

Wenn ich eine Klasse sehe, die eine extendsandere Klasse ist, frage ich mich, was sich in dieser Klasse ändert . Wenn Sie nichts ändern, lassen Sie mich überhaupt nicht durch die Klasse schauen.

Zurück zu Ihrer Frage: Wann sollten Sie eine Java-Klasse erweitern? Wenn Sie einen wirklich, wirklich, wirklich guten Grund haben, eine Klasse zu verlängern.

Hier ein konkretes Beispiel: Eine Möglichkeit zum benutzerdefinierten Malen (für ein Spiel oder eine Animation oder nur für eine benutzerdefinierte Komponente) besteht darin, die JPanelKlasse zu erweitern. (Mehr dazu hier .) Sie erweitern die JPanelKlasse, weil Sie die Funktion überschreibenpaintComponent() müssen. Sie ändern damit das Verhalten der Klasse . Sie können mit der Standardimplementierung kein benutzerdefiniertes Malen ausführen JPanel.

Aber wie gesagt, Ihr Lehrer verwendet diese Beispiele wahrscheinlich nur als Entschuldigung, um Sie zu zwingen, das Erweitern des Unterrichts zu üben.

Kevin Workman
quelle
1
Oh warte, du kannst eine einstellen Border, die zusätzliche Dekorationen malt. Oder erstellen Sie eine JLabelund übergeben Sie eine benutzerdefinierte IconImplementierung an diese. Es ist nicht notwendig, JPanelfür das Malen eine Unterklasse zu bilden (und warum JPanelstattdessen JComponent).
Holger
1
Ich denke, Sie haben hier die richtige Idee, könnten aber wahrscheinlich bessere Beispiele auswählen.
1
Aber ich möchte noch einmal wiederholen, dass Ihre Antwort richtig ist und Sie gute Argumente vorbringen . Ich denke nur, dass das spezifische Beispiel und das Tutorial es schwach unterstützen.
1
@KevinWorkman Ich kenne mich mit der Entwicklung von Swing-Spielen nicht aus, aber ich habe einige benutzerdefinierte Benutzeroberflächen in Swing erstellt und "Swing-Klassen nicht erweitert, so dass es einfach ist, Komponenten und Renderer über die Konfiguration zu verbinden", ist dort ziemlich Standard.
1
"Ich wette, sie benutzen dies nur als Beispiel, um dich zu zwingen ..." Einer meiner größten Ärmel! Lehrer , die lehren , wie X zu tun mit schrecklich deranged Beispielen , warum X. zu tun
Solomon Langsam