In den 2000er Jahren sagte mir ein Kollege, es sei ein Anti-Pattern, öffentliche Methoden virtuell oder abstrakt zu machen.
Zum Beispiel hielt er eine Klasse wie diese für nicht gut designt:
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 implementiert
Method1
und überschreibt,Method2
muss die Argumentvalidierung wiederholen. - Falls der Entwickler der Basisklasse beschließt, etwas um den anpassbaren Teil von
Method1
oderMethod2
später 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 schlimm ist, öffentliche Methoden (oder Eigenschaften) virtuell oder abstrakt zu machen, wie Felder öffentlich zu machen. Wenn Sie Felder in Eigenschaften einschließen, können Sie bei Bedarf den Zugriff auf diese Felder später abfangen. Dasselbe gilt für öffentliche virtuelle / abstrakte Member: Wenn diese wie in der ProtectedAbstractOrVirtual
Klasse gezeigt verpackt werden, kann der Basisklassenentwickler alle Aufrufe abfangen, die an die virtuellen / abstrakten Methoden gesendet werden.
Aber ich sehe das nicht als Designrichtlinie. Selbst Microsoft folgt dem nicht: Schauen Sie sich die Stream
Klasse an, um dies zu überprüfen.
Was halten Sie von dieser Richtlinie? Ergibt es irgendeinen Sinn oder denken Sie, dass es die API überkompliziert?
quelle
virtual
ermöglicht optionales Überschreiben. Ihre Methode sollte wahrscheinlich öffentlich sein, da sie möglicherweise nicht überschrieben wird. Wenn Sie Methodenabstract
erstellen, müssen Sie diese überschreiben. Sie sollten es wahrscheinlich seinprotected
, weil sie in einempublic
Kontext nicht besonders nützlich sind .protected
ist besonders nützlich, wenn Sie private Mitglieder der abstrakten Klasse abgeleiteten Klassen aussetzen möchten. Auf jeden Fall bin ich nicht besonders besorgt über die Meinung Ihres Freundes; Wählen Sie den Zugriffsmodifikator, der für Ihre spezielle Situation am sinnvollsten ist.Antworten:
Sprichwort
vermischt Ursache und Wirkung. Es wird davon ausgegangen, dass für jede überschreibbare Methode eine nicht anpassbare Argumentvalidierung erforderlich ist. Ganz anders herum:
Wenn eine Methode so entworfen werden soll, dass sie in allen Ableitungen der Klasse (oder allgemeiner gesagt in einem anpassbaren und einem nicht anpassbaren Teil) einige feste Argumentvalidierungen liefert, ist es sinnvoll, den Einstiegspunkt nicht virtuell zu machen Stellen Sie stattdessen eine virtuelle oder abstrakte Methode für den anpassbaren Teil bereit, 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
oderEquals
oder an.GetHashCode
Würde es das Design derobject
Klasse verbessern , wenn diese nicht öffentlich sind und gleichzeitig virtuell? Ich glaube nicht.Oder in Bezug auf Ihren eigenen Code: Wenn der Code in der Basisklasse endgültig und absichtlich so aussieht
Diese Trennung zwischen
Method1
undMethod1Core
kompliziert die Dinge nur ohne ersichtlichen Grund.quelle
ToString()
Methode hat Microsoft sie besser nicht-virtuell gemacht und eine virtuelle Vorlagenmethode eingeführtToStringCore()
. Warum: aus diesem Grund:ToString()
- Hinweis an die Erben . Sie geben an, dassToString()
nicht null zurückgegeben werden soll. Sie hätten diese Forderung durch Umsetzung erzwingen könnenToString() => ToStringCore() ?? string.Empty
.string.Empty
Sie das bemerkt? Und es werden viele andere Dinge empfohlen, die nicht durch die Einführung einerToStringCore
Methode im Code erzwungen werden können. Daher ist diese Technik wahrscheinlich nicht das richtige Werkzeug fürToString
.anyObjectIGot.ToString()
stattanyObjectIGot?.ToString()
“ - wie dies relevant ist? IhrToStringCore()
Ansatz würde verhindern, dass eine Nullzeichenfolge zurückgegeben wird, aber es würde trotzdem einen werfen,NullReferenceException
wenn das Objekt Null ist.public virtual
, aber es gibt Fälle, in denenpublic virtual
es in Ordnung ist. Wir könnten argumentieren, dass ein leerer, nicht anpassbarer Teil den Code zukunftssicher macht ... Aber das funktioniert nicht. Ein Zurückgehen und Ändern kann abgeleitete Typen beschädigen. Somit verdient es nichts.Wenn Sie dies so tun, wie es Ihr Kollege vorschlägt, wird der Implementierer der Basisklasse flexibler. Dies bringt jedoch auch eine größere Komplexität mit sich, die in der Regel nicht durch den vermuteten Nutzen gerechtfertigt ist.
Geist , dass die erhöhte Flexibilität für die Implementierer Basisklasse kommt auf Kosten von weniger Flexibilität für die zwingende Partei. Sie bekommen ein auferlegtes Verhalten, für das sie sich möglicherweise nicht besonders interessieren. Für sie sind die Dinge nur noch steifer geworden. Dies kann gerechtfertigt und hilfreich sein, hängt jedoch vom jeweiligen Szenario ab.
Die Namenskonvention, um dies zu implementieren (die ich kenne), besteht darin, den guten Namen für die öffentliche Schnittstelle zu reservieren und dem Namen der internen Methode das Präfix "Do" voranzustellen.
Ein nützlicher Fall ist, wenn für die ausgeführte Aktion einige Einstellungen erforderlich sind und einige beendet werden müssen. Öffnen Sie einen Stream und schließen Sie ihn, nachdem der Overrider abgeschlossen ist. Im Allgemeinen die gleiche Art der Initialisierung und Finalisierung. Es ist ein gültiges Muster, aber es wäre sinnlos, es in allen abstrakten und virtuellen Szenarien anzuwenden.
quelle
BindingList
. Außerdem habe ich die Empfehlung irgendwo gefunden, vielleicht deren Framework Design Guidelines oder ähnliches. Und: Es ist ein Postfix . ;-)In C ++ wird dies als nicht-virtuelles Schnittstellenmuster (NVI) bezeichnet. (Früher hieß es Template-Methode. Das war verwirrend, aber einige ältere 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 sie es tut.
Beispielsweise verfügt eine Form möglicherweise über eine Move-Methode zum Verschieben der Form. Konkrete Implementierungen (z. B. Square und Circles) sollten Move nicht direkt außer Kraft setzen, da Shape definiert, was Moving bedeutet (auf konzeptioneller Ebene). In Bezug auf die interne Darstellung der Position kann ein Quadrat andere Implementierungsdetails aufweisen als ein Kreis. Sie müssen daher eine Methode außer Kraft setzen, um die Verschiebungsfunktion bereitzustellen.
In einfachen Beispielen läuft dies oft auf einen öffentlichen Umzug hinaus, bei dem die gesamte Arbeit an eine private virtuelle ReallyDoTheMove-Instanz delegiert wird.
Diese Eins-zu-Eins-Korrespondenz ist jedoch keine Voraussetzung. Beispielsweise können Sie der öffentlichen API von Shape eine Animate-Methode hinzufügen und diese implementieren, indem Sie ReallyDoTheMove in einer Schleife aufrufen. Am Ende stehen Ihnen zwei öffentliche, nicht virtuelle Methoden-APIs zur Verfügung, die beide auf der einen privaten abstrakten Methode basieren. Ihre Kreise und Quadrate müssen keine zusätzliche Arbeit leisten, und Animate kann nicht außer Kraft gesetzt werden .
Die Basisklasse definiert die öffentliche Schnittstelle, die von Verbrauchern verwendet wird, und definiert eine Schnittstelle von primitiven Operationen, die zum Implementieren dieser öffentlichen Methoden erforderlich sind. Die abgeleiteten Typen sind dafür verantwortlich, Implementierungen dieser primitiven Operationen bereitzustellen.
Ich kenne keine Unterschiede zwischen C # und C ++, die diesen Aspekt des Klassendesigns verändern würden.
quelle