L' is
opérateur C # et l'opérateur Java instanceof
vous permettent de créer une branche sur l'interface (ou, plus simplement, sa classe de base) implémentée par une instance d'objet.
Est-il approprié d'utiliser cette fonctionnalité pour les branches de haut niveau en fonction des fonctionnalités fournies par une interface?
Ou bien une classe de base devrait-elle fournir des variables booléennes pour fournir une interface décrivant les capacités d’un objet?
Exemple:
if (account is IResetsPassword)
((IResetsPassword)account).ResetPassword();
else
Print("Not allowed to reset password with this account type!");
contre.
if (account.CanResetPassword)
((IResetsPassword)account).ResetPassword();
else
Print("Not allowed to reset password with this account type!");
Existe-t-il des pièges pour utiliser l'implémentation d'interface pour l'identification des capacités?
Cet exemple était juste cela, un exemple. Je m'interroge sur une application plus générale.
object-oriented
TheCatWhisperer
la source
la source
CanResetPassword
cela ne sera vrai que lorsque quelque chose sera mis en œuvreIResetsPassword
. Vous êtes maintenant en train de faire déterminer par une propriété quelque chose que le système de type doit contrôler (et ultimement lorsque vous préformerez la distribution).Réponses:
Avec l'avertissement qu'il est préférable d'éviter si possible le downcasting, la première forme est préférable pour deux raisons:
is
incendies se produisent, le downcast va définitivement fonctionner. Dans le second formulaire, vous devez vous assurer manuellement que seuls les objets qui implémententIResetsPassword
renvoient true pour cette propriété.Cela ne veut pas dire que vous ne pouvez pas améliorer quelque peu ces problèmes. Vous pouvez, par exemple, faire en sorte que la classe de base dispose d'un ensemble d'interfaces publiées dans lesquelles vous pouvez vérifier l'inclusion. Toutefois, vous n'implémentez que manuellement une partie du système de types existant qui est redondante.
BTW ma préférence naturelle est la composition sur l'héritage, mais surtout en ce qui concerne l'état. L'héritage d'interface n'est généralement pas aussi grave. En outre, si vous vous trouvez en train d'implémenter la hiérarchie de types d'un homme pauvre, vous feriez mieux d'utiliser celle qui existe déjà car le compilateur vous aidera.
la source
(account as IResettable)?.ResetPassword();
ouvar resettable = account as IResettable; if(resettable != null) {resettable.ResetPassword()} else {....}
Les capacités d'un objet ne doivent pas être identifiées du tout.
Le client utilisant un objet ne devrait pas être obligé de savoir comment il fonctionne. Le client ne doit savoir que les choses qu'il peut dire à l'objet à faire. Ce que fait l'objet, une fois qu'il a été annoncé, n'est pas le problème du client.
Donc plutôt que
ou
considérer
ou
car
account
sait déjà si cela fonctionnera. Pourquoi le demander? Dites-lui simplement ce que vous voulez et laissez-le faire ce qu'il va faire.Imaginez que cela soit fait de manière à ce que le client ne se soucie pas de savoir si cela a fonctionné ou non, parce que c'est quelque chose d'autre. Le travail des clients consistait uniquement à tenter le coup. C'est fait maintenant et il y a d'autres choses à gérer. Ce style a beaucoup de noms mais le nom que j’aime le plus est celui de dire, ne demandez pas .
Lorsque vous écrivez au client, il est très tentant de penser que vous devez tout garder en mémoire et ainsi tirer vers vous tout ce que vous pensez avoir besoin de savoir. Lorsque vous faites cela, vous retournez les objets. Valorisez votre ignorance. Repoussez les détails et laissez les objets les traiter. Moins vous en savez, mieux c'est.
la source
if (!account.AttemptPasswordReset()) /* attempt failed */;
De cette façon, le client connaît le résultat mais évite certains des problèmes liés à la logique de décision dans la question. Si la tentative est asynchrone, vous passez un rappel.account
lors de sa construction. Des pop-ups, des enregistrements, des impressions et des alertes audio peuvent se produire à ce stade. Le travail du client est de dire "fais-le maintenant". Il n'a pas à faire plus que cela. Vous n'êtes pas obligé de tout faire au même endroit.Contexte
L'héritage est un outil puissant qui sert à la programmation orientée objet. Cependant, tous les problèmes ne sont pas résolus avec élégance: parfois, d'autres solutions sont meilleures.
Si vous repensez à vos premiers cours d'informatique (en supposant que vous ayez un diplôme en informatique), vous vous souviendrez peut-être qu'un professeur vous a donné un paragraphe indiquant ce que le client souhaite que le logiciel fasse. Votre travail consiste à lire le paragraphe, à identifier les acteurs et les actions, puis à vous donner un aperçu approximatif des classes et des méthodes. Il y aura des brutes qui semblent importantes, mais qui ne le sont pas. Il est également très possible que les exigences soient mal interprétées.
Même les plus expérimentés se trompent: c’est une compétence importante: identifier correctement les exigences et les traduire en langues machine.
Ta question
D'après ce que vous avez écrit, je pense que vous avez peut-être mal compris le cas général des actions facultatives que les classes peuvent effectuer. Oui, je sais que votre code n'est qu'un exemple et que le cas général vous intéresse. Cependant, il semble que vous souhaitiez savoir comment gérer la situation dans laquelle certains sous - types d'un objet peuvent effectuer une action, alors que d'autres ne le peuvent pas.
Ce
account
n’est pas parce qu’un objet tel qu’un a un type de compte que cela se traduit par un type dans un langage OO. "Taper" dans un langage humain ne signifie pas toujours "classe". Dans le contexte d'un compte, "type" peut être plus en corrélation avec "jeu d'autorisations". Vous souhaitez utiliser un compte d'utilisateur pour effectuer une action, mais cette action peut ou non être exécutée par ce compte. Plutôt que d'utiliser l'héritage, j'utiliserais un délégué ou un jeton de sécurité.Ma solution
Considérons une classe de compte pouvant effectuer plusieurs actions facultatives. Au lieu de définir "peut effectuer l'action X" via l'héritage, pourquoi ne pas demander au compte de renvoyer un objet délégué (mot de passe, demandeur, formulaire, etc.) ou un jeton d'accès?
L'avantage de la dernière option est ce que je veux si je veux utiliser les informations d'identification de mon compte pour réinitialiser le mot de passe de quelqu'un d' autre ?
Non seulement le système est plus flexible, non seulement j'ai supprimé les transtypages, mais le design est plus expressif . Je peux lire ceci et comprendre facilement ce qu'il fait et comment il peut être utilisé.
TL; DR: cela se lit comme un problème XY . En général, face à deux options sous-optimales, il est utile de prendre du recul et de réfléchir à «Qu'est-ce que j'essaie vraiment d'accomplir ici? Devrais-je vraiment réfléchir à la manière de rendre la conversion de texte moins laide ou de chercher des moyens de enlever le typecast entièrement? "
la source
account.getPasswordResetter().resetPassword();
.The benefit to the last option there is what if I want to use my account credentials to reset someone else's password?
.. facile. Vous créez un objet de domaine Compte pour l'autre compte et utilisez celui-ci. Les classes statiques sont problématiques pour les tests unitaires (cette méthode nécessitant par définition un accès à un espace de stockage, vous ne pouvez donc pas tester facilement à l'unité les codes qui touchent cette classe) et il est préférable de les éviter. La possibilité de retourner une autre interface est souvent une bonne idée mais ne traite pas vraiment le problème sous-jacent (vous venez de déplacer le problème vers une autre interface).Bill Venner dit que votre approche est absolument bonne ; Passez à la section intitulée "Quand utiliser instanceof". Vous descendez à un type / interface spécifique qui implémente uniquement ce comportement spécifique, vous avez donc parfaitement raison de faire preuve de sélectivité à ce sujet.
Toutefois, si vous souhaitez utiliser une autre approche, il existe de nombreuses façons de raser un yak.
Il y a l'approche du polymorphisme; vous pourriez faire valoir que tous les comptes ont un mot de passe; par conséquent, tous les comptes devraient au moins pouvoir tenter de réinitialiser un mot de passe. Cela signifie que, dans la classe de compte de base, nous devrions avoir une
resetPassword()
méthode que tous les comptes implémentent, mais à leur manière. La question est de savoir comment cette méthode devrait se comporter lorsque la capacité n'existe pas.Il peut retourner un vide et terminer en silence s'il réinitialise le mot de passe ou non, en prenant éventuellement la responsabilité d'imprimer le message en interne s'il ne réinitialise pas le mot de passe. Pas la meilleure idée.
Il pourrait retourner un booléen indiquant si la réinitialisation a réussi. En activant ce booléen, nous pourrions relier que la réinitialisation du mot de passe a échoué.
Il pourrait retourner une chaîne indiquant le résultat de la tentative de réinitialisation du mot de passe. La chaîne pourrait donner plus de détails sur la raison pour laquelle une réinitialisation a échoué et pourrait être sortie.
Il pourrait renvoyer un objet ResetResult contenant plus de détails et combinant tous les éléments de retour précédents.
Cela pourrait retourner un vide et renvoyer une exception si vous essayez de réinitialiser un compte qui n'a pas cette capacité (ne le faites pas, car la gestion des exceptions pour le contrôle de flux normal est une mauvaise pratique pour diverses raisons).
Avoir une
canResetPassword()
méthode de correspondance peut ne pas sembler être la pire des choses au monde, car théoriquement, il s'agit d'une fonctionnalité statique intégrée à la classe au moment de son écriture. Ceci est un indice de la raison pour laquelle l’approche méthode est une mauvaise idée, car cela suggère que la capacité est dynamique et que celacanResetPassword()
pourrait changer, ce qui crée également la possibilité supplémentaire de changer entre demander l’autorisation et passer l’appel. Comme mentionné ailleurs, dites plutôt que de demander la permission.La composition sur l'héritage peut être une option: vous pouvez avoir un
passwordResetter
champ final (ou un getter équivalent) et des classes équivalentes pour lesquelles vous pouvez rechercher la valeur null avant de l'appeler. Bien que cela ressemble un peu à demander une permission, la finalité pourrait éviter toute nature dynamique inférée.Vous pourriez penser à externaliser la fonctionnalité dans sa propre classe, ce qui pourrait prendre un compte en paramètre et agir dessus (par exemple
resetter.reset(account)
), bien que cela soit aussi souvent une mauvaise pratique (une analogie courante est un commerçant cherchant de l'argent dans son portefeuille).Dans les langues disposant de cette fonctionnalité, vous pouvez utiliser des mixins ou des traits. Cependant, vous vous retrouvez à la position où vous avez commencé, où vous pouvez vérifier l'existence de ces fonctionnalités.
la source
Pour cet exemple spécifique du " peut réinitialiser un mot de passe ", je vous recommande d'utiliser la composition plutôt que l'héritage (dans ce cas, l'héritage d'une interface / contrat). Parce que, en faisant cela:
Vous spécifiez immédiatement (au moment de la compilation) que votre classe " peut réinitialiser un mot de passe ". Mais, si dans votre scénario, la présence de la capacité est conditionnelle et dépend d'autres éléments, vous ne pouvez plus spécifier d'éléments au moment de la compilation. Ensuite, je suggère de faire ceci:
Maintenant, au moment de l'exécution, vous pouvez vérifier si
myFoo.passwordResetter != null
avant de faire cette opération. Si vous souhaitez découpler encore plus de choses (et que vous envisagez d'ajouter beaucoup plus de fonctionnalités), vous pouvez:MISE À JOUR
Après avoir lu d’autres réponses et commentaires de OP, j’ai bien compris que la question ne concernait pas la solution compositionnelle. Je pense donc que la question est de savoir comment mieux identifier les capacités des objets en général, dans un scénario comme celui-ci:
Maintenant, je vais essayer d'analyser toutes les options mentionnées:
Deuxième exemple d'OP:
Disons que ce code reçoit une classe de compte de base (par exemple:
BaseAccount
dans mon exemple); c'est dommage car cela insère des booléens dans la classe de base, la polluant avec un code qui n'a aucun sens d'être là.OP premier exemple:
Pour répondre à la question, cette option est plus appropriée que l’option précédente, mais en fonction de l’implémentation, elle enfreindra le principe de solide, et des vérifications telles que celles-ci seraient réparties dans le code et rendraient la maintenance ultérieure plus difficile.
Ansie de CandiedOrange:
Si cette
ResetPassword
méthode est insérée enBaseAccount
classe, alors elle pollue également la classe de base avec un code inapproprié, comme dans le deuxième exemple de OPRéponse de bonhomme de neige:
C'est une bonne solution, mais elle considère que les fonctionnalités sont dynamiques (et peuvent évoluer dans le temps). Cependant, après avoir lu plusieurs commentaires de OP, je suppose que nous parlons ici de polymorphisme et de classes statiquement définies (bien que l’exemple de comptes indique intuitivement le scénario dynamique). EG: dans cet
AccountManager
exemple, la vérification de l'autorisation serait une requête à la base de données; dans la question OP, les vérifications sont des tentatives de lancer les objets.Une autre suggestion de moi:
Utilisez le modèle de méthode de modèle pour les ramifications de haut niveau. La hiérarchie de classe mentionnée est conservée telle quelle; nous ne créons que des gestionnaires plus appropriés pour les objets, afin d'éviter les conversions et les propriétés / méthodes inappropriées qui polluent la classe de base.
Vous pouvez lier l'opération à la classe de compte approprié à l'aide d' un dictionnaire / Hashtable, ou effectuer des opérations en temps exécuter à l' aide des méthodes d'extension, l' utilisation de
dynamic
mots clés, ou comme la dernière utilisation de l' option une seule coulée afin de passer l'objet de compte à l'opération (en dans ce cas, le nombre de lancers est un seul, au début de l’opération).la source
Aucune des options que vous avez présentées n'est bonne OO. Si vous écrivez si des déclarations sur le type d'un objet, vous faites probablement une erreur OO (il y a des exceptions, ce n'est pas une.) Voici la réponse OO simple à votre question (peut ne pas être valide C #):
J'ai modifié cet exemple pour correspondre à ce que le code original faisait. Sur la base de certains commentaires, il semble que les gens se demandent si c'est le code «correct» spécifique ici. Ce n'est pas le but de cet exemple. La surcharge polymorphe entière consiste essentiellement à exécuter de manière conditionnelle différentes implémentations de la logique en fonction du type de l'objet. Ce que vous faites dans les deux exemples, c'est de brouiller à la main ce que votre langue vous donne comme caractéristique. En un mot, vous pouvez supprimer les sous-types et définir la possibilité de réinitialisation en tant que propriété booléenne du type de compte (en ignorant les autres fonctionnalités des sous-types.)
Sans une vue plus large de la conception, il est impossible de dire s'il s'agit d'une bonne solution pour votre système particulier. C'est simple et si cela fonctionne pour ce que vous faites, vous n'aurez probablement jamais besoin d'y penser à moins que quelqu'un ne vérifie pas CanResetPassword () avant d'appeler ResetPassword (). Vous pouvez également renvoyer un booléen ou échouer en silence (non recommandé). Cela dépend vraiment des spécificités de la conception.
la source
NotResetable
. "un compte pourrait avoir plus de fonctionnalités que d'être réinitialisable", je suppose, mais cela ne semble pas être important pour la question à l'examen.Répondre
Ma réponse légèrement oppressée à votre question est la suivante:
Addenda:
Randonnée
Je vois que vous n'avez pas ajouté la
c#
balise, mais que vous posez la question dans unobject-oriented
contexte général .Ce que vous décrivez est une technicité des langages statiques / forts, et non spécifique à "OO" en général. Votre problème provient, entre autres, du fait que vous ne pouvez pas appeler des méthodes dans ces langages sans avoir un type suffisamment étroit au moment de la compilation (via une définition de variable, une définition de paramètre de méthode / valeur de retour ou une conversion explicite). J'ai déjà utilisé vos deux variantes par le passé, dans ce type de langage, et les deux me paraissent laides ces temps-ci.
Dans les langages OO à typage dynamique, ce problème ne survient pas en raison d'un concept appelé "typage de canard". En bref, cela signifie que vous ne déclarez pas le type d'un objet où que ce soit (sauf lors de sa création). Vous envoyez des messages à des objets et les objets gèrent ces messages. Vous ne vous souciez pas de savoir si l'objet a réellement une méthode de ce nom. Certaines langues ont même une
method_missing
méthode générique, fourre-tout , qui rend cela encore plus flexible.Ainsi, dans un tel langage (Ruby, par exemple), votre code devient:
Période.
Le
account
sera soit réinitialiser le mot de passe, soit lancer une exception "refusée", soit lancer une exception "ne sait pas comment gérer 'réinitialisation" mot de passe ".Si vous avez besoin de créer une branche de manière explicite pour une raison quelconque, vous le feriez quand même sur la capacité, pas sur la classe:
(Où
can?
est juste une méthode qui vérifie si l'objet peut exécuter une méthode sans l'appeler.)Notez que même si ma préférence personnelle concerne actuellement les langues à typage dynamique, il ne s'agit que d'une préférence personnelle. Les langues typées statiquement ont aussi leur utilité, alors s'il vous plaît, ne considérez pas cela comme une farce contre les langues statiquement typées ...
la source
Il semble que vos options proviennent de différentes forces de motivation. Le premier semble être un bidouillage employé en raison d'une mauvaise modélisation d'objet. Cela a démontré que vos abstractions sont fausses, car elles brisent le polymorphisme. Plus vraisemblablement qu'autrement, cela rompt avec le principe Open open , qui permet un couplage plus étroit.
Votre deuxième option pourrait convenir du point de vue de la programmation orientée objet, mais le transtypage me confond. Si votre compte vous permet de réinitialiser son mot de passe, pourquoi avez-vous besoin d'une conversion de type? Donc, en supposant que votre
CanResetPassword
clause représente une exigence commerciale fondamentale qui fait partie de l'abstraction d'un compte, votre deuxième option est OK.la source