J'ai une classe de base, Base
. Il a deux sous-classes, Sub1
et Sub2
. Chaque sous-classe a quelques méthodes supplémentaires. Par exemple, Sub1
a Sandwich makeASandwich(Ingredients... ingredients)
et Sub2
a boolean contactAliens(Frequency onFrequency)
.
Puisque ces méthodes prennent des paramètres différents et font des choses complètement différentes, elles sont complètement incompatibles et je ne peux pas simplement utiliser le polymorphisme pour résoudre ce problème.
Base
fournit la plupart des fonctionnalités, et j'ai une grande collection d' Base
objets. Cependant, tous les Base
objets sont soit un, Sub1
soit un Sub2
et parfois j'ai besoin de savoir qui ils sont.
Cela semble être une mauvaise idée de faire ce qui suit:
for (Base base : bases) {
if (base instanceof Sub1) {
((Sub1) base).makeASandwich(getRandomIngredients());
// ... etc.
} else { // must be Sub2
((Sub2) base).contactAliens(getFrequency());
// ... etc.
}
}
Donc, je suis venu avec une stratégie pour éviter cela sans casting. Base
a maintenant ces méthodes:
boolean isSub1();
Sub1 asSub1();
Sub2 asSub2();
Et bien sûr, Sub1
implémente ces méthodes comme
boolean isSub1() { return true; }
Sub1 asSub1(); { return this; }
Sub2 asSub2(); { throw new IllegalStateException(); }
Et les Sub2
met en œuvre dans le sens opposé.
Malheureusement, maintenant Sub1
et Sub2
ont ces méthodes dans leur propre API. Donc, je peux le faire, par exemple, sur Sub1
.
/** no need to use this if object is known to be Sub1 */
@Deprecated
boolean isSub1() { return true; }
/** no need to use this if object is known to be Sub1 */
@Deprecated
Sub1 asSub1(); { return this; }
/** no need to use this if object is known to be Sub1 */
@Deprecated
Sub2 asSub2(); { throw new IllegalStateException(); }
De cette façon, si l'objet est connu pour être seulement un Base
, ces méthodes sont non dépréciées et peuvent être utilisées pour se "convertir" en un type différent afin que je puisse invoquer les méthodes de la sous-classe. Cela me semble élégant d’une certaine manière, mais d’un autre côté, j’abuse un peu des annotations obsolètes comme moyen de "supprimer" des méthodes d’une classe.
Puisqu'une Sub1
instance est vraiment une base, il est logique d'utiliser l'héritage plutôt que l'encapsulation. Est-ce que je fais bien? Existe-t-il un meilleur moyen de résoudre ce problème?
la source
instanceof
, d'une manière qui nécessite beaucoup de dactylographie, est source d'erreurs et rend difficile l'ajout de plusieurs sous-classes.Sub1
etSub2
ne peuvent pas être utilisés de manière interchangeable, alors pourquoi les traitez-vous comme tels? Pourquoi ne pas garder une trace de vos "sandwich-makers" et de vos "alien-contacters" séparément?Réponses:
Cela n'a pas toujours de sens d'ajouter des fonctions à la classe de base, comme suggéré dans certaines des réponses. L'ajout d'un trop grand nombre de fonctions de cas particulier peut aboutir à la liaison de composants sans lien entre eux.
Par exemple, je pourrais avoir une
Animal
classe, avecCat
etDog
composants. Si je veux pouvoir les imprimer ou les afficher dans l'interface graphique, il pourrait être excessif pour moi d'ajouterrenderToGUI(...)
etsendToPrinter(...)
à la classe de base.L’approche que vous utilisez, à l’aide des vérifications de type et des transtypages, est fragile - mais au moins gardez les problèmes séparés.
Cependant, si vous vous retrouvez souvent confronté à ce type de vérifications / lancements, une option consiste à implémenter le modèle visiteur / double envoi correspondant. Cela ressemble un peu à ça:
Maintenant votre code devient
Le principal avantage est que si vous ajoutez une sous-classe, vous obtiendrez des erreurs de compilation plutôt que des cas manquants. La nouvelle classe de visiteurs devient également une cible intéressante pour intégrer des fonctions. Par exemple , il peut être judicieux de se déplacer
getRandomIngredients()
dansActOnBase
.Vous pouvez également extraire la logique de bouclage: par exemple, le fragment ci-dessus peut devenir
Un peu plus loin en massant et en utilisant les lambdas et la diffusion en continu de Java 8 vous permettraient de
Quelle OMI est à la fois aussi soignée et succincte que possible.
Voici un exemple plus complet de Java 8:
la source
Sub3
. Le visiteur n'a-t-il pas exactement le même problème queinstanceof
? Maintenant, vous devez ajouteronSub3
au visiteur et cela casse si vous oubliez.onSub3
l'interface de visiteur, vous obtenez une erreur de compilation à chaque endroit où vous créez un nouveau visiteur qui n'a pas encore été mis à jour. Au contraire, l'activation du type générera au mieux une erreur d'exécution - ce qui peut être plus difficile à déclencher.De mon point de vue: votre conception est fausse .
Traduit en langage naturel, vous dites ce qui suit:
Étant donné que nous avons
animals
, il y acats
etfish
.animals
avoir des propriétés communes àcats
etfish
. Mais cela ne suffit pas: il existe certaines propriétés qui se différencientcat
des autresfish
, vous devez donc créer une sous-classe.Maintenant, vous avez le problème, vous avez oublié de modéliser le mouvement . D'accord. C'est relativement facile:
Mais c'est une mauvaise conception. La bonne façon serait:
Où
move
serait un comportement partagé mis en œuvre différemment par chaque animal.Cela signifie que votre design est cassé.
Ma recommandation: refactor
Base
,Sub1
etSub2
.la source
.move()
ou.attack()
et bien abstraitethat
partie - au lieu d'avoir.swim()
,.fly()
,.slide()
,.hop()
,.jump()
,.squirm()
,.phone_home()
, etc. Comment refactoring-vous correctement? Inconnu sans meilleurs exemples ... mais toujours la bonne réponse - à moins que l'exemple de code et plus de détails ne suggèrent le contraire.move
vsfly
/run
/ etc, cela peut être expliqué en une phrase comme suit: Vous devez indiquer à l’objet quoi faire et non comment le faire. En termes d’accesseurs, comme vous le dites, c’est plus le cas réel dans un commentaire ici, vous devriez poser des questions à l’objet, pas en inspecter l’état.Il est un peu difficile d'imaginer une situation dans laquelle vous avez un groupe de choses et que vous voulez qu'elles fassent un sandwich ou contactent des extraterrestres. Dans la plupart des cas où vous trouvez un tel casting, vous utiliserez un type - par exemple, vous filtrez un ensemble de noeuds pour les déclarations où getAsFunction renvoie non-null, plutôt que de faire quelque chose de différent pour chaque noeud de la liste.
Il se peut que vous ayez besoin d'effectuer une séquence d'actions et qu'il ne soit pas pertinent que les objets effectuant l'action soient liés.
Donc, au lieu d'une liste de
Base
, travaillez sur la liste des actionsoù
Vous pouvez, le cas échéant, faire en sorte que Base implémente une méthode pour renvoyer l'action, ou tout autre moyen permettant de sélectionner l'action dans les occurrences de votre liste de base (puisque vous dites que vous ne pouvez pas utiliser le polymorphisme, donc supposément l'action à effectuer. n'est pas une fonction de la classe mais une autre propriété des bases, sinon vous donneriez à Base la méthode act (Context)
la source
Context
objet ne fait qu’aggraver les choses. Je pourrais aussi bien passerObject
aux méthodes, les exprimer et espérer qu’elles sont du bon type.Context
n’a pas besoin d’être une nouvelle classe, ni même une interface. Habituellement, le contexte n'est que l'appelant, les appels sont donc dans le formulaireaction.act(this)
. SigetFrequency()
etgetRandomIngredients()
sont des méthodes statiques, vous n'aurez peut-être même pas besoin d'un contexte. C’est ainsi que je mettrais en œuvre une file d’emploi, par exemple, qui résume votre problème.Et si vos sous-classes implémentaient une ou plusieurs interfaces définissant ce qu’elles peuvent faire? Quelque chose comme ça:
Ensuite, vos cours ressembleront à ceci:
Et votre boucle va devenir:
Deux avantages majeurs de cette approche:
PS: Désolé pour les noms des interfaces, je ne pouvais pas penser à quelque chose de plus cool à ce moment précis: D.
la source
L'approche peut être un bon dans les cas où presque tous les types au sein d' une famille , soit directement utilisable comme une mise en œuvre d' une certaine interface qui répond à certains critères ou peut être utilisé pour créer une implémentation de cette interface. Les types de collection intégrés auraient profité de ce modèle pour IMHO, mais comme ils ne le font pas à des fins d'exemple, je vais inventer une interface de collection
BunchOfThings<T>
.Certaines implémentations de
BunchOfThings
sont mutables; certains ne le sont pas. Dans de nombreux cas, un objet Fred peut vouloir contenir quelque chose qu'il peut utiliserBunchOfThings
et savoir que seul Fred pourra le modifier. Cette exigence peut être satisfaite de deux manières:Fred sait qu'il contient les seules références à cela
BunchOfThings
, et aucune référence extérieure àBunchOfThings
ses composants internes n'existe dans l'univers. Si personne d’autre n’a une référence àBunchOfThings
ou à ses éléments internes, personne ne pourra la modifier et la contrainte sera satisfaite.Ni le
BunchOfThings
, ni aucun de ses composants internes auxquels il existe des références externes, ne peuvent être modifiés par quelque moyen que ce soit. Si absolument personne ne peut modifier unBunchOfThings
, alors la contrainte sera satisfaite.Une façon de satisfaire la contrainte serait de copier inconditionnellement tout objet reçu (en traitant de manière récursive les composants imbriqués). Une autre solution serait de tester si un objet reçu promet l'immuabilité et, si ce n'est pas le cas, d'en faire une copie et de faire de même avec les composants imbriqués. Une alternative, qui est plus propre que la seconde et plus rapide que la première, consiste à proposer une
AsImmutable
méthode qui demande à un objet de faire une copie immuable de lui-même (en utilisantAsImmutable
sur tout composant imbriqué qui la prend en charge).Des méthodes associées peuvent également être fournies
asDetached
(à utiliser lorsque le code reçoit un objet et ne sait pas s'il souhaite le muter, auquel cas un objet mutable doit être remplacé par un nouvel objet mutable, mais un objet immuable peut être conservé telasMutable
quel ), (dans les cas où un objet sait qu'il conservera un objet renvoyé précédemmentasDetached
, c'est-à-dire soit une référence non partagée à un objet mutable, soit une référence partageable à un objet mutable), etasNewMutable
(dans les cas où le code reçoit une reference et sait qu’il va vouloir muter une copie des données qui y sont contenues - si les données entrantes sont mutables, il n’ya aucune raison de commencer par faire une copie immuable qui sera immédiatement utilisée pour créer une copie mutable puis abandonnée).Notez que, si les
asXX
méthodes peuvent renvoyer des types légèrement différents, leur véritable rôle est de s’assurer que les objets renvoyés répondent aux besoins du programme.la source
Ignorant la question de savoir si votre conception est bonne ou non, et si elle est bonne ou au moins acceptable, je voudrais examiner les capacités des sous-classes, pas le type.
Par conséquent, soit:
Déplacez des connaissances sur l'existence de sandwichs et d'aliens dans la classe de base, même si vous savez que certaines instances de la classe de base ne peuvent pas le faire. Implémentez-le dans la classe de base pour générer des exceptions et modifiez le code en:
Ensuite, une ou les deux sous-classes se substituent
canMakeASandwich()
et un seul implémente chacun des élémentsmakeASandwich()
andcontactAliens()
.Utilisez des interfaces, et non des sous-classes concrètes, pour détecter les capacités d'un type. Laissez la classe de base seule et modifiez le code en:
ou éventuellement (et n'hésitez pas à ignorer cette option si elle ne correspond pas à votre style, ou d'ailleurs à tout style Java que vous jugeriez judicieux):
Personnellement, je n'aime généralement pas ce dernier type d'attraper une exception à moitié attendue, à cause du risque d'attraper de façon inappropriée une
ClassCastException
sortie degetRandomIngredients
oumakeASandwich
, mais YMMV.la source
<
est d'éviter d'attraperArrayIndexOutOfBoundsException
, et certaines personnes décident de le faire aussi. Pas de prise en compte du goût ;-) En gros, mon "problème" est de travailler en Python, où le style préféré est qu'il est préférable de capturer une exception plutôt que de vérifier à l'avance si l'exception se produira, aussi simple que le test préalable soit. . Peut-être que la possibilité ne vaut tout simplement pas la peine d'être mentionnée en Java.Nous avons ici un cas intéressant de classe de base qui se réduit à ses propres classes dérivées. Nous savons très bien que c'est normalement mauvais, mais si nous voulons dire que nous avons trouvé une bonne raison, voyons quelles sont les contraintes qui pourraient en résulter:
Si 4 alors nous avons: 5. La politique de la classe dérivée est toujours sous le même contrôle politique que la politique de la classe de base.
2 et 5 impliquent directement que nous pouvons énumérer toutes les classes dérivées, ce qui signifie qu'il n'y a pas de classe dérivée en dehors.
Mais voici la chose. Si elles sont toutes à vous, vous pouvez remplacer le if par une abstraction qui est un appel de méthode virtuelle (même si cela n'a aucun sens) et vous débarrasser du if et de l'auto-casting. Par conséquent, ne le fais pas. Un meilleur design est disponible.
la source
Mettez une méthode abstraite dans la classe de base qui fait une chose dans Sub1 et l'autre dans Sub2, et appelez cette méthode abstraite dans vos boucles mixtes.
Notez que cela peut nécessiter d'autres modifications, en fonction de la définition des méthodes getRandomIngredients et getFrequency. Mais honnêtement, au sein de la classe qui contacte les extraterrestres est probablement le meilleur endroit pour définir getFrequency de toute façon.
Incidemment, vos méthodes asSub1 et asSub2 sont définitivement une mauvaise pratique. Si vous voulez faire cela, faites-le en faisant un casting. Il n'y a rien que ces méthodes vous donnent que le casting ne fait pas.
la source
Vous pouvez pousser les deux
makeASandwich
etcontactAliens
dans la classe de base, puis les implémenter avec des implémentations factices. Étant donné que les types / arguments de retour ne peuvent pas être résolus en une classe de base commune.Il y a des inconvénients évidents à ce genre de choses, comme ce que cela signifie pour le contrat de la méthode et ce que vous pouvez et ne pouvez pas déduire du contexte du résultat. Vous ne pouvez pas penser depuis que je n'ai pas réussi à faire un sandwich les ingrédients ont été utilisés dans la tentative, etc.
la source
Ce que vous faites est parfaitement légitime. Ne faites pas attention aux opposants qui ne font que réitérer le dogme parce qu'ils le lisent dans certains livres. Le dogme n'a pas sa place dans l'ingénierie.
J'ai utilisé le même mécanisme à plusieurs reprises, et je peux affirmer avec certitude que le runtime java aurait également pu faire la même chose à au moins un endroit auquel je peux penser, améliorant ainsi les performances, la convivialité et la lisibilité du code. l'utilise.
Prenons par exemple
java.lang.reflect.Member
, qui est la base dejava.lang.reflect.Field
etjava.lang.reflect.Method
. (La hiérarchie réelle est un peu plus compliquée que cela, mais ce n'est pas pertinent.) Les champs et les méthodes sont des animaux très différents: l'un a une valeur que vous pouvez obtenir ou définir, tandis que l'autre n'a rien de tel, mais il peut être invoqué avec un certain nombre de paramètres et il peut renvoyer une valeur. Ainsi, les champs et les méthodes sont tous les deux membres, mais les choses que vous pouvez faire avec eux sont aussi différents les uns des autres que de faire des sandwichs ou de contacter des extraterrestres.Maintenant, quand on écrit un code qui utilise la réflexion, on a très souvent un
Member
entre nos mains, et nous savons que c’est unMethod
ou unField
(ou, rarement, autre chose), et pourtant nous devons faire tout l’ennuiinstanceof
pour comprendre avec précision ce qu’il est et ensuite nous devons le lancer pour obtenir une référence appropriée à celui-ci. (Et ce n'est pas seulement fastidieux, mais cela ne fonctionne pas très bien non plus.) LaMethod
classe aurait pu très facilement implémenter le modèle que vous décrivez, facilitant ainsi la vie de milliers de programmeurs.Bien sûr, cette technique n’est viable que dans de petites hiérarchies bien définies de classes étroitement couplées dont vous avez (et aurez toujours) le contrôle au niveau source: vous ne voulez pas le faire si votre hiérarchie de classes est susceptible d’être étendu par des personnes qui ne sont pas libres de reformer la classe de base.
Voici ce que j'ai fait diffère de ce que vous avez fait:
La classe de base fournit une implémentation par défaut pour toute la
asDerivedClass()
famille de méthodes, chacune étant renvoyéenull
.Chaque classe dérivée remplace uniquement l'une des
asDerivedClass()
méthodes et renvoiethis
au lieu denull
. Il ne remplace aucun des autres, et ne veut rien savoir à leur sujet. Donc, pas deIllegalStateException
s sont jetés.La classe de base fournit également des
final
implémentations pour toute laisDerivedClass()
famille de méthodes, codée comme suit: Ainsireturn asDerivedClass() != null;
, le nombre de méthodes devant être remplacées par des classes dérivées est réduit au minimum.Je n'ai pas utilisé
@Deprecated
ce mécanisme parce que je n'y avais pas pensé. Maintenant que vous m'avez donné l'idée, je vais la mettre à profit, merci!C # a un mécanisme associé intégré via l'utilisation du
as
mot clé. En C #, vous pouvez direDerivedClass derivedInstance = baseInstance as DerivedClass
et vous obtiendrez une référence à unDerivedClass
si vous étiezbaseInstance
de cette classe, ounull
non. Cela (théoriquement) donne de meilleurs résultats que ceuxis
suivis par la conversion (is
est le mot-clé C # pourinstanceof
, bien sûr ), mais le mécanisme personnalisé que nous avons créé manuellement est encore plus performant: la paire d'instanceof
opérations de conversion et de conversion de Java, également. en tantas
qu'opérateur de C #, n'agissez pas aussi rapidement que l'appel de méthode virtuelle unique de notre approche personnalisée.J'avance par la présente la proposition selon laquelle cette technique devrait être déclarée être un motif et qu'il faudrait lui trouver un joli nom.
Gee, merci pour les votes négatifs!
Un résumé de la controverse, pour vous éviter la peine de lire les commentaires:
L'objection des gens semble être que la conception d'origine était fausse, ce qui signifie que vous ne devriez jamais avoir de classes très différentes dérivant d'une classe de base commune, ou que même si vous le faites, le code qui utilise une telle hiérarchie ne devrait jamais être dans la position d'avoir une référence de base et besoin de comprendre la classe dérivée. Par conséquent, disent-ils, le mécanisme d'auto-casting proposé par cette question et par ma réponse, qui améliore l'utilisation de la conception d'origine, n'aurait jamais dû être nécessaire. (Ils ne disent vraiment rien sur le mécanisme de l'auto-casting, ils se plaignent seulement de la nature des dessins auxquels le mécanisme est censé s'appliquer.)
Cependant, dans l'exemple ci - dessus j'ai déjà montré que les créateurs du moteur d' exécution Java ont en fait choisir la conception d'une précision telle la
java.lang.reflect.Member
,Field
, laMethod
hiérarchie, et dans les commentaires ci - dessous je aussi montrer que les créateurs du C # runtime sont arrivés indépendamment à un équivalent conception de laSystem.Reflection.MemberInfo
,FieldInfo
, laMethodInfo
hiérarchie. Il s’agit donc de deux scénarios réels différents, qui se présentent sous le nez de tout le monde et qui proposent des solutions manifestement exploitables en utilisant précisément ces conceptions.C'est ce que tous les commentaires suivants résument. Le mécanisme de l'auto-casting est à peine mentionné.
la source
java.lang.reflection.Member
hiérarchie. Les créateurs de Java Runtime se sont retrouvés dans ce type de situation, je ne suppose pas que vous diriez qu'ils se sont conçus eux-mêmes dans une boîte ou que vous les blâmez de ne pas suivre une sorte de meilleure pratique? Donc, ce que je veux dire ici, c'est qu'une fois que vous avez ce genre de situation, ce mécanisme est un bon moyen d'améliorer les choses.Member
interface, mais elle ne sert qu'à vérifier les qualificatifs d'accès. Généralement, vous obtenez une liste de méthodes ou de propriétés et vous l'utilisez directement (sans les mélanger, aucuneList<Member>
apparence n'apparaît), vous n'avez donc pas besoin de transtyper. Notez queClass
ne fournit pas degetMembers()
méthode. Bien sûr, un programmeur désemparés pourrait créerList<Member>
, mais cela ferait aussi peu de sens que ce qui en fait uneList<Object>
et en ajoutant certainsInteger
ouString
à elle.null
place d'une exception ne le rend pas beaucoup mieux. Toutes choses étant égales par ailleurs, le visiteur est plus en sécurité tout en faisant le même travail.