Ich habe ein Designproblem in Bezug auf .NET-Eigenschaften.
interface IX
{
Guid Id { get; }
bool IsInvalidated { get; }
void Invalidate();
}
Problem:
Diese Schnittstelle verfügt über zwei schreibgeschützte Eigenschaften Id
und IsInvalidated
. Die Tatsache, dass sie schreibgeschützt sind, ist jedoch keine Garantie dafür, dass ihre Werte konstant bleiben.
Nehmen wir an, ich wollte klarstellen, dass ...
Id
stellt einen konstanten Wert dar (der daher sicher zwischengespeichert werden kann), währendIsInvalidated
kann seinen Wert während der Lebensdauer einesIX
Objekts ändern (und sollte daher nicht zwischengespeichert werden).
Wie könnte ich interface IX
diesen Vertrag so ändern , dass er ausreichend explizit ist?
Meine eigenen drei Lösungsversuche:
Die Schnittstelle ist bereits gut gestaltet. Das Vorhandensein einer aufgerufenen Methode
Invalidate()
lässt einen Programmierer darauf schließen, dass der Wert der gleichnamigen EigenschaftIsInvalidated
davon betroffen sein könnte.Dieses Argument gilt nur in Fällen, in denen die Methode und die Eigenschaft ähnlich benannt sind.
Ergänzen Sie diese Schnittstelle mit einem Ereignis
IsInvalidatedChanged
:bool IsInvalidated { get; } event EventHandler IsInvalidatedChanged;
Das Vorhandensein eines
…Changed
Ereignisses fürIsInvalidated
besagt, dass diese Eigenschaft ihren Wert ändern kann, und das Fehlen eines ähnlichen Ereignisses fürId
ist ein Versprechen, dass diese Eigenschaft ihren Wert nicht ändern wird.Ich mag diese Lösung, aber es gibt viele zusätzliche Dinge, die möglicherweise überhaupt nicht verwendet werden.
Ersetzen Sie die Eigenschaft
IsInvalidated
durch eine MethodeIsInvalidated()
:bool IsInvalidated();
Dies könnte eine zu subtile Änderung sein. Es soll ein Hinweis sein, dass ein Wert jedes Mal neu berechnet wird - was nicht nötig wäre, wenn es eine Konstante wäre. Das MSDN-Thema "Auswählen zwischen Eigenschaften und Methoden" enthält Folgendes:
Verwenden Sie in den folgenden Situationen eine Methode anstelle einer Eigenschaft. […] Die Operation gibt bei jedem Aufruf ein anderes Ergebnis zurück, auch wenn sich die Parameter nicht ändern.
Welche Antworten erwarte ich?
Ich interessiere mich am meisten für ganz andere Lösungen des Problems, zusammen mit einer Erklärung, wie sie meine obigen Versuche schlagen.
Wenn meine Versuche logisch fehlerhaft sind oder wesentliche, noch nicht erwähnte Nachteile aufweisen, so dass nur eine (oder keine) Lösung übrig bleibt, würde ich gerne erfahren, wo ich einen Fehler gemacht habe.
Wenn die Mängel geringfügig sind und mehr als eine Lösung nach Berücksichtigung verbleibt, kommentieren Sie bitte.
Zumindest würde ich gerne eine Rückmeldung darüber erhalten, welche Lösung Sie bevorzugen und aus welchem Grund.
quelle
IsInvalidated
ein benötigenprivate set
?class Foo : IFoo { private bool isInvalidated; public bool IsInvalidated { get { return isInvalidated; } } public void Invalidate() { isInvalidated = true; } }
InvalidStateException
zum Beispiel nie um s kümmern müssen , aber ich bin mir nicht sicher, ob dies überhaupt theoretisch möglich ist. Wäre aber nett.Antworten:
Ich würde Lösung 3 vor 1 und 2 bevorzugen.
Mein Problem mit Lösung 1 ist : Was passiert, wenn es keine
Invalidate
Methode gibt? Nehmen Sie eine Schnittstelle mit einer zurückgegebenenIsValid
Eigenschaft anDateTime.Now > MyExpirationDate
. Möglicherweise benötigen Sie hier keine expliziteSetInvalid
Methode. Was ist, wenn sich eine Methode auf mehrere Eigenschaften auswirkt? Nehmen Sie einen Verbindungstyp mitIsOpen
undIsConnected
Eigenschaften an - beide sind von derClose
Methode betroffen .Lösung 2 : Wenn der einzige Punkt des Ereignisses darin besteht, den Entwicklern mitzuteilen, dass eine ähnlich benannte Eigenschaft bei jedem Aufruf andere Werte zurückgeben kann, würde ich dringend davon abraten. Sie sollten Ihre Schnittstellen kurz und übersichtlich halten. Darüber hinaus ist möglicherweise nicht jede Implementierung in der Lage, dieses Ereignis für Sie auszulösen. Ich werde mein
IsValid
Beispiel von oben wiederverwenden : Sie müssten einen Timer implementieren und ein Ereignis auslösen, wenn Sie erreichenMyExpirationDate
. Wenn dieses Ereignis Teil Ihrer öffentlichen Schnittstelle ist, erwarten Benutzer der Schnittstelle, dass dieses Ereignis funktioniert.Davon abgesehen sind diese Lösungen nicht schlecht. Das Vorhandensein einer Methode oder eines Ereignisses wird anzeigen, dass eine ähnlich benannte Eigenschaft bei jedem Aufruf unterschiedliche Werte zurückgeben kann. Ich sage nur, dass sie allein nicht ausreichen, um diese Bedeutung immer wieder zu vermitteln.
Lösung 3 ist das, wonach ich streben würde. Wie bereits erwähnt, funktioniert dies möglicherweise nur für C # -Entwickler. Für mich als C # -Dev bedeutet die Tatsache, dass
IsInvalidated
es sich nicht um eine Eigenschaft handelt, sofort, dass es sich nicht nur um einen einfachen Accessor handelt, sondern dass hier etwas vor sich geht. Dies gilt jedoch nicht für alle, und wie MainMa hervorhob, ist das .NET-Framework selbst hier nicht konsistent.Wenn Sie Lösung 3 verwenden möchten, würde ich empfehlen, sie als Konvention zu deklarieren und das gesamte Team zu beauftragen. Dokumentation ist auch wichtig, glaube ich. Meiner Meinung es ist eigentlich ziemlich einfach Hinweis Werte ändern: „ true zurück , wenn der Wert immer noch gültig “ „ zeigt an, ob die Verbindung ist bereits geöffnet “ vs. „ gibt true zurück , wenn dieses Objekt ist gültig “. Es würde überhaupt nicht schaden, in der Dokumentation genauer zu sein.
Mein Rat wäre also:
Erklären Sie Lösung 3 zu einer Konvention und befolgen Sie sie konsequent. Machen Sie in der Dokumentation der Eigenschaften deutlich, ob eine Eigenschaft sich ändernde Werte aufweist. Entwickler, die mit einfachen Eigenschaften arbeiten, gehen korrekterweise davon aus, dass sie sich nicht ändern (dh sie ändern sich nur, wenn der Objektstatus geändert wird). Entwickler , ein Verfahren zu begegnen , dass klingt wie es eine Eigenschaft sein könnte (
Count()
,Length()
,IsOpen()
) wissen , dass some ist hier los und (hoffentlich) die Methode docs lesen , zu verstehen , was genau die Methode funktioniert und wie es sich verhält.quelle
Es gibt eine vierte Lösung: Verlassen Sie sich auf Dokumentation .
Woher weißt du, dass diese
string
Klasse unveränderlich ist? Sie wissen das einfach, weil Sie die MSDN-Dokumentation gelesen haben.StringBuilder
Auf der anderen Seite ist es veränderlich, weil die Dokumentation Ihnen das wieder sagt.Ihre dritte Lösung ist nicht schlecht, aber .NET Framework folgt ihr nicht . Beispielsweise:
sind Eigenschaften, aber erwarten Sie nicht, dass sie jedes Mal konstant bleiben.
In .NET Framework selbst wird durchweg Folgendes verwendet:
ist eine Methode, während:
ist eine Eigenschaft. Im ersten Fall kann das Ausführen von Aufgaben zusätzliche Arbeit erfordern, z. B. das Abfragen der Datenbank. Diese zusätzliche Arbeit kann einige Zeit in Anspruch nehmen . Es wird erwartet, dass eine Eigenschaft nur eine kurze Zeit benötigt: Die Länge kann sich während der Lebensdauer der Liste zwar ändern, für die Rückgabe ist jedoch praktisch nichts erforderlich.
Bei Klassen zeigt ein einfacher Blick auf den Code, dass sich der Wert einer Eigenschaft während der Lebensdauer des Objekts nicht ändert. Zum Beispiel,
ist klar: der preis bleibt gleich.
Leider können Sie nicht dasselbe Muster auf eine Schnittstelle anwenden, und da C # in diesem Fall nicht aussagekräftig genug ist, sollte Dokumentation (einschließlich XML-Dokumentation und UML-Diagramme) verwendet werden, um die Lücke zu schließen.
quelle
Lazy<T>.Value
kann die Berechnung sehr lange dauern (wenn zum ersten Mal darauf zugegriffen wird).Meine 2 Cent:
Option 3: Als Nicht-C # -Er wäre dies kein großer Hinweis. Anhand des Zitats, das Sie vorlegen, sollte es jedoch erfahrenen C # -Programmierern klar sein, dass dies etwas bedeutet. Sie sollten jedoch weiterhin Dokumente dazu hinzufügen.
Option 2: Gehen Sie nicht dorthin, es sei denn, Sie planen, Ereignisse zu allen Änderungen hinzuzufügen.
Andere Optionen:
id
, von dem normalerweise angenommen wird, dass er auch in veränderlichen Objekten unveränderlich ist.quelle
Eine andere Möglichkeit wäre, die Schnittstelle zu teilen: Angenommen, nach dem Aufruf
Invalidate()
jedes Aufrufs vonIsInvalidated
sollte derselbe Wert zurückgegeben werdentrue
, scheint es keinen Grund zu geben,IsInvalidated
für denselben aufgerufenen Teil aufzurufenInvalidate()
.Daher schlage ich vor, dass Sie entweder auf Ungültigkeit prüfen oder es verursachen , aber es erscheint unvernünftig, beides zu tun. Daher ist es sinnvoll, eine Schnittstelle, die die
Invalidate()
Operation enthält, für den ersten Teil und eine andere Schnittstelle zum Prüfen (IsInvalidated
) für den anderen Teil anzubieten . Da es ziemlich offensichtlich aus der Signatur der ersten Schnittstelle ist , dass es die Instanz bewirkt , dass für ungültig erklärt werden, ist die verbleibende Frage (für die zweite Schnittstelle) in der Tat ganz allgemein: Wie bestimmen Sie, ob der angegebene Typ ist unveränderlich .Ich weiß, das beantwortet die Frage nicht direkt, aber es reduziert sie zumindest auf die Frage, wie man einen unveränderlichen Typ markiert , was ein weit verbreitetes und verstandenes Problem ist. Wenn Sie beispielsweise annehmen, dass alle Typen, sofern nicht anders definiert, veränderbar sind, können Sie daraus schließen, dass die zweite Schnittstelle (
IsInvalidated
) veränderbar ist und daher den Wert vonIsInvalidated
von Zeit zu Zeit ändern kann . Angenommen, die erste Schnittstelle sieht folgendermaßen aus (Pseudosyntax):Da es als unveränderlich markiert ist, wissen Sie , dass sich die ID nicht ändert. Natürlich führt der Aufruf von invalidate zu einer Änderung des Zustands der Instanz, aber diese Änderung kann über diese Schnittstelle nicht beobachtet werden.
quelle
[Immutable]
solange sie Methoden umfasst, deren einziger Zweck darin besteht, eine Mutation zu verursachen. Ich würde dieInvalidate()
Methode auf dieInvalidatable
Schnittstelle verschieben.Id
, erhalten Sie jedes Mal den gleichen Wert. Gleiches gilt fürInvalidate()
(Sie bekommen nichts, da der Rückgabetyp istvoid
). Daher ist der Typ unveränderlich. Natürlich, werden Sie haben Nebenwirkungen , aber Sie werden nicht in der Lage zu beobachten , sie über die SchnittstelleIX
, so werden sie an den Kunden keine Auswirkungen habenIX
.IX
unveränderlich ist. Und dass sie Nebenwirkungen sind; Sie werden einfach auf zwei aufgeteilte Schnittstellen verteilt, sodass sich die Clients nicht darum kümmern müssen (im Fall vonIX
) oder offensichtlich sind, was die Nebenwirkung sein könnte (im Fall vonInvalidatable
). Aber warum nicht bewegenInvalidate
zu überInvalidatable
? Das würdeIX
unveränderlich und rein machen undInvalidatable
würde weder veränderlicher noch unreiner werden (weil es bereits beides ist).Invalidate()
undIsInvalidated
denselben Benutzer der Schnittstelle aufzurufen . Wenn Sie anrufenInvalidate()
, sollten Sie wissen, dass es ungültig wird. Warum sollten Sie das überprüfen? Somit können Sie zwei unterschiedliche Schnittstellen für unterschiedliche Zwecke bereitstellen.