Mit is
dem instanceof
Operator von C # und dem Operator von Java können Sie auf die von einer Objektinstanz implementierte Schnittstelle (oder, noch schlimmer, ihre Basisklasse) verzweigen.
Ist es angemessen, diese Funktion für Verzweigungen auf hoher Ebene zu verwenden, basierend auf den Fähigkeiten, die eine Schnittstelle bietet?
Oder sollte eine Basisklasse boolesche Variablen bereitstellen, um eine Schnittstelle zur Beschreibung der Fähigkeiten eines Objekts bereitzustellen?
Beispiel:
if (account is IResetsPassword)
((IResetsPassword)account).ResetPassword();
else
Print("Not allowed to reset password with this account type!");
gegen
if (account.CanResetPassword)
((IResetsPassword)account).ResetPassword();
else
Print("Not allowed to reset password with this account type!");
Gibt es Fallstricke für die Verwendung der Schnittstellenimplementierung zur Identifizierung von Funktionen?
Dieses Beispiel war nur das, ein Beispiel. Ich frage mich über eine allgemeinere Anwendung.
object-oriented
TheCatWhisperer
quelle
quelle
CanResetPassword
nur dann der Fall ist, wenn etwas implementiert wirdIResetsPassword
. Jetzt legen Sie mit einer Eigenschaft fest, was das Typsystem steuern soll (und letztendlich, wenn Sie den Cast vorbereiten).Antworten:
Mit der Einschränkung, dass es am besten ist, Downcasting zu vermeiden, ist die erste Form aus zwei Gründen vorzuziehen:
is
der Downcast definitiv funktionieren , wenn die Brände ausbrechen. Im zweiten Formular müssen Sie manuell sicherstellen, dass nur Objekte, die implementierenIResetsPassword
, für diese Eigenschaft true zurückgebenDas heißt nicht, dass Sie diese Probleme nicht irgendwie verbessern können. Sie könnten beispielsweise festlegen, dass die Basisklasse über eine Reihe von veröffentlichten Schnittstellen verfügt, in die Sie die Einbeziehung einchecken können. Sie implementieren jedoch tatsächlich nur manuell einen Teil des vorhandenen Typsystems, der redundant ist.
Übrigens ist meine natürliche Vorliebe die Zusammensetzung gegenüber der Vererbung, aber hauptsächlich in Bezug auf den Staat. Die Schnittstellenvererbung ist normalerweise nicht so schlecht. Wenn Sie feststellen, dass Sie die Typhierarchie eines armen Mannes implementieren, ist es besser, diejenige zu verwenden, die bereits vorhanden ist, da der Compiler Ihnen dabei hilft.
quelle
(account as IResettable)?.ResetPassword();
var resettable = account as IResettable; if(resettable != null) {resettable.ResetPassword()} else {....}
Fähigkeiten eines Objekts sollten überhaupt nicht identifiziert werden.
Der Client, der ein Objekt verwendet, sollte nicht wissen müssen, wie es funktioniert. Der Kunde sollte nur Dinge wissen, die er dem Objekt mitteilen kann. Was das Objekt tut, ist nicht das Problem des Kunden.
Also lieber als
oder
Erwägen
oder
account
weiß da schon ob das klappen wird. Warum fragst du es? Sagen Sie ihm einfach, was Sie wollen, und lassen Sie es tun, was immer es tun wird.Stellen Sie sich vor, dies geschieht so, dass es dem Kunden egal ist, ob es funktioniert oder nicht, da dies ein anderes Problem darstellt. Die Aufgabe des Kunden bestand nur darin, den Versuch zu machen. Das ist jetzt erledigt und es gibt andere Dinge, mit denen man sich befassen muss. Dieser Stil hat viele Namen, aber der Name, den ich am meisten mag, ist sagen, nicht fragen .
Wenn Sie dem Kunden schreiben, ist es sehr verlockend zu glauben, Sie müssten alles im Auge behalten und so alles auf sich zuziehen, was Sie zu wissen glauben. Wenn Sie das tun, drehen Sie Objekte um. Schätzen Sie Ihre Ignoranz. Schieben Sie die Details weg und lassen Sie die Objekte damit umgehen. Je weniger Sie wissen, desto besser.
quelle
if (!account.AttemptPasswordReset()) /* attempt failed */;
Auf diese Weise kennt der Kunde das Ergebnis, vermeidet jedoch einige Probleme mit der Entscheidungslogik in der Frage. Wenn der Versuch asynchron ist, würden Sie einen Rückruf übergeben.account
als es erstellt wurde. Zu diesem Zeitpunkt können Popups, Protokollierungen, Ausdrucke und akustische Warnungen auftreten. Die Aufgabe des Kunden ist es, "mach es jetzt" zu sagen. Mehr muss es nicht tun. Sie müssen nicht alles an einem Ort erledigen.Hintergrund
Vererbung ist ein leistungsfähiges Werkzeug, das in der objektorientierten Programmierung eingesetzt wird. Es löst jedoch nicht jedes Problem auf elegante Weise: Manchmal sind andere Lösungen besser.
Wenn Sie an Ihre frühen Informatikkurse zurückdenken (vorausgesetzt, Sie haben einen CS-Abschluss), erinnern Sie sich vielleicht an einen Professor, der Ihnen einen Absatz gegeben hat, in dem steht, was der Kunde von der Software erwartet. Ihre Aufgabe ist es, den Absatz zu lesen, die Akteure und Aktionen zu identifizieren und einen groben Überblick über die Klassen und Methoden zu erhalten. Es wird einige Penner-Leads geben, die so aussehen, als wären sie wichtig, aber nicht. Es besteht auch die Möglichkeit, dass Anforderungen falsch interpretiert werden.
Dies ist eine wichtige Fähigkeit, die selbst die erfahrensten von uns falsch verstehen: Anforderungen richtig identifizieren und in Maschinensprachen übersetzen.
Ihre Frage
Ich denke, Sie verstehen den allgemeinen Fall von optionalen Aktionen, die Klassen ausführen können, aufgrund Ihrer Ausführungen falsch. Ja, ich weiß, Ihr Code ist nur ein Beispiel und Sie interessieren sich für den allgemeinen Fall. Es hört sich jedoch so an, als ob Sie wissen möchten, wie Sie mit der Situation umgehen sollen, in der bestimmte Untertypen eines Objekts eine Aktion ausführen können, andere Untertypen jedoch nicht.
Nur weil ein Objekt wie a einen
account
Kontotyp hat, heißt das nicht, dass es in einen Typ in einer OO-Sprache übersetzt wird. "Typ" in einer menschlichen Sprache bedeutet nicht immer "Klasse". Im Kontext eines Kontos kann "Typ" enger mit "Berechtigungssatz" korrelieren. Sie möchten ein Benutzerkonto zum Ausführen einer Aktion verwenden, diese Aktion kann jedoch möglicherweise von diesem Konto ausgeführt werden. Anstatt die Vererbung zu verwenden, würde ich einen Delegaten oder ein Sicherheitstoken verwenden.Meine Lösung
Stellen Sie sich eine Kontoklasse vor, die über mehrere optionale Aktionen verfügt, die sie ausführen kann. Anstatt zu definieren, dass "Aktion X ausführen kann" über die Vererbung erfolgen soll, können Sie das Konto ein Stellvertretungsobjekt (Kennwort-Resetter, Formular-Übermittler usw.) oder ein Zugriffstoken zurückgeben lassen.
Der Vorteil der letzten Option besteht darin, dass ich mithilfe meiner Kontoanmeldeinformationen das Kennwort eines anderen Benutzers zurücksetzen möchte .
Das System ist nicht nur flexibler, ich habe auch Schriftabdrücke entfernt, sondern das Design ist aussagekräftiger . Ich kann dies lesen und leicht verstehen, was es tut und wie es verwendet werden kann.
TL; DR: Das liest sich wie ein XY-Problem . Im Allgemeinen lohnt es sich, angesichts zweier suboptimaler Optionen einen Schritt zurückzutreten und zu überlegen: "Was versuche ich hier wirklich zu erreichen? Sollte ich wirklich darüber nachdenken, wie ich die Typisierung weniger hässlich gestalten kann, oder nach Wegen suchen, um dies zu erreichen?" den typecast komplett entfernen? "
quelle
account.getPasswordResetter().resetPassword();
.The benefit to the last option there is what if I want to use my account credentials to reset someone else's password?
.. einfach. Sie erstellen ein Kontodomänenobjekt für das andere Konto und verwenden dieses. Statische Klassen sind für Komponententests problematisch (diese Methode benötigt per Definition Zugriff auf Speicher, so dass Sie jetzt keinen Code mehr auf einfache Weise testen können, der diese Klasse berührt) und am besten vermieden werden. Die Option, eine andere Schnittstelle zurückzugeben, ist häufig eine gute Idee, beschäftigt sich jedoch nicht wirklich mit dem zugrunde liegenden Problem (Sie haben das Problem einfach auf die andere Schnittstelle verschoben).Bill Venner sagt, dass Ihr Ansatz absolut in Ordnung ist ; Wechseln Sie zu dem Abschnitt mit dem Titel "Wann wird instanceof verwendet?". Sie stürzen sich auf einen bestimmten Typ / eine bestimmte Schnittstelle, die nur dieses bestimmte Verhalten implementiert, sodass Sie absolut richtig sind, diesbezüglich selektiv vorzugehen.
Wenn Sie jedoch einen anderen Ansatz verwenden möchten, gibt es viele Möglichkeiten, einen Yak zu rasieren.
Es gibt den Polymorphismus-Ansatz; Sie könnten argumentieren, dass alle Konten ein Passwort haben, daher sollten alle Konten zumindest versuchen können, ein Passwort zurückzusetzen. Dies bedeutet, dass wir in der Basiskontoklasse eine
resetPassword()
Methode haben sollten , die alle Konten implementieren, aber auf ihre eigene Weise. Die Frage ist, wie sich diese Methode verhalten soll, wenn die Fähigkeit nicht vorhanden ist.Es könnte eine Leerstelle zurückgeben und stillschweigend vervollständigen, ob das Kennwort zurückgesetzt wurde oder nicht, und möglicherweise die Verantwortung dafür übernehmen, die Nachricht intern auszudrucken, wenn das Kennwort nicht zurückgesetzt wird. Nicht die beste Idee.
Es könnte ein Boolescher Wert zurückgegeben werden, der angibt, ob das Zurücksetzen erfolgreich war. Wenn Sie diesen Booleschen Wert einschalten, kann dies dazu führen, dass das Zurücksetzen des Kennworts fehlgeschlagen ist.
Es kann ein String zurückgegeben werden, der das Ergebnis des Kennwortrücksetzversuchs angibt. Die Zeichenfolge kann weitere Informationen darüber enthalten, warum ein Zurücksetzen fehlgeschlagen ist, und kann ausgegeben werden.
Es könnte ein ResetResult-Objekt zurückgeben, das mehr Details übermittelt und alle vorherigen Rückgabeelemente kombiniert.
Wenn Sie versuchen, ein Konto zurückzusetzen, das über diese Funktion nicht verfügt, kann dies zu einer Stornierung und stattdessen zu einer Ausnahmebedingung führen.
Eine Matching-
canResetPassword()
Methode zu haben scheint vielleicht nicht das Schlimmste auf der Welt zu sein, da es sich fälschlicherweise um eine statische Funktion handelt, die in die Klasse eingebaut war, als sie geschrieben wurde. Dies ist ein Hinweis darauf, warum der Methodenansatz eine schlechte Idee ist, da er darauf hindeutet, dass die Fähigkeit dynamisch ist und sichcanResetPassword()
möglicherweise ändert, was auch die zusätzliche Möglichkeit schafft, dass sie sich zwischen der Erlaubnisanfrage und dem Aufruf ändert. Wie an anderer Stelle erwähnt, sagen Sie, anstatt um Erlaubnis zu bitten.Komposition über Vererbung könnte eine Option sein: Sie könnten ein abschließendes
passwordResetter
Feld (oder einen äquivalenten Getter) und eine oder mehrere äquivalente Klassen haben, auf die Sie vor dem Aufruf auf Null prüfen können. Während es ein bisschen so aussieht, als würde man um Erlaubnis bitten, könnte die Finalität jede gefolgerte dynamische Natur vermeiden.Möglicherweise möchten Sie die Funktionalität in eine eigene Klasse auslagern, die einen Parameter berücksichtigt und darauf reagiert (z. B.
resetter.reset(account)
). Dies ist jedoch häufig auch eine schlechte Praxis (eine übliche Analogie ist ein Ladenbesitzer, der in Ihre Brieftasche greift, um Bargeld zu erhalten).In Sprachen mit der Fähigkeit können Sie Mixins oder Merkmale verwenden. Sie landen jedoch an der Stelle, an der Sie begonnen haben, um zu überprüfen, ob diese Fähigkeiten vorhanden sind.
quelle
Für dieses spezielle Beispiel für " kann ein Kennwort zurückgesetzt werden " würde ich die Verwendung der Komposition anstelle der Vererbung empfehlen (in diesem Fall die Vererbung einer Schnittstelle / eines Vertrags). Denn auf diese Weise:
Sie geben sofort (zur Kompilierungszeit) an, dass Ihre Klasse ein Kennwort zurücksetzen kann . Wenn jedoch in Ihrem Szenario die Anwesenheit der Funktion von anderen Faktoren abhängt, können Sie diese beim Kompilieren nicht mehr angeben. Dann schlage ich vor:
Jetzt können Sie zur Laufzeit prüfen, ob
myFoo.passwordResetter != null
dies der Fall ist, bevor Sie diesen Vorgang ausführen. Wenn Sie noch mehr Dinge entkoppeln möchten (und viel mehr Funktionen hinzufügen möchten), können Sie:AKTUALISIEREN
Nachdem ich einige andere Antworten und Kommentare von OP gelesen hatte, verstand ich, dass es sich bei der Frage nicht um die kompositorische Lösung handelt. Ich denke, die Frage ist, wie die Fähigkeiten von Objekten im Allgemeinen in einem Szenario wie dem Folgenden besser identifiziert werden können:
Nun werde ich versuchen, alle genannten Optionen zu analysieren:
OP zweites Beispiel:
Angenommen, dieser Code erhält eine Basis-Account-Klasse (z. B .:
BaseAccount
in meinem Beispiel). Das ist schlecht, da es Boolesche Werte in die Basisklasse einfügt und sie mit Code verschmutzt, der überhaupt keinen Sinn ergibt, dort zu sein.OP erstes Beispiel:
Zur Beantwortung der Frage ist dies angemessener als die vorherige Option, aber je nach Implementierung wird das L-Prinzip des Solid gebrochen, und Überprüfungen wie diese würden wahrscheinlich über den Code verbreitet und die weitere Wartung erschweren.
CandiedOrange's Antwort:
Wenn diese
ResetPassword
Methode in eineBaseAccount
Klasse eingefügt wird , wird auch die Basisklasse mit unangemessenem Code belastet, wie im zweiten Beispiel von OPSchneemanns Antwort:
Dies ist eine gute Lösung, berücksichtigt jedoch, dass die Funktionen dynamisch sind (und sich im Laufe der Zeit ändern können). Nachdem ich jedoch einige Kommentare von OP gelesen habe, denke ich, dass es hier um Polymorphismus und statisch definierte Klassen geht (obwohl das Beispiel von Konten intuitiv auf das dynamische Szenario verweist). EG: in diesem
AccountManager
Beispiel wäre die Prüfung auf Erlaubnis eine Abfrage an DB; In der OP-Frage handelt es sich bei den Prüfungen um Versuche, die Objekte zu werfen.Ein weiterer Vorschlag von mir:
Verwenden Sie das Muster der Vorlagenmethode für Verzweigungen auf hoher Ebene. Die erwähnte Klassenhierarchie bleibt unverändert; Wir erstellen nur geeignetere Handler für die Objekte, um Umwandlungen und unangemessene Eigenschaften / Methoden zu vermeiden, die die Basisklasse belasten.
Sie können den Vorgang mithilfe eines Wörterbuchs / einer Hashtabelle an die entsprechende Kontoklasse binden oder Laufzeitvorgänge mithilfe von Erweiterungsmethoden ausführen, ein
dynamic
Schlüsselwort verwenden oder als letzte Option nur eine Besetzung verwenden, um das Kontoobjekt an den Vorgang (in In diesem Fall ist die Anzahl der Würfe zu Beginn der Operation nur eins.quelle
Keine der von Ihnen vorgestellten Optionen ist gut, oo. Wenn Sie if-Anweisungen für den Typ eines Objekts schreiben, machen Sie wahrscheinlich OO falsch (es gibt Ausnahmen, dies ist keine). Hier ist die einfache OO-Antwort auf Ihre Frage (möglicherweise ist C # ungültig):
Ich habe dieses Beispiel so geändert, dass es mit dem ursprünglichen Code übereinstimmt. Ausgehend von einigen Kommentaren scheint es, als würden sich die Leute darüber aufregen, ob dies der "richtige" spezifische Code ist. Das ist nicht der Sinn dieses Beispiels. Die gesamte polymorphe Überladung dient im Wesentlichen dazu, verschiedene Implementierungen von Logik abhängig vom Typ des Objekts bedingt auszuführen. Was Sie in beiden Beispielen tun, ist Hand-Jamming, was Ihre Sprache Ihnen als Feature bietet. Kurz gesagt, Sie könnten die Untertypen loswerden und das Zurücksetzen als boolesche Eigenschaft des Kontotyps festlegen (andere Funktionen der Untertypen ignorieren).
Ohne eine breitere Sicht auf das Design ist es unmöglich zu sagen, ob dies eine gute Lösung für Ihr bestimmtes System ist. Es ist einfach und wenn es für das, was Sie tun, funktioniert, müssen Sie wahrscheinlich nie wieder viel darüber nachdenken, es sei denn, jemand überprüft CanResetPassword () nicht, bevor er ResetPassword () aufruft. Sie können auch einen Booleschen Wert zurückgeben oder im Hintergrund fehlschlagen (nicht empfohlen). Es kommt wirklich auf die Besonderheiten des Designs an.
quelle
NotResetable
. "Vielleicht hat ein Konto mehr Funktionen, als dass es zurücksetzbar ist", würde ich erwarten, aber es scheint für die vorliegende Frage nicht wichtig zu sein.Antworten
Meine leicht einschätzige Antwort auf Ihre Frage lautet:
Nachtrag:
Wandern
Ich sehe, dass Sie das
c#
Tag nicht hinzugefügt haben , aber in einem allgemeinenobject-oriented
Kontext fragen .Was Sie beschreiben, ist eine technische Eigenschaft statischer / stark typisierter Sprachen und nicht spezifisch für "OO" im Allgemeinen. Ihr Problem ergibt sich unter anderem aus der Tatsache, dass Sie in diesen Sprachen keine Methoden aufrufen können, ohne zur Kompilierungszeit einen ausreichend engen Typ zu haben (entweder durch Variablendefinition, Methodenparameter- / Rückgabewertdefinition oder eine explizite Umwandlung). Ich habe in der Vergangenheit beide Ihrer Varianten in diesen Arten von Sprachen gemacht, und beide erscheinen mir heutzutage hässlich.
In dynamisch getippten OO-Sprachen tritt dieses Problem aufgrund eines Konzepts mit der Bezeichnung "duck typing" nicht auf. Kurz gesagt bedeutet dies, dass Sie den Typ eines Objekts grundsätzlich nirgendwo deklarieren (außer beim Erstellen). Sie senden Nachrichten an Objekte, und die Objekte verarbeiten diese Nachrichten. Es ist Ihnen egal, ob das Objekt tatsächlich eine Methode mit diesem Namen hat. Einige Sprachen haben sogar eine generische Catch-All-
method_missing
Methode, die dies noch flexibler macht.In einer solchen Sprache (z. B. Ruby) lautet Ihr Code also:
Zeitraum.
Das
account
wird entweder das Passwort zurücksetzen, eine "verweigerte" Ausnahme auslösen oder eine "Weiß nicht, wie mit" reset_password "" - Ausnahme umgegangen wird.Wenn Sie aus irgendeinem Grund explizit verzweigen müssen, würden Sie dies immer noch für die Fähigkeit tun, nicht für die Klasse:
(Wo
can?
ist nur eine Methode, die prüft, ob das Objekt eine Methode ausführen kann, ohne sie aufzurufen.)Beachten Sie, dass meine persönliche Präferenz derzeit dynamisch getippten Sprachen entspricht, dies jedoch nur eine persönliche Präferenz ist. Statisch getippte Sprachen haben auch einiges zu bieten, nehmen Sie diese Streifzüge also nicht als Bash gegen statisch getippte Sprachen ...
quelle
Es scheint, dass Ihre Optionen von verschiedenen Motivationskräften abgeleitet sind. Der erste scheint ein Hack zu sein, der aufgrund einer schlechten Objektmodellierung eingesetzt wird. Es hat gezeigt, dass Ihre Abstraktionen falsch sind, da sie den Polymorphismus brechen. Wahrscheinlich bricht es das Open-Closed-Prinzip , was zu einer engeren Kopplung führt.
Ihre zweite Option könnte aus OOP-Sicht in Ordnung sein, aber das Typ-Casting verwirrt mich. Wenn Sie mit Ihrem Konto das Kennwort zurücksetzen können, warum benötigen Sie dann eine Typumwandlung? Unter der Annahme, dass Ihre
CanResetPassword
Klausel eine grundlegende Geschäftsanforderung darstellt, die Teil der Abstraktion des Kontos ist, ist Ihre zweite Option in Ordnung.quelle