Dois-je utiliser des méthodes abstraites ou virtuelles?

11

Si nous supposons qu'il n'est pas souhaitable que la classe de base soit une classe d'interface pure, et en utilisant les 2 exemples ci-dessous, quelle est la meilleure approche, en utilisant la définition de classe de méthode abstraite ou virtuelle?

  • L'avantage de la version "abstraite" est qu'elle est probablement plus propre et force la classe dérivée à donner une implémentation, espérons-le, significative.

  • L'avantage de la version "virtuelle" est qu'elle peut être facilement tirée par d'autres modules et utilisée pour les tests sans ajouter un tas de framework sous-jacent comme la version abstraite l'exige.

Version abstraite:

public abstract class AbstractVersion
{
    public abstract ReturnType Method1();        
    public abstract ReturnType Method2();
             .
             .
    public abstract ReturnType MethodN();

    //////////////////////////////////////////////
    // Other class implementation stuff is here
    //////////////////////////////////////////////
}

Version virtuelle:

public class VirtualVersion
{
    public virtual ReturnType Method1()
    {
        return ReturnType.NotImplemented;
    }

    public virtual ReturnType Method2()
    {
        return ReturnType.NotImplemented;
    }
             .
             .
    public virtual ReturnType MethodN()
    {
        return ReturnType.NotImplemented;
    }

    //////////////////////////////////////////////
    // Other class implementation stuff is here
    //////////////////////////////////////////////
}
Tremper
la source
Pourquoi supposons-nous qu'une interface n'est pas souhaitable?
Anthony Pegram
Sans problème partiel, il est difficile de dire que l'un est meilleur que l'autre.
Codism
@Anthony: Une interface n'est pas souhaitable car il existe des fonctionnalités utiles qui iront également dans cette classe.
Dunk
4
return ReturnType.NotImplemented? Sérieusement? Si vous ne pouvez pas rejeter le type non implémenté au moment de la compilation (vous pouvez; utiliser des méthodes abstraites) au moins lever une exception.
Jan Hudec
3
@Dunk: Ici, ils sont nécessaires. Les valeurs de retour ne seront pas contrôlées.
Jan Hudec

Réponses:

15

Mon vote, si je consommais vos affaires, serait pour les méthodes abstraites. Cela va de pair avec «échouer tôt». Il peut être difficile au moment de la déclaration d'ajouter toutes les méthodes (bien que tout outil de refactorisation décent le fasse rapidement), mais au moins je sais quel est le problème immédiatement et le corrige. Je préfère cela plutôt que de déboguer 6 mois et 12 personnes de changements plus tard pour voir pourquoi nous obtenons soudainement une exception non implémentée.

Erik Dietrich
la source
Bon point concernant l'obtention inattendue de l'erreur NotImplemented. Ce qui est un plus du côté abstrait, car vous obtiendrez une erreur de compilation au lieu d'une erreur d'exécution.
Dunk
3
+1 - En plus d'échouer tôt, les héritiers peuvent voir immédiatement les méthodes dont ils ont besoin pour implémenter via "Implémenter la classe abstraite" plutôt que de faire ce qu'ils pensaient être suffisant, puis échouer lors de l'exécution.
Telastyn
J'ai accepté cette réponse car l'échec au moment de la compilation était un avantage que je n'ai pas répertorié et en raison de la référence à l'utilisation de l'EDI pour implémenter automatiquement les méthodes rapidement.
Dunk
25

La version virtuelle est à la fois sujette aux bogues et sémantiquement incorrecte.

Le résumé dit "cette méthode n'est pas implémentée ici. Vous devez l'implémenter pour que cette classe fonctionne"

Virtual dit "J'ai une implémentation par défaut mais vous pouvez me changer si vous en avez besoin"

Si votre objectif ultime est la testabilité, les interfaces sont normalement la meilleure option. (cette classe fait x plutôt que cette classe est ax). Vous devrez peut-être diviser vos classes en composants plus petits pour que cela fonctionne correctement.

Tom Squires
la source
3

Cela dépend de l'utilisation de votre classe.

Si les méthodes ont une implémentation «vide» raisonnable, vous avez beaucoup de méthodes et vous en remplacez souvent quelques-unes, alors utiliser des virtualméthodes est logique. Par exemple, ExpressionVisitorest implémenté de cette façon.

Sinon, je pense que vous devriez utiliser des abstractméthodes.

Idéalement, vous ne devriez pas avoir de méthodes qui ne sont pas implémentées, mais dans certains cas, c'est la meilleure approche. Mais si vous décidez de le faire, ces méthodes devraient lancer NotImplementedException, et non retourner une valeur spéciale.

svick
la source
Je voudrais noter que "NotImplementedException" indique souvent une erreur d'omission, tandis que "NotSupportedException" indique un choix manifeste. A part ça, je suis d'accord.
Anthony Pegram
Il convient de noter que de nombreuses méthodes sont définies en termes de "Faites tout ce qui est nécessaire pour satisfaire toutes les obligations liées à X". Invoquer une telle méthode sur un objet qui n'a aucun élément lié à X peut être inutile, mais serait toujours bien défini. De plus, si un objet peut ou non avoir des obligations liées à X, il est généralement plus propre et plus efficace de dire sans condition "Satisfaire à toutes vos obligations liées à X", que de demander d'abord s'il a des obligations et de lui demander conditionnellement de les satisfaire .
supercat
1

Je vous suggère de reconsidérer la définition d'une interface distincte que votre classe de base implémente, puis de suivre l'approche abstraite.

Imagerie de code comme ceci:

public interface IVersion
{
    ReturnType Method1();        
    ReturnType Method2();
             .
             .
    ReturnType MethodN();
}

public abstract class AbstractVersion : IVersion
{
    public abstract ReturnType Method1();        
    public abstract ReturnType Method2();
             .
             .
    public abstract ReturnType MethodN();

    //////////////////////////////////////////////
    // Other class implementation stuff is here
    //////////////////////////////////////////////
}

Faire cela résout ces problèmes:

  1. En ayant tout le code qui utilise des objets dérivés de AbstractVersion peut maintenant être implémenté pour recevoir l'interface IVersion à la place, cela signifie qu'ils peuvent être plus facilement testés unitairement.

  2. La version 2 de votre produit peut alors implémenter une interface IVersion2 pour fournir des fonctionnalités supplémentaires sans casser votre code client existant.

par exemple.

public interface IVersion
{
    ReturnType Method1();        
    ReturnType Method2();
             .
             .
    ReturnType MethodN();
}

public interface IVersion2
{
    ReturnType Method2_1();
}

public abstract class AbstractVersion : IVersion, IVersion2
{
    public abstract ReturnType Method1();        
    public abstract ReturnType Method2();
             .
             .
    public abstract ReturnType MethodN();
    public abstract ReturnType Method2_1();

    //////////////////////////////////////////////
    // Other class implementation stuff is here
    //////////////////////////////////////////////
}

Il vaut également la peine de lire sur l'inversion des dépendances, pour empêcher cette classe de contenir des dépendances codées en dur qui empêchent les tests unitaires efficaces.

Michael Shaw
la source
J'ai voté pour avoir fourni la possibilité de gérer différentes versions. Cependant, j'ai essayé et essayé à nouveau d'utiliser efficacement les classes d'interface, en tant que partie normale de la conception, et je finis toujours par réaliser que les classes d'interface ne fournissent aucune / peu de valeur et obscurcissent réellement le code au lieu de rendre la vie plus facile. Il est très rare que plusieurs classes héritent d'une autre, comme une classe d'interface, et il n'y a pas beaucoup de points communs qui ne soient pas partageables. Les classes abstraites ont tendance à mieux fonctionner car j'obtiens l'aspect intégré partageable que les interfaces ne fournissent pas.
Dunk
Et l'utilisation de l'héritage avec de bons noms de classe fournit un moyen beaucoup plus intuitif de comprendre facilement les classes (et donc le système) qu'un tas de noms de classe d'interface fonctionnelle qui sont difficiles à lier mentalement dans leur ensemble. En outre, l'utilisation de classes d'interface a tendance à créer une tonne de classes supplémentaires, ce qui rend le système plus difficile à comprendre d'une autre manière.
Dunk
-2

L'injection de dépendances repose sur des interfaces. Voici un court exemple. L'élève de classe a une fonction appelée CreateStudent qui nécessite un paramètre qui implémente l'interface "IReporting" (avec une méthode ReportAction). Après avoir créé un étudiant, il appelle ReportAction sur le paramètre de classe concret. Si le système est configuré pour envoyer un e-mail après avoir créé un étudiant, nous envoyons une classe concrète qui envoie un e-mail dans son implémentation ReportAction, ou nous pouvons envoyer une autre classe concrète qui envoie une sortie à une imprimante dans son implémentation ReportAction. Idéal pour la réutilisation du code.

Roger
la source
1
cela ne semble pas offrir quoi que ce soit de substantiel par rapport aux points avancés et expliqués dans les 5 réponses précédentes
gnat