Les capacités d'un objet doivent-elles être identifiées exclusivement par les interfaces qu'il implémente?


36

L' isopérateur C # et l'opérateur Java instanceofvous 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.


10
Regardez vos deux exemples de code. Lequel est le plus lisible?
Robert Harvey

39
Juste mon avis, mais je choisirais le premier simplement parce que cela vous permet de choisir en toute sécurité le bon type. La seconde suppose que CanResetPasswordcela ne sera vrai que lorsque quelque chose sera mis en œuvre IResetsPassword. 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).
Becuzz

10
@nocomprende: xkcd.com/927
Robert Harvey

14
Le titre de votre question et le corps de la question demandent des choses différentes. Pour répondre au titre: idéalement, oui. Pour adresser le corps: Si vous vérifiez le type et transformez manuellement, vous n’exploitez probablement pas votre système de type correctement. Pouvez-vous nous dire plus précisément comment vous vous êtes retrouvé dans cette situation?
MetaFight

9
Cette question peut sembler simple, mais devient assez large quand vous commencez à y penser. Questions qui me viennent à l'esprit: Prévoyez-vous ajouter de nouveaux comportements aux comptes existants ou créer des types de comptes avec des comportements différents? Tous les types de comptes ont-ils des comportements similaires ou existe-t-il de grandes différences entre eux? L'exemple de code connaît-il tous les types de comptes ou simplement l'interface de base IAccount?
Euphoric

Réponses:


7

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:

  1. vous laissez le système de types fonctionner pour vous. Dans le premier exemple, si les isincendies se produisent, le downcast va définitivement fonctionner. Dans le second formulaire, vous devez vous assurer manuellement que seuls les objets qui implémentent IResetsPasswordrenvoient true pour cette propriété.
  2. classe de base fragile. Vous devez ajouter une propriété à la classe / interface de base pour chaque interface à ajouter. C'est lourd et sujet aux erreurs.

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.


Je préfère également beaucoup le typage compositionnel. +1 pour m'avoir fait comprendre que cet exemple particulier ne l'exploite pas correctement. Bien que, je dirais, le typage informatique (comme l'héritage traditionnel) est plus limité lorsque les capacités varient considérablement (par opposition à la manière dont elles sont mises en œuvre). D'où l'approche d'interfaçage, qui résout un problème différent du typage compositionnel ou de l'héritage standard.
TheCatWhisperer

vous pouvez simplifier le chèque comme ceci: (account as IResettable)?.ResetPassword();ouvar resettable = account as IResettable; if(resettable != null) {resettable.ResetPassword()} else {....}
Berin Loritsch

89

Les capacités d'un objet doivent-elles être identifiées exclusivement par les interfaces qu'il implémente?

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

if (account is IResetsPassword)
    ((IResetsPassword)account).ResetPassword();
else
    Print("Not allowed to reset password with this account type!");

ou

if (account.CanResetPassword)
    ((IResetsPassword)account).ResetPassword();
else
    Print("Not allowed to reset password with this account type!");

considérer

account.ResetPassword();

ou

account.ResetPassword(authority);

car accountsait 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.


54
Le polymorphisme ne fonctionne que lorsque je ne sais pas ou ne me soucie pas de quoi je parle. Donc, la façon dont je gère cette situation est de l’éviter soigneusement.
candied_orange

7
Si le client a vraiment besoin de savoir si la tentative a réussi, la méthode peut renvoyer un booléen. 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.
Radiodef

5
@DavidZ Nous parlons tous les deux anglais. Je peux donc vous dire de relire ce commentaire deux fois. Est-ce que cela signifie que vous êtes capable de le faire? Dois-je savoir si vous le pouvez avant de pouvoir vous dire de le faire?
candied_orange

5
@QPaysTaxes pour tout ce que vous savez, un gestionnaire d'erreurs a été passé accountlors 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.
candied_orange

6
@QPaysTaxes Cela pourrait être traité ailleurs et de différentes façons, il est superflu pour cette conversation et ne ferait que brouiller les cartes.
Tom.Bowen89

17

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 accountn’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?

account.getPasswordResetter().doAction();
account.getFormSubmitter().doAction(view.getContents());

AccountManager.resetPassword(account, account.getAccessToken());

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 ?

AccountManager.resetPassword(otherAccount, adminAccount.getAccessToken());

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? "


1
+1 pour la conception sent, même si vous n'avez pas utilisé la phrase.
Jared Smith

Qu'en est-il des cas où le mot de passe réinitialisé a des arguments complètement différents selon le type? ResetPassword (pswd) vs ResetPasswordToRandom ()
TheCatWhisperer

1
@TheCatWhisperer Le premier exemple est "changer le mot de passe", ce qui est une action différente. Le deuxième exemple est ce que je décris dans cette réponse.

Pourquoi ne pas account.getPasswordResetter().resetPassword();.
AnoE

1
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).
Voo

1

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 cela canResetPassword()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 passwordResetterchamp 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.


Tous les comptes ne peuvent pas avoir de mot de passe (du point de vue de ce système particulier). Par exemple, le compte peut être une tierce partie ... Google, Facebook, etc. Pour ne pas dire que ce n'est pas une bonne réponse!
TheCatWhisperer

0

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:

class Foo : IResetsPassword {
    //...
}

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:

class Foo {

    PasswordResetter passwordResetter;

}

Maintenant, au moment de l'exécution, vous pouvez vérifier si myFoo.passwordResetter != nullavant 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:

class Foo {
    //... foo stuff
}

class PasswordResetOperation {
    bool Execute(Foo foo) { ... }
}

class SendMailOperation {
    bool Execute(Foo foo) { ... }
}

//...and you follow this pattern for each new capability...

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:

class BaseAccount {
    //...
}
class GuestAccount : BaseAccount {
    //...
}
class UserAccount : BaseAccount, IMyPasswordReset, IEditPosts {
    //...
}
class AdminAccount : BaseAccount, IPasswordReset, IEditPosts, ISendMail {
    //...
}

//Capabilities

interface IMyPasswordReset {
    bool ResetPassword();
}

interface IPasswordReset {
    bool ResetPassword(UserAccount userAcc);
}

interface IEditPosts {
    bool EditPost(long postId, ...);
}

interface ISendMail {
    bool SendMail(string from, string to, ...);
}

Maintenant, je vais essayer d'analyser toutes les options mentionnées:

Deuxième exemple d'OP:

if (account.CanResetPassword)
       ((IResetsPassword)account).ResetPassword();
else
       Print("Not allowed to reset password with this account type!");

Disons que ce code reçoit une classe de compte de base (par exemple: BaseAccountdans 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:

if (account is IResetsPassword)
       ((IResetsPassword)account).ResetPassword();
else
       Print("Not allowed to reset password with this account type!");

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:

account.ResetPassword(authority);

Si cette ResetPasswordméthode est insérée en BaseAccountclasse, alors elle pollue également la classe de base avec un code inapproprié, comme dans le deuxième exemple de OP

Réponse de bonhomme de neige:

AccountManager.resetPassword(otherAccount, adminAccount.getAccessToken());

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 AccountManagerexemple, 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.

//Template method
class BaseAccountOperation {

    BaseAccount account;

    void Execute() {

        //... some processing

        TryResetPassword();

        //... some processing

        TrySendMail();

        //... some processing
    }

    void TryResetPassword() {
        Print("Not allowed to reset password with this account type!");
    }

    void TrySendMail() {
        Print("Not allowed to reset password with this account type!");
    }
}

class UserAccountOperation : BaseAccountOperation {

    UserAccount userAccount;

    void TryResetPassword() {
        account.ResetPassword(...);
    }

}

class AdminAccountOperation : BaseAccountOperation {

    AdminAccount adminAccount;

    override void TryResetPassword() {
        account.ResetPassword(...);
    }

    void TrySendMail() {
        account.SendMail(...);
    }
}

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 dynamicmots 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).


1
Est-ce que ça marcherait de toujours mettre en œuvre la méthode "Execute ()" mais parfois de ne rien faire? Il retourne un bool, donc je suppose que cela signifie qu'il l'a fait ou non. Cela renvoie le client: "J'ai essayé de réinitialiser le mot de passe, mais cela ne s'est pas produit (pour une raison quelconque), alors maintenant je devrais ..."

4
Avoir des méthodes « CANX () » et « Dox () » est un antimodèle: si une interface expose une méthode « dox () », il vaut mieux être en mesure de le faire même X si cela x signifie un no-op (par exemple modèle d'objet null ) Il ne devrait jamais incomber aux utilisateurs d’une interface de s’engager dans un couplage temporel (invoquer la méthode A avant la méthode B) car il est trop facile de se tromper et de casser des choses.

1
Avoir des interfaces n'a aucune incidence sur l'utilisation de la composition sur l'héritage. Ils représentent simplement une fonctionnalité disponible. La manière dont cette fonctionnalité entre en jeu est indépendante de l'utilisation de l'interface. Ce que vous avez fait ici est d'utiliser une implémentation ad-hoc d'une interface sans aucun des avantages.
Miyamoto Akira

Intéressant: la réponse la plus votée dit essentiellement ce que dit mon commentaire ci-dessus. Certains jours, vous avez juste de la chance.

@nocomprende - oui, j'ai mis à jour ma réponse en fonction de plusieurs commentaires. Merci pour les commentaires :)
Emerson Cardoso

0

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 #):

interface IAccount {
  bool CanResetPassword();

  void ResetPassword();

  // Other Account operations as needed
}

public class Resetable : IAccount {
  public bool CanResetPassword() {
    return true;
  }

  public void ResetPassword() {
    /* RESET PASSWORD */
  }
}

public class NotResetable : IAccount {
  public bool CanResetPassword() {
    return false;
  }

  public void ResetPassword() {
    Print("Not allowed to reset password with this account type!");}
  }

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.


Tous les comptes ne peuvent pas être réinitialisés. De plus, un compte pourrait-il avoir plus de fonctionnalités que d'être réinitialisable?
TheCatWhisperer

2
"Tous les comptes ne peuvent pas être réinitialisés." Pas sûr que cela ait été écrit avant l'édition. Deuxième classe est 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.
JimmyJames

1
Je pense que cette solution va dans la bonne direction, car elle offre une solution intéressante à l’intérieur de OO. Probablement changerait le nom du nom de l'interface en IPasswordReseter (ou quelque chose du genre), pour séparer les interfaces. IAccount peut être déclaré comme prenant en charge plusieurs autres interfaces. Cela vous donnerait la possibilité d'avoir des classes avec l'implémentation qui est ensuite composée et utilisée par délégation sur les objets de domaine. @JimmyJames, mineur à la carte, sur C #, les deux classes devront spécifier qu'elles implémentent IAccount. Sinon, le code a l'air un peu étrange ;-)
Miyamoto Akira

1
Consultez également un commentaire de Snowman dans l’une des réponses à propos de CanX () et DoX (). Ce n'est pas toujours un anti-modèle, comme il l'a fait (par exemple sur le comportement de l'interface utilisateur)
Miyamoto Akira

1
Cette réponse commence bien: c’est mauvais. Malheureusement, votre solution est tout aussi mauvaise car elle subvertit également le système de types mais génère une exception. Une bonne solution semble différente: ne pas avoir de méthodes non implémentées sur un type. Le framework .NET utilise votre approche pour certains types, mais c’est une erreur sans équivoque.
Konrad Rudolph

0

Répondre

Ma réponse légèrement oppressée à votre question est la suivante:

Les capacités d'un objet doivent être identifiées par elles-mêmes et non par la mise en oeuvre d'une interface. Un langage statiquement fortement typé limitera toutefois cette possibilité (par conception).

Addenda:

Ramifier sur le type d'objet est diabolique.

Randonnée

Je vois que vous n'avez pas ajouté la c#balise, mais que vous posez la question dans un object-orientedcontexte 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_missingméthode générique, fourre-tout , qui rend cela encore plus flexible.

Ainsi, dans un tel langage (Ruby, par exemple), votre code devient:

account.reset_password

Période.

Le accountsera 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:

account.reset_password  if account.can? :reset_password

(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 ...


Super d'avoir votre point de vue ici! Nous, du côté java / C #, avons tendance à oublier qu’il existe une autre branche de la programmation orientée objet. Quand je le dis à mes collègues, JS (pour tous ses problèmes) est, à bien des égards, plus de OO que de C #, ils se moquent de moi!
TheCatWhisperer

J'adore le langage C # et je pense que c'est un bon langage généraliste , mais c'est mon opinion personnelle que, pour ce qui est de OO, il est assez pauvre. Dans les deux cas, cela tend à encourager la programmation procédurale.
TheCatWhisperer

2
On peut soutenir que tester l'existence d'une méthode dans un langage de type canard est identique à celui de l'implémentation d'une interface dans une interface de type statique: vous effectuez une vérification au moment de l'exécution pour déterminer si l'objet possède une capacité particulière. Chaque déclaration de méthode est en réalité sa propre interface, identifiée uniquement par le nom de la méthode, et son existence ne peut être utilisée pour déduire d'autres informations sur l'objet.
IMSoP

0

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 CanResetPasswordclause représente une exigence commerciale fondamentale qui fait partie de l'abstraction d'un compte, votre deuxième option est OK.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.