it-swarm.com.de

Niemals öffentliche Mitglieder virtuell / abstrakt machen - wirklich?

In den 2000er Jahren sagte mir ein Kollege, dass es ein Anti-Muster ist, öffentliche Methoden virtuell oder abstrakt zu machen.

Zum Beispiel hielt er eine Klasse wie diese für nicht gut gestaltet:

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
  }
}

Das hat er gesagt

  • der Entwickler einer abgeleiteten Klasse, die Method1 implementiert und Method2 überschreibt, muss die Argumentvalidierung wiederholen.
  • falls der Entwickler der Basisklasse beschließt, später etwas um den anpassbaren Teil von Method1 oder Method2 hinzuzufügen, kann er dies nicht tun.

Stattdessen schlug mein Kollege diesen Ansatz vor:

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
  }
}

Er sagte mir, dass es genauso schlecht ist, öffentliche Methoden (oder Eigenschaften) virtuell oder abstrakt zu machen, wie Felder öffentlich zu machen. Durch Umschließen von Feldern in Eigenschaften kann bei Bedarf später der Zugriff auf diese Felder abgefangen werden. Gleiches gilt für öffentliche virtuelle/abstrakte Mitglieder: Wenn Sie sie wie in der Klasse ProtectedAbstractOrVirtual gezeigt verpacken, kann der Basisklassenentwickler alle Aufrufe abfangen, die an die virtuellen/abstrakten Methoden gehen.

Aber ich sehe das nicht als Designrichtlinie. Auch Microsoft folgt dem nicht: Schauen Sie sich einfach die Klasse Stream an, um dies zu überprüfen.

Was halten Sie von dieser Richtlinie? Ist das sinnvoll oder ist die API Ihrer Meinung nach zu kompliziert?

20
Peter Perot

Sprichwort

dass es ein Anti-Muster ist, öffentliche Methoden virtuell oder abstrakt zu machen, weil der Entwickler einer abgeleiteten Klasse, die Methode1 implementiert und Methode2 überschreibt, die Argumentvalidierung wiederholen muss

vermischt Ursache und Wirkung. Es wird davon ausgegangen, dass für jede überschreibbare Methode eine nicht anpassbare Argumentvalidierung erforderlich ist. Aber es ist umgekehrt:

If man möchte eine Methode so entwerfen, dass sie einige feste Argumentvalidierungen in allen Ableitungen der Klasse (oder - allgemeiner - einem anpassbaren und einem nicht anpassbaren Teil) liefert, dann Es ist sinnvoll, den Einstiegspunkt nicht virtuell zu machen und stattdessen eine virtuelle oder abstrakte Methode für den anpassbaren Teil bereitzustellen, der intern aufgerufen wird.

Es gibt jedoch viele Beispiele, bei denen es durchaus sinnvoll ist, eine öffentliche virtuelle Methode zu haben, da es keinen festen, nicht anpassbaren Teil gibt: Sehen Sie sich Standardmethoden wie ToString oder Equals oder GetHashCode- würde es das Design der object -Klasse verbessern, wenn diese nicht gleichzeitig öffentlich und virtuell sind? Das glaube ich nicht.

Oder in Bezug auf Ihren eigenen Code: Wenn der Code in der Basisklasse endlich und absichtlich so aussieht

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

mit dieser Trennung zwischen Method1 und Method1Core kompliziert die Dinge nur ohne ersichtlichen Grund.

30
Doc Brown

Wenn Sie dies so tun, wie es Ihr Kollege vorschlägt, bietet dies dem Implementierer der Basisklasse mehr Flexibilität. Damit verbunden ist jedoch auch eine höhere Komplexität, die in der Regel nicht durch die vermuteten Vorteile gerechtfertigt ist.

Beachten Sie, dass die erhöhte Flexibilität für den Basisklassenimplementierer auf Kosten von weniger Flexibilität für die übergeordnete Partei geht. Sie bekommen ein auferlegtes Verhalten, für das sie sich möglicherweise nicht besonders interessieren. Für sie sind die Dinge gerade starrer geworden. Dies kann gerechtfertigt und hilfreich sein, dies hängt jedoch vom jeweiligen Szenario ab.

Die Namenskonvention, um dies zu implementieren (von der ich weiß), besteht darin, den guten Namen für die öffentliche Schnittstelle zu reservieren und dem Namen der internen Methode "Do" voranzustellen.

Ein nützlicher Fall ist, wenn die ausgeführte Aktion eingerichtet und geschlossen werden muss. Öffnen Sie einen Stream und schließen Sie ihn, nachdem der Overrider fertig ist. Im Allgemeinen die gleiche Art der Initialisierung und Finalisierung. Es ist ein gültiges Muster, aber es wäre sinnlos, seine Verwendung in allen abstrakten und virtuellen Szenarien vorzuschreiben.

6
Martin Maat

In C++ wird dies als nicht virtuelles Schnittstellenmuster (NVI) bezeichnet. (Es war einmal die Vorlagenmethode. Das war verwirrend, aber einige der älteren Artikel haben diese Terminologie.) NVI wird von Herb Sutter beworben, der mindestens einige Male darüber geschrieben hat. Ich denke, einer der frühesten ist hier .

Wenn ich mich richtig erinnere, ist die Prämisse, dass eine abgeleitete Klasse nicht ändern sollte , was die Basisklasse tut, sondern wie es macht es.

Beispielsweise kann eine Form über eine Verschiebungsmethode verfügen, um die Form zu verschieben. Konkrete Implementierungen (z. B. wie Quadrat und Kreise) sollten Verschieben nicht direkt überschreiben, da Form definiert, was Verschieben bedeutet (auf konzeptioneller Ebene). Ein Quadrat hat möglicherweise andere Implementierungsdetails als ein Kreis, was die interne Darstellung der Position betrifft. Daher müssen sie eine Methode überschreiben, um die Verschiebungsfunktionalität bereitzustellen.

In einfachen Beispielen läuft dies oft auf einen öffentlichen Umzug hinaus, bei dem die gesamte Arbeit nur an einen privaten virtuellen ReallyDoTheMove delegiert wird. Es scheint also viel Aufwand ohne Nutzen zu sein.

Diese Eins-zu-Eins-Korrespondenz ist jedoch keine Voraussetzung. Sie können beispielsweise der öffentlichen API von Shape eine Animate-Methode hinzufügen und diese implementieren, indem Sie ReallyDoTheMove in einer Schleife aufrufen. Am Ende stehen zwei öffentliche APIs für nicht virtuelle Methoden zur Verfügung, die beide auf einer privaten abstrakten Methode basieren. Ihre Kreise und Quadrate müssen keine zusätzliche Arbeit leisten und das Überschreiben kann nicht animiert werden .

Die Basisklasse definiert die öffentliche Schnittstelle, die von Verbrauchern verwendet wird, und sie definiert eine Schnittstelle primitiver Operationen, die zur Implementierung dieser öffentlichen Methoden erforderlich ist. Die abgeleiteten Typen sind für die Bereitstellung von Implementierungen dieser primitiven Operationen verantwortlich.

Mir sind keine Unterschiede zwischen C # und C++ bekannt, die diesen Aspekt des Klassendesigns verändern würden.

2
Adrian McCarthy