Ma compréhension actuelle de l'implémentation de l'héritage est que l'on ne doit é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 abstraits dans le parent.
Je doute de cette compréhension à cause de ce que mon Java professeur nous recommande de faire. Il a recommandé que pour une application JSwing
que nous construisions en classe
Il faut étendre toutes les classes JSwing
(JFrame
, JButton
, JTextBox
, etc.) dans des classes personnalisées distinctes et spécifier des personnalisations liées à l'interface graphique (comme le composant taille, étiquette de composant, etc.)
Jusqu'à présent, 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 voir, la seule différence réside dans la fonction setText
.
Est-ce donc cette pratique standard?
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 du code réutilisable tout en suivant les normes de conception de l'interface graphique. Par exemple, si le développeur a utilisé des boutons Okay
et Cancel
personnalisés dans une fenêtre, il sera également plus facile de placer les mêmes boutons dans d'autres fenêtres.
J'obtiens la raison que je suppose, mais il s'agit toujours d'exploiter l'héritage et de fragiliser le code.
Plus tard, n'importe quel développeur pourrait accidentellement appeler le setText
sur un bouton Okay
et le modifier. La sous-classe devient juste une nuisance dans ce cas.
Cela viole le Liskov Substitution Principle car un OkayButton
ne peut être substitué à aucun endroit où un Button
est attendu. Par exemple, vous pouvez modifier le libellé de n'importe quel bouton à votre guise. Mais faire cela avec un OkayButton
viole ses invariants internes.
Il s'agit d'une mauvaise utilisation 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 que c'est juste une façon compliquée de réaliser la même chose que le code linéaire.
C'est complètement terrible de toutes les manières possibles. À la plupart, utilisez une fonction d'usine pour produire des JButtons. Vous ne devez en hériter que si vous avez de sérieux besoins d'extension.
Il s'agit d'une façon très non standard d'écrire du code Swing. En règle générale, vous ne créez que rarement des sous-classes de composants Swing UI, le plus souvent JFrame (afin de configurer des fenêtres enfants et des gestionnaires d'événements), mais même cette sous-classe est inutile et découragée par beaucoup. La personnalisation du texte aux boutons et ainsi de suite est généralement effectuée en étendant la classe AbstractAction (ou la interface Action qu'elle fournit). Cela peut fournir du texte, des icônes et d'autres personnalisations visuelles nécessaires et les lier au code réel qu'ils représentent. C'est une bien meilleure façon d'écrire du code d'interface 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?)
À mon humble avis, les meilleures pratiques de programmation dans Java sont définies par le livre de Joshua Bloch, "Effective Java." C'est génial que votre professeur vous donne OOP exercices et c'est important) apprendre à lire et à écrire les styles de programmation des autres. Mais en dehors du livre de Josh Bloch, les opinions varient assez largement sur les meilleures pratiques.
Si vous comptez étendre cette classe, vous pouvez tout aussi bien profiter de l'héritage. Créez une classe MyButton pour gérer le code commun et sous-classe-le 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 pop-up et que la signature est:
public void showPopUp(String text, JButton ok, JButton cancel)
Ces types que vous venez de créer ne servent à rien. Mais:
public void showPopUp(String text, OkButton ok, CancelButton cancel)
Vous venez de créer quelque chose d'utile.
Le compilateur vérifie que showPopUp prend un OkButton et un CancelButton. Quelqu'un lisant le code sait comment cette fonction est censée être utilisée car ce type de documentation provoquera une erreur de compilation si elle est obsolète. C'est un avantage MAJEUR. Les 1 ou 2 études empiriques sur les avantages de la sécurité de type ont révélé que la compréhension du code humain était le seul avantage quantifiable.
Cela empêche é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, c'est donc un avantage MINEUR. Cela a été utilisé pour vendre la sécurité des types, mais n'a pas été empiriquement prouvé être particulièrement utile.
La première forme de la fonction est plus flexible car elle affichera deux boutons quelconques. Parfois, c'est mieux que de limiter le type de boutons qu'il faudra. N'oubliez pas que vous devez tester la fonction de n'importe quel bouton dans plus de circonstances - si cela ne fonctionne que lorsque vous lui passez exactement le bon type de boutons, vous ne faites de faveur à personne en prétendant qu'il prendra tout type de bouton.
Le problème avec la programmation orientée objet est qu'elle met le chariot avant le cheval. Vous devez d'abord écrire la fonction pour savoir ce qu'est la signature pour savoir s'il vaut la peine de faire des types pour elle. Mais en Java, vous devez d'abord créer vos types pour pouvoir écrire la signature de la fonction.
Pour cette raison, vous voudrez peut-être é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 d'immuabilité de Clojure empêche les bogues autant que les types le font en Java.
Je suis toujours un fan des types statiques pour les grands projets et j'utilise maintenant quelques utilitaires fonctionnels qui me permettent de écrire les fonctions d'abord et nommer mes types plus tard en Java . Juste une pensée.
P.S. J'ai fait le pointeur mui
final - il n'a pas besoin d'être mutable.
Je serais prêt à parier que votre professeur ne croit pas réellement que vous devriez toujours étendre un composant Swing pour l'utiliser. Je parie qu'ils utilisent juste cela comme exemple pour vous forcer à pratiquer l'extension des cours. Je ne me soucierais pas trop des meilleures pratiques du monde réel pour l'instant.
Cela étant dit, dans le monde réel, nous privilégions composition par rapport à l'héritage .
Votre règle "il ne faut é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 grosses lettres grasses.
Vos exemples ne correspondent pas à ces critères. Vous n'avez pas à extend
la classe JButton
juste pour définir son texte. Vous n'avez pas à extend
la classe JFrame
juste pour y ajouter des composants. Vous pouvez très bien faire ces choses en utilisant leurs implémentations par défaut, donc l'ajout d'héritage ne fait qu'ajouter des complications inutiles.
Si je vois une classe qui extends
une autre classe, je vais me demander ce que cette classe change . Si vous ne changez rien, ne me faites pas du tout regarder la classe.
Revenons à votre question: quand faut-il étendre une Java? Quand vous avez une vraiment, vraiment, vraiment bonne raison d'étendre une classe.
Voici un exemple spécifique: une façon de faire une peinture personnalisée (pour un jeu ou une animation, ou tout simplement pour un composant personnalisé) consiste à étendre la classe JPanel
. (Plus d'informations à ce sujet ici .) Vous étendez la classe JPanel
car vous devez remplacer le paintComponent()
fonction. Vous êtes en fait en train de changer le comportement de la classe en faisant cela. Vous ne pouvez pas faire de peinture personnalisée avec l'implémentation par défaut JPanel
.
Mais comme je l'ai dit, votre professeur utilise probablement ces exemples comme excuse pour vous forcer à pratiquer l'extension des cours.