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 JSwing
wir für eine Anwendung im Unterricht bauen
Man sollte alle erweitern JSwing
Klassen ( 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 setText
Funktion.
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 Okay
und Cancel
Schaltflä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 setText
on aufrufen Okay
und sie ändern. Die Unterklasse wird in diesem Fall nur zum Ärgernis.
JButton
öffentliche Methoden in ihrem Konstruktor erweitern und aufrufen, wenn SieJButton
diese öffentlichen Methoden einfach außerhalb der Klasse erstellen und aufrufen können?Antworten:
Dies verstößt gegen das Liskov-Substitutionsprinzip, da a an
OkayButton
keiner Stelle substituiert werden kann, an der aButton
erwartet wird. Sie können beispielsweise die Beschriftung einer beliebigen Schaltfläche nach Ihren Wünschen ändern. Aber das mit einerOkayButton
Verletzung 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.
quelle
OkayButton
hat die Invariante, an die Sie denken. DerOkayButton
Benutzer 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.activateButton(Button btn)
eine Schaltfläche erwartet, können Sie ihr leicht eine Instanz von gebenOkayButton
. Das Prinzip bedeutet nicht, dass es genau die gleiche Funktionalität haben muss, da dies die Vererbung so gut wie unbrauchbar machen würde.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 sagtif (myText == "OK") ProcessOK(); else ProcessCancel();
? Dann ist der Text Teil der Invariante.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.
quelle
JButton
habenJCheckBox
,JRadioButton
ist 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.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?)
quelle
JPanel
. Es ist tatsächlich sinnvoller, dies zu unterordnen alsJFrame
, da ein Panel nur ein abstrakter Container ist.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:
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:
Diese Typen, die Sie gerade erstellt haben, sind überhaupt nicht gut. Aber:
Jetzt haben Sie etwas Nützliches erstellt.
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.
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.
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
mui
Zeiger endgültig gemacht - er muss nicht veränderbar sein.quelle
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
extend
derJButton
Klasse nicht nur den Text zuweisen. Sie müssen nicht inextend
dieJFrame
Klasse, 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
extends
andere 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
JPanel
Klasse zu erweitern. (Mehr dazu hier .) Sie erweitern dieJPanel
Klasse, weil Sie die Funktion überschreibenpaintComponent()
müssen. Sie ändern damit das Verhalten der Klasse . Sie können mit der Standardimplementierung kein benutzerdefiniertes Malen ausführenJPanel
.Aber wie gesagt, Ihr Lehrer verwendet diese Beispiele wahrscheinlich nur als Entschuldigung, um Sie zu zwingen, das Erweitern des Unterrichts zu üben.
quelle
Border
, die zusätzliche Dekorationen malt. Oder erstellen Sie eineJLabel
und übergeben Sie eine benutzerdefinierteIcon
Implementierung an diese. Es ist nicht notwendig,JPanel
für das Malen eine Unterklasse zu bilden (und warumJPanel
stattdessenJComponent
).