“Programmer une interface” en utilisant des méthodes d’extension: Quand est-ce que ça va trop loin?

Contexte: Dans l’esprit du “programme pour une interface, pas une implémentation” et des classes de type Haskell , et en tant qu’expérience de codage, je réfléchis à ce que signifierait la création d’une API principalement fondée sur la combinaison d’interfaces et d’extension méthodes. J’ai deux lignes direcsortingces en tête:

  1. Évitez l’inheritance de classe autant que possible. Les interfaces doivent être implémentées en tant que sealed class .
    (Ceci est dû à deux raisons: premièrement, parce que la sous-classification soulève de vives questions sur la manière de spécifier et d’appliquer le contrat de la classe de base dans ses classes dérivées. Deuxièmement, et c’est l’influence de la classe de type Haskell, le polymorphism n’exige pas de sous-classe.)

  2. Évitez les méthodes d’instance chaque fois que possible. Si cela peut être fait avec des méthodes d’extension, celles-ci sont préférées.
    (Ceci est destiné à aider à garder les interfaces compactes: tout ce qui peut être fait en combinant d’autres méthodes d’instance devient une méthode d’extension. Ce qui rest dans l’interface est une fonctionnalité principale, et notamment des méthodes de changement d’état.)

Problème: J’ai des problèmes avec la deuxième directive. Considère ceci:

 interface IApple { } static void Eat(this IApple apple) { Console.WriteLine("Yummy, that was good!"); } interface IRottenApple : IApple { } static void Eat(this IRottenApple apple) { Console.WriteLine("Eat it yourself, you disgusting human, you!"); } sealed class RottenApple : IRottenApple { } IApple apple = new RottenApple(); // API user might expect virtual dispatch to happen (as usual) when 'Eat' is called: apple.Eat(); // ==> "Yummy, that was good!" 

De toute évidence, pour le résultat attendu ( "Eat it yourself…" ), Eat devrait être une méthode d’instance régulière.

Question: Quelle serait une ligne direcsortingce affinée / plus précise sur l’utilisation des méthodes d’extension par rapport aux méthodes d’instance (virtuelle)? Quand l’utilisation de méthodes d’extension pour “programmer à une interface” va-t-elle trop loin? Dans quels cas les méthodes d’instance sont-elles réellement requirejses?

Je ne sais pas s’il existe une règle claire et générale, je ne m’attends donc pas à une réponse parfaite et universelle. Toute amélioration bien argumentée de la directive (2) ci-dessus est appréciée.

    Votre ligne direcsortingce est assez bonne telle quelle: elle dit déjà “dans la mesure du possible”. La tâche consiste donc à préciser le “dans la mesure du possible” de manière plus détaillée.

    J’utilise cette simple dichotomie: si l’ajout d’une méthode a pour but de masquer les différences entre les sous-classes, utilisez une méthode d’extension. si le but est de mettre en évidence les différences, utilisez une méthode virtuelle.

    Votre méthode Eat est un exemple de méthode qui introduit une différence entre les sous-classes: le processus de manger (ou non) une pomme dépend de quel type de pomme il s’agit. Par conséquent, vous devez l’implémenter en tant que méthode d’instance.

    Un exemple de méthode qui essaie de cacher les différences serait ThrowAway :

     public static void ThrowAway(this IApple apple) { var theBin = RecycleBins.FindCompostBin(); if (theBin != null) { theBin.Accept(apple); return; } apple.CutUp(); RecycleBins.FindGarbage().Accept(apple); } 

    Si le processus d’élimination d’une pomme est le même quel que soit le type de pomme, l’opération est le candidat idéal pour être implémenté dans une méthode d’extension.

    Pour moi, le résultat attendu était correct. Vous avez typescript (probablement en utilisant cette erreur) la variable sous la forme d’un IApple.

    Par exemple:

     IApple apple = new RottenApple(); apple.Eat(); // "Yummy, that was good!" IRottenApple apple2 = new RottenApple(); apple2.Eat(); // "Eat it yourself, you disgusting human, you!" var apple3 = new RottenApple(); apple.Eat(); // "Eat it yourself, you disgusting human, you!" 

    Question: Quelle serait une ligne direcsortingce affinée / plus précise sur l’utilisation des méthodes d’extension par rapport aux méthodes d’instance (virtuelle)? Quand l’utilisation de méthodes d’extension pour “programmer à une interface” va-t-elle trop loin? Dans quels cas les méthodes d’instance sont-elles réellement requirejses?

    Juste mon opinion personnelle lors du développement d’une application:

    J’utilise des méthodes d’instance lorsque j’écris quelque chose que je peux ou que quelqu’un d’autre peut consumr. C’est parce que c’est une exigence pour ce que le type est réellement. Considérons une interface / classe FlyingObject avec une méthode Fly() . C’est une méthode fondamentale de base d’un object volant. Créer une méthode d’extension n’a vraiment aucun sens.

    J’utilise beaucoup de méthodes d’extension, mais celles-ci ne sont jamais indispensables à l’utilisation de la classe qu’elles étendent. Par exemple, j’ai une méthode d’extension sur int qui crée un SqlParameter (en plus, il est interne). Néanmoins, cela n’a aucun sens de faire de cette méthode une partie de la classe de base de int, cela n’a vraiment rien à voir avec ce que fait ou fait un int. La méthode d’extension est un moyen visuellement agréable de créer une méthode réutilisable qui utilise une classe / structure.

    J’ai remarqué que les méthodes d’extension C # peuvent être assez similaires aux fonctions non membres non membres non membres de C ++ de la manière suivante: Scott Meyers et Herb Sutter affirment qu’en encapsulation C ++, l’encapsulation est parfois augmentée si une fonction n’est pas membre de la classe:

    “Si possible, préférez les fonctions d’écriture en tant que non-amis non membres.” – Résumé du GotW # 84 de Herb Sutter

    (Sutter justifie cette approche dans son article sur le principe d’interface .)

    En 1991, Scott Meyers avait même mis au point un algorithme permettant de décider si une fonction devait être une fonction membre, une fonction amie ou une fonction non membre non membre:

    if ( f doit être virtuel )
    faire de f une fonction membre de C ;
    sinon si ( f est operator>> ou operator<< )
    faire de f une fonction non membre ;
    if ( f besoin d'accéder à des membres non-publics de C )
    faire de f un ami de C ;
    else if ( f besoin de conversions de types sur son argument le plus à gauche )
    faire de f une fonction non membre ;
    if ( f besoin d'accéder à des membres non-publics de C )
    faire de f un ami de C ;
    else if ( f peut être implémenté via l'interface publique de C )
    faire de f une fonction non membre ;
    autre
    faire de f une fonction membre de C ;

    - algorithme développé par Scott Meyers à partir de 1998 (reformaté)

    Une partie de celui-ci est évidemment spécifique au C ++, mais il devrait être assez facile de trouver un algorithme analogue pour le langage C #. (Pour commencer, un friend peut être approché en C # avec internal modificateur d'access internal ; les "fonctions non membres" peuvent être des méthodes d'extension ou d'autres méthodes statiques.)

    Ce que cet algorithme ne dit pas, c'est quand, ou pourquoi, f "doit être virtuel". La réponse de @ dasblinkenlight explique en grande partie ce point.


    Questions pertinentes sur le dépassement de capacité de la stack:

    • Utilisation à grande échelle des conseils de Meyer pour préférer les fonctions non membres, non amis?

    • Quand les fonctions doivent-elles être des fonctions membres?