web-dev-qa-db-fra.com

Ne jamais rendre les membres publics virtuels / abstraits - vraiment?

Dans les années 2000, un de mes collègues m'a dit que c'était un anti-modèle de rendre les méthodes publiques virtuelles ou abstraites.

Par exemple, il a considéré une classe comme celle-ci pas bien conçue:

public abstract class PublicAbstractOrVirtual
{
  public abstract void Method1(string argument);

  public virtual void Method2(string argument)
  {
    if (argument == null) throw new ArgumentNullException(nameof(argument));
    // default implementation
  }
}

Il a déclaré que

  • le développeur d'une classe dérivée qui implémente Method1 et remplace Method2 doit répéter la validation de l'argument.
  • dans le cas où le développeur de la classe de base décide d'ajouter quelque chose autour de la partie personnalisable de Method1 ou Method2 plus tard, il ne peut pas le faire.

Au lieu de cela, mon collègue a proposé cette approche:

public abstract class ProtectedAbstractOrVirtual
{
  public void Method1(string argument)
  {
    if (argument == null) throw new ArgumentNullException(nameof(argument));
    this.Method1Core(argument);
  }

  public void Method2(string argument)
  {
    if (argument == null) throw new ArgumentNullException(nameof(argument));
    this.Method2Core(argument);
  }

  protected abstract void Method1Core(string argument);

  protected virtual void Method2Core(string argument)
  {
    // default implementation
  }
}

Il m'a dit que rendre les méthodes publiques (ou propriétés) virtuelles ou abstraites est aussi mauvais que rendre les champs publics. En encapsulant des champs dans des propriétés, vous pouvez intercepter ultérieurement tout accès à ces champs, si nécessaire. La même chose s'applique aux membres publics virtuels/abstraits: les envelopper comme indiqué dans la classe ProtectedAbstractOrVirtual permet au développeur de la classe de base d'intercepter tous les appels qui vont aux méthodes virtuelles/abstraites.

Mais je ne vois pas cela comme une directive de conception. Même Microsoft ne le suit pas: jetez un œil à la classe Stream pour le vérifier.

Que pensez-vous de cette ligne directrice? Cela a-t-il un sens ou pensez-vous que cela complique trop l'API?

20
Peter Perot

En disant

qu'il s'agit d'un anti-modèle pour rendre les méthodes publiques virtuelles ou abstraites à cause du développeur d'une classe dérivée qui implémente la méthode 1 et remplace la méthode 2 doit répéter la validation de l'argument

mélange la cause et l'effet. Il suppose que chaque méthode remplaçable nécessite une validation d'argument non personnalisable. Mais c'est tout le contraire:

Si on veut concevoir une méthode de manière à fournir des validations d'arguments fixes dans toutes les dérivations de la classe (ou - plus généralement - une partie personnalisable et une partie non personnalisable), alors il est logique de rendre le point d'entrée non virtuel et de fournir à la place une méthode virtuelle ou abstraite pour la partie personnalisable qui est appelée en interne.

Mais il existe de nombreux exemples où il est parfaitement logique d'avoir une méthode virtuelle publique, car il n'y a pas de partie fixe non personnalisable: regardez les méthodes standard comme ToString ou Equals ou GetHashCode- est-ce que cela améliorerait la conception de la classe object pour que celles-ci ne soient pas publiques et virtuelles à la fois? Je ne pense pas.

Ou, en termes de votre propre code: lorsque le code de la classe de base ressemble finalement et intentionnellement à ceci

 public void Method1(string argument)
 {
    // nothing to validate here, all strings including null allowed
    this.Method1Core(argument);
 }

ayant cette séparation entre Method1 et Method1Core ne fait que compliquer les choses sans raison apparente.

30
Doc Brown

Le faire comme le suggère votre collègue offre plus de flexibilité à l'implémenteur de la classe de base. Mais cela s'accompagne également d'une complexité accrue qui n'est généralement pas justifiée par les avantages présumés.

Gardez à l'esprit que la flexibilité accrue de l'implémenteur de la classe de base se fait au détriment de la flexibilité de less pour la partie dominante. Ils obtiennent un comportement imposé dont ils ne se soucient pas particulièrement. Pour eux, les choses sont devenues plus rigides. Cela peut être justifié et utile, mais tout dépend du scénario.

La convention de dénomination pour implémenter cela (que je sache) est de réserver le bon nom pour l'interface publique et de préfixer le nom de la méthode interne avec "Do".

Un cas utile est lorsque l'action effectuée nécessite une configuration et une fermeture. Comme ouvrir un flux et le fermer une fois le remplacement terminé. En général, même type d'initialisation et de finalisation. C'est un modèle valide à utiliser, mais il serait inutile de rendre obligatoire son utilisation dans tous les scénarios abstraits et virtuels.

6
Martin Maat

En C++, cela s'appelle modèle d'interface non virtuelle (NVI). (Il était une fois la méthode Template. C'était déroutant, mais certains des anciens articles ont cette terminologie.) NVI est promu par Herb Sutter, qui a écrit à ce sujet au moins quelques fois. Je pense que l'un des premiers est ici .

Si je me souviens bien, la prémisse est qu'une classe dérivée ne devrait pas changer ce que la classe de base fait mais comment il le fait.

Par exemple, une forme peut avoir une méthode Move pour déplacer la forme. Une implémentation concrète (par exemple, comme Square et Circles) ne doit pas remplacer directement Move, car Shape définit ce que signifie Moving (au niveau conceptuel). Un carré peut avoir des détails d'implémentation différents d'un cercle en termes de représentation interne de la position, ils devront donc remplacer une méthode pour fournir la fonctionnalité de déplacement.

Dans des exemples simples, cela se résume souvent à un Move public qui délègue tout le travail à un ReallyDoTheMove virtuel privé, il semble donc que beaucoup de frais généraux sans aucun avantage.

Mais cette correspondance individuelle n'est pas une exigence. Par exemple, vous pouvez ajouter une méthode Animate à l'API publique de Shape, et elle peut l'implémenter en appelant ReallyDoTheMove en boucle. Vous vous retrouvez avec deux API de méthodes publiques non virtuelles qui reposent toutes deux sur la seule méthode abstraite privée. Vos cercles et carrés n'ont pas besoin de faire de travail supplémentaire, ni le remplacement d'animer .

La classe de base définit l'interface publique utilisée par les consommateurs et définit une interface d'opérations primitives dont elle a besoin pour implémenter ces méthodes publiques. Les types dérivés sont chargés de fournir des implémentations de ces opérations primitives.

Je ne suis au courant d'aucune différence entre C # et C++ qui changerait cet aspect de la conception des classes.

2
Adrian McCarthy