Quand devrais-je étendre une classe Java Swing?

35

Ma compréhension actuelle de l'implémentation d'héritage est qu'il ne faut étendre une classe que si une relation IS-A est présente. Si la classe parent peut en outre avoir des types enfants plus spécifiques avec des fonctionnalités différentes, mais partagera des éléments communs résumés dans le parent.

Je remets en question cette compréhension à cause de ce que mon professeur de Java nous recommande de faire. Il a recommandé que pour une JSwingapplication que nous construisons en classe

Il faut étendre toutes les JSwingclasses ( JFrame, JButton, JTextBox, etc.) dans des classes personnalisées séparées et spécifiez GUI personnalisation liée à eux (comme la taille des composants, l' étiquette des composants, etc.)

Jusqu'ici, tout va bien, mais il ajoute que chaque JButton devrait avoir sa propre classe étendue personnalisée, même si le seul facteur distinctif est son étiquette.

Par exemple, si l'interface graphique a deux boutons Ok et Annuler . Il recommande de les prolonger comme suit:

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);        
    }
}

Comme vous pouvez le constater, la seule différence réside dans la setTextfonction.

Alors, est-ce la pratique habituelle?

Btw, le cours où cela a été discuté s'appelle Meilleures pratiques de programmation en Java

[Réponse du Prof]

J'ai donc discuté du problème avec le professeur et soulevé tous les points mentionnés dans les réponses.

Sa justification est que le sous-classement fournit un code réutilisable tout en respectant les normes de conception d'interface graphique. Par exemple , si le développeur a personnalisé utilisé Okayet des Cancelboutons dans une fenêtre, il sera plus facile de placer les mêmes boutons dans d' autres Windows ainsi.

Je comprends la raison, je suppose, mais il ne s'agit tout simplement que d'exploiter l' héritage et de fragiliser le code.

Par la suite, tout développeur pourrait appeler le par mégarde setTextsur un Okaybouton et le modifier. La sous-classe devient simplement gênante dans ce cas.

Paras
la source
3
Pourquoi étendre JButtonet appeler des méthodes publiques dans son constructeur, alors que vous pouvez simplement créer JButtonet appeler ces mêmes méthodes publiques en dehors de la classe?
17
restez loin de ce professeur si vous le pouvez. C'est assez loin d'être les meilleures pratiques . La duplication de code, telle que vous l'avez illustrée, dégage une très mauvaise odeur.
njzk2
7
Il suffit de lui demander pourquoi, n'hésitez pas à communiquer sa motivation ici.
Alex
9
Je veux faire une downvote parce que c'est un code affreux, mais je veux aussi une upvote parce que c'est bien de poser des questions au lieu d'accepter aveuglément ce qu'un mauvais professeur dit.
La poursuite de Monica
1
@Alex J'ai mis à jour la question avec la justification du prof
Paras

Réponses:

21

Ceci viole le principe de substitution de Liskov car un objet OkayButtonne peut être remplacé dans aucun endroit Buttonprévu. Par exemple, vous pouvez modifier l’étiquette de n’importe quel bouton à votre guise. Mais faire cela avec un OkayButtonviole ses invariants internes.

C'est un abus classique de l'héritage pour la réutilisation de code. Utilisez plutôt une méthode d'assistance.

Une autre raison de ne pas le faire est qu’il s’agit simplement d’un moyen compliqué d’obtenir la même chose que le code linéaire.

usr
la source
Cela ne viole pas le LSP sauf si OkayButtonvous avez l'invariant auquel vous songez. Le OkayButtonpeut prétendre ne pas avoir d’invariants supplémentaires, il peut simplement considérer le texte comme un défaut et avoir l’intention de supporter pleinement les modifications apportées au texte. Ce n'est toujours pas une bonne idée, mais pas pour cette raison.
hvd
Cela ne le viole pas parce que vous le pouvez. Par exemple, si une méthode activateButton(Button btn)attend un bouton, vous pouvez facilement lui donner une instance de OkayButton. Le principe ne signifie pas qu'il doit avoir exactement les mêmes fonctionnalités, car cela rendrait l'héritage pratiquement inutile.
Sebb
1
@Sebb mais vous ne pouvez pas l'utiliser avec une fonction setText(button, "x")car cela viole l'invariant (supposé). Il est vrai que cet OkayButton particulier n'a peut-être pas d'invariant intéressant, alors je suppose que j'ai pris un mauvais exemple. Mais ça pourrait. Par exemple, que se passe-t-il si le gestionnaire de clics dit if (myText == "OK") ProcessOK(); else ProcessCancel();? Ensuite, le texte fait partie de l'invariant.
USR
@usr Je n'ai pas pensé au texte faisant partie de l'invariant. Vous avez raison, c'est violé alors.
Sebb
50

C'est complètement terrible de toutes les manières possibles. Tout au plus , utilisez une fonction usine pour produire des boutons JB. Vous ne devriez en hériter que si vous avez des besoins d'extension importants.

DeadMG
la source
20
+1 Pour plus de références, consultez la hiérarchie des classes Swing de AbstractButton . Ensuite, regardez la hiérarchie de JToggleButton . L'héritage est utilisé pour conceptualiser différents types de boutons. Mais il n'est pas utilisé pour différencier les concepts commerciaux ( oui, non, continuer, annuler ... ).
Laiv
2
@Laiv: même avoir des types distincts pour, par exemple JButton, JCheckBox, JRadioButton, est juste une concession pour les développeurs étant familiers avec d' autres boîtes à outils, comme la AWT plaine, où ces types sont la norme. En principe, ils pourraient tous être gérés par une seule classe de boutons. C'est la combinaison du modèle et du délégué de l'interface utilisateur qui fait la différence.
Holger
Oui, ils pourraient être manipulés par un seul bouton. Cependant, la hiérarchie actuelle peut être présentée comme une bonne pratique. Pour créer un nouveau bouton ou simplement pour déléguer le contrôle d'événement et de behaivor à un autre composant, il faut choisir ses préférences. Si j'étais moi, j'allongerais les composants Swing afin de modéliser mon bouton owm et d'encapsuler ses comportements, actions, ... Juste pour faciliter sa mise en œuvre dans le projet et le rendre plus facile pour les juniors. Dans les environnements Web, je préfère les composants Web à une trop grande gestion d'événements. Quoi qu'il en soit. Vous avez raison, nous pouvons découpler l'interface utilisateur de Behaivors.
Laiv
12

C’est une façon très inhabituelle d’écrire du code Swing. En règle générale, vous ne créez que rarement des sous-classes de composants de l'interface utilisateur Swing, le plus souvent JFrame (afin de configurer des fenêtres enfants et des gestionnaires d'événements), mais même ce sous-classement est inutile et découragé. La personnalisation du texte en boutons et ainsi de suite est généralement effectuée en étendant la classe AbstractAction (ou l' interface Action fournie). Cela peut fournir du texte, des icônes et toute autre personnalisation visuelle nécessaire et les lier au code réel qu’ils représentent. C’est un moyen bien meilleur d’écrire du code d’interface utilisateur que les exemples que vous montrez.

(Au fait, Google Scholar n'a jamais entendu parler du document que vous citez - avez-vous une référence plus précise?)

Jules
la source
Quelle est exactement la "manière standard"? Je conviens qu'écrire une sous-classe pour chaque composant est probablement une mauvaise idée, mais les tutoriels officiels de Swing favorisent toujours cette pratique. Je pense que le professeur ne fait que suivre le style indiqué dans la documentation officielle du framework.
VENEZ DU
@COMEFROM Je vais admettre que je n'ai pas lu l'intégralité du tutoriel Swing, alors j'ai peut-être oublié où ce style est utilisé, mais les pages que j'ai consultées ont tendance à utiliser les classes de base et non les sous-classes de disques, par exemple docs.oracle
Jules
@ Jules désolé pour la confusion, je voulais dire que le cours / le papier enseigné par le professeur s'appelle Best Practices in Java
Paras
1
Généralement vrai, mais il existe une autre exception majeure - JPanel. En fait, il est plus utile de sous-classer cela JFrame, puisqu'un panneau n'est qu'un conteneur abstrait.
Ordous
10

IMHO, Les meilleures pratiques de programmation en Java sont définies dans le livre de Joshua Bloch, "Effective Java". C'est bien que votre professeur vous donne des exercices de POO et il est important d'apprendre à lire et à écrire les styles de programmation des autres personnes. Mais en dehors du livre de Josh Bloch, les opinions divergent beaucoup quant aux meilleures pratiques.

Si vous envisagez d'étendre cette classe, vous pouvez également tirer parti de l'héritage. Créez une classe MyButton pour gérer le code commun et sous-classe pour les parties variables:

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");
    }
}

Quand est-ce une bonne idée? Lorsque vous utilisez les types que vous avez créés! Par exemple, si vous avez une fonction pour créer une fenêtre contextuelle et que la signature est:

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

Les types que vous venez de créer ne font absolument aucun bien. Mais:

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

Maintenant, vous avez créé quelque chose d'utile.

  1. Le compilateur vérifie que showPopUp prend un OkButton et un CancelButton. Quelqu'un qui lit le code sait comment cette fonction est censée être utilisée car ce type de documentation provoquera une erreur de compilation si elle devient obsolète. C'est un avantage MAJEUR. Les 1 ou 2 études empiriques sur les avantages de la sécurité de type ont montré que la compréhension du code humain était le seul avantage quantifiable.

  2. Cela évite également les erreurs lorsque vous inversez l'ordre des boutons que vous passez à la fonction. Ces erreurs sont difficiles à repérer, mais elles sont également assez rares. Il s'agit donc d'un avantage MINEUR. Ceci a été utilisé pour vendre la sécurité de type, mais n'a pas été empiriquement prouvé comme étant particulièrement utile.

  3. La première forme de la fonction est plus flexible car elle permet d'afficher deux boutons quelconques. Parfois, cela vaut mieux que de limiter le type de boutons que cela prendra. Rappelez-vous simplement que vous devez tester la fonction any-button dans davantage de circonstances - si cela ne fonctionne que lorsque vous la passez exactement au bon type de boutons, vous ne faites de faveurs à personne en prétendant que cela prendrait n'importe quel type de bouton.

Le problème avec la programmation orientée objet est qu’elle met la charrue avant les boeufs. Vous devez d'abord écrire la fonction pour savoir en quoi consiste la signature et savoir s'il est intéressant de créer des types pour cette signature. Mais en Java, vous devez d’abord créer vos types pour pouvoir écrire la signature de la fonction.

Pour cette raison, vous pouvez écrire du code Clojure pour voir à quel point il peut être génial d'écrire vos fonctions en premier. Vous pouvez coder rapidement, en pensant autant que possible à la complexité asymptotique, et l'hypothèse de l'immuabilité de Clojure évite les bogues autant que les types le font en Java.

Je suis toujours un fan des types statiques pour les grands projets et utilise maintenant des utilitaires fonctionnels qui me permettent d' écrire des fonctions en premier et de nommer mes types plus tard en Java . Juste une pensée.

PS J'ai fait le muipointeur final - il n'a pas besoin d'être mutable.

GlenPeterson
la source
6
De vos 3 points, 1 et 3 peuvent tout aussi bien être traités avec des boutons JButtons génériques et un schéma de nommage des paramètres d'entrée approprié. le point 2 est en réalité un inconvénient dans la mesure où il rend votre code plus étroitement couplé: que faire si vous voulez que l'ordre des boutons soit inversé? Et si, au lieu des boutons OK / Annuler, vous souhaitez utiliser les boutons Oui / Non? Ferez-vous une méthode showPopup entièrement nouvelle qui prend un YesButton et un Nobutton? Si vous utilisez uniquement des boutons JBut par défaut, vous n'avez pas besoin de créer vos types car ils existent déjà.
Nzall
En prolongeant ce que @NateKerkhofs a dit, un IDE moderne vous montrera les noms des arguments formels pendant que vous tapez les expressions réelles.
Salomon Slow
8

Je serais prêt à parier que votre professeur ne croit pas réellement que vous devriez toujours utiliser un composant Swing pour l'utiliser. Je parie qu'ils ne font que citer cet exemple pour vous forcer à vous entraîner à prolonger les cours. Je ne m'inquiéterais pas trop des meilleures pratiques du monde réel pour l'instant.

Cela dit, dans le monde réel, nous privilégions la composition au détriment de l'héritage .

Votre règle "on ne devrait étendre une classe que si une relation IS-A est présente" est incomplète. Il devrait se terminer par "... et nous devons changer le comportement par défaut de la classe" en gros caractères gras.

Vos exemples ne correspondent pas à ce critère. Vous n'avez pas à extendla JButtonclasse juste pour définir son texte. Vous n'êtes pas obligé de simplement ajouter des composants à extendla JFrameclasse. Vous pouvez très bien faire ces choses en utilisant leurs implémentations par défaut. Ajouter l'héritage, c'est donc ajouter des complications inutiles.

Si je vois une classe dans extendsune autre classe, je vais me demander ce que cette classe est en train de changer . Si vous ne changez rien, ne me faites pas du tout regarder la classe.

Retour à votre question: quand devriez-vous étendre une classe Java? Lorsque vous avez vraiment une très bonne raison de prolonger un cours.

Voici un exemple spécifique: une façon de faire une peinture personnalisée (pour un jeu ou une animation, ou simplement pour un composant personnalisé) consiste à étendre la JPanelclasse. (Plus d'informations à ce sujet ici .) Vous étendez la JPanelclasse parce que vous devez remplacer la paintComponent()fonction. En fait, vous changez le comportement de la classe . Vous ne pouvez pas faire de peinture personnalisée avec l' JPanelimplémentation par défaut .

Mais comme je l'ai dit, votre professeur utilise probablement ces exemples comme excuse pour vous forcer à vous entraîner à prolonger les cours.

Kevin Workman
la source
1
Oh, attends, tu peux définir un décor Borderqui peint d'autres décorations. Ou créez une JLabelet passez une Iconimplémentation personnalisée . Il n'est pas nécessaire de sous-classer JPanelpour la peinture (et pourquoi JPanelau lieu de JComponent).
Holger
1
Je pense que vous avez la bonne idée ici, mais que vous pourriez probablement choisir de meilleurs exemples.
1
Mais je tiens à réitérer que votre réponse est correcte et que vous faites valoir de bons arguments . Je pense juste que l'exemple spécifique et le tutoriel le supportent faiblement.
1
@KevinWorkman Je ne connais pas le développement de jeux Swing, mais j'ai créé des interfaces utilisateur personnalisées dans Swing et "ne pas étendre les classes Swing, il est donc facile de câbler des composants et des moteurs de rendu via config".
1
"Je parie qu'ils ne font que citer cela à titre d'exemple pour vous forcer à ..." L'un de mes plus gros peeves! Les instructeurs qui enseignent comment faire X en utilisant des exemples horriblement dérangés expliquant pourquoi il faut faire X.
Solomon Slow