Sollten die Fähigkeiten eines Objekts ausschließlich durch die von ihm implementierten Schnittstellen identifiziert werden?


36

Mit isdem instanceofOperator 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.


10
Schauen Sie sich Ihre beiden Codebeispiele an. Welches ist besser lesbar?
Robert Harvey

39
Nur meine Meinung, aber ich würde mit der ersten gehen, nur weil es sicherstellt, dass Sie sicher auf den entsprechenden Typ werfen können. Der zweite geht davon aus, dass dies CanResetPasswordnur dann der Fall ist, wenn etwas implementiert wird IResetsPassword. Jetzt legen Sie mit einer Eigenschaft fest, was das Typsystem steuern soll (und letztendlich, wenn Sie den Cast vorbereiten).
Becuzz

10
@nocomprende: xkcd.com/927
Robert Harvey

14
Ihr Fragentitel und Ihr Fragenkörper stellen verschiedene Fragen. Um den Titel zu beantworten: Idealerweise ja. So richten Sie sich an den Körper: Wenn Sie feststellen, dass Sie den Typ überprüfen und manuell gießen, nutzen Sie Ihr Typensystem wahrscheinlich nicht richtig. Können Sie uns genauer sagen, wie Sie sich in dieser Situation befunden haben?
MetaFight

9
Diese Frage mag einfach erscheinen, wird aber ziemlich weit gefasst, wenn Sie darüber nachdenken. Fragen, die mir in den Sinn kommen: Erwarten Sie, vorhandenen Konten neue Verhaltensweisen hinzuzufügen oder Kontotypen mit unterschiedlichen Verhaltensweisen zu erstellen? Haben alle Kontotypen ein ähnliches Verhalten oder gibt es große Unterschiede zwischen ihnen? Kennt der Beispielcode alle Kontotypen oder nur die grundlegende IAccount-Schnittstelle?
Euphoric

Antworten:


7

Mit der Einschränkung, dass es am besten ist, Downcasting zu vermeiden, ist die erste Form aus zwei Gründen vorzuziehen:

  1. Sie lassen das Typensystem für sich arbeiten. Im ersten Beispiel wird isder Downcast definitiv funktionieren , wenn die Brände ausbrechen. Im zweiten Formular müssen Sie manuell sicherstellen, dass nur Objekte, die implementieren IResetsPassword, für diese Eigenschaft true zurückgeben
  2. fragile Basisklasse. Sie müssen der Basisklasse / Schnittstelle für jede Schnittstelle, die Sie hinzufügen möchten, eine Eigenschaft hinzufügen. Dies ist umständlich und fehleranfällig.

Das 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.


Ich bevorzuge auch das kompositorische Schreiben. +1, damit ich sehe, dass dieses Beispiel es nicht richtig nutzt. Ich würde jedoch sagen, dass die rechnergestützte Typisierung (wie die traditionelle Vererbung) eingeschränkter ist, wenn die Funktionen stark variieren (im Gegensatz zu der Art und Weise, wie sie implementiert werden). Daher der Schnittstellenansatz, der ein anderes Problem löst als die kompositorische Typisierung oder die Standardvererbung.
TheCatWhisperer

Sie könnten die Prüfung wie (account as IResettable)?.ResetPassword();var resettable = account as IResettable; if(resettable != null) {resettable.ResetPassword()} else {....}
folgt

89

Sollten die Fähigkeiten eines Objekts ausschließlich durch die von ihm implementierten Schnittstellen identifiziert werden?

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

if (account is IResetsPassword)
    ((IResetsPassword)account).ResetPassword();
else
    Print("Not allowed to reset password with this account type!");

oder

if (account.CanResetPassword)
    ((IResetsPassword)account).ResetPassword();
else
    Print("Not allowed to reset password with this account type!");

Erwägen

account.ResetPassword();

oder

account.ResetPassword(authority);

accountweiß 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.


54
Polymorphismus funktioniert nur, wenn ich nicht genau weiß oder mich nicht darum kümmere, womit ich rede. Ich gehe mit dieser Situation um, indem ich sie sorgfältig vermeide.
candied_orange

7
Wenn der Client wirklich wissen muss, ob der Versuch erfolgreich war, kann die Methode einen Booleschen Wert zurückgeben. 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.
Radiodef

5
@DavidZ Wir sprechen beide Englisch. Daher kann ich Ihnen sagen, dass Sie diesen Kommentar zweimal positiv bewerten müssen. Heißt das, Sie sind dazu in der Lage? Muss ich wissen, ob Sie es können, bevor ich Ihnen sagen kann, dass Sie es tun sollen?
candied_orange

5
@QPaysTaxes für alles, was Sie wissen, ein Fehlerhandler wurde übergeben, accountals 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.
candied_orange

6
@QPaysTaxes Es könnte anderswo und auf verschiedene Arten gehandhabt werden, es ist überflüssig für dieses Gespräch und würde nur das Wasser trüben.
Tom.Bowen89

17

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 accountKontotyp 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.

account.getPasswordResetter().doAction();
account.getFormSubmitter().doAction(view.getContents());

AccountManager.resetPassword(account, account.getAccessToken());

Der Vorteil der letzten Option besteht darin, dass ich mithilfe meiner Kontoanmeldeinformationen das Kennwort eines anderen Benutzers zurücksetzen möchte .

AccountManager.resetPassword(otherAccount, adminAccount.getAccessToken());

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? "


1
+1 für das Design riecht, auch wenn Sie die Phrase nicht verwendet haben.
Jared Smith

Was ist mit Fällen, in denen das Zurücksetzen des Kennworts je nach Typ völlig unterschiedliche Argumente hat? ResetPassword (pswd) vs ResetPasswordToRandom ()
TheCatWhisperer

1
@TheCatWhisperer Das erste Beispiel ist "Passwort ändern", was eine andere Aktion ist. Das zweite Beispiel ist das, was ich in dieser Antwort beschreibe.

Warum nicht account.getPasswordResetter().resetPassword();.
AnoE

1
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).
Voo

1

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 sich canResetPassword()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 passwordResetterFeld (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.


Möglicherweise haben nicht alle Konten ein Kennwort (aus der Sicht dieses bestimmten Systems ohnehin). Zum Beispiel kann der Account ein Dritter sein ... google, facebook, ect. Um nicht zu sagen, dass dies keine gute Antwort ist!
TheCatWhisperer

0

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:

class Foo : IResetsPassword {
    //...
}

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:

class Foo {

    PasswordResetter passwordResetter;

}

Jetzt können Sie zur Laufzeit prüfen, ob myFoo.passwordResetter != nulldies 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:

class Foo {
    //... foo stuff
}

class PasswordResetOperation {
    bool Execute(Foo foo) { ... }
}

class SendMailOperation {
    bool Execute(Foo foo) { ... }
}

//...and you follow this pattern for each new capability...

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:

class BaseAccount {
    //...
}
class GuestAccount : BaseAccount {
    //...
}
class UserAccount : BaseAccount, IMyPasswordReset, IEditPosts {
    //...
}
class AdminAccount : BaseAccount, IPasswordReset, IEditPosts, ISendMail {
    //...
}

//Capabilities

interface IMyPasswordReset {
    bool ResetPassword();
}

interface IPasswordReset {
    bool ResetPassword(UserAccount userAcc);
}

interface IEditPosts {
    bool EditPost(long postId, ...);
}

interface ISendMail {
    bool SendMail(string from, string to, ...);
}

Nun werde ich versuchen, alle genannten Optionen zu analysieren:

OP zweites Beispiel:

if (account.CanResetPassword)
       ((IResetsPassword)account).ResetPassword();
else
       Print("Not allowed to reset password with this account type!");

Angenommen, dieser Code erhält eine Basis-Account-Klasse (z. B .: BaseAccountin 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:

if (account is IResetsPassword)
       ((IResetsPassword)account).ResetPassword();
else
       Print("Not allowed to reset password with this account type!");

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:

account.ResetPassword(authority);

Wenn diese ResetPasswordMethode in eine BaseAccountKlasse eingefügt wird , wird auch die Basisklasse mit unangemessenem Code belastet, wie im zweiten Beispiel von OP

Schneemanns Antwort:

AccountManager.resetPassword(otherAccount, adminAccount.getAccessToken());

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 AccountManagerBeispiel 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.

//Template method
class BaseAccountOperation {

    BaseAccount account;

    void Execute() {

        //... some processing

        TryResetPassword();

        //... some processing

        TrySendMail();

        //... some processing
    }

    void TryResetPassword() {
        Print("Not allowed to reset password with this account type!");
    }

    void TrySendMail() {
        Print("Not allowed to reset password with this account type!");
    }
}

class UserAccountOperation : BaseAccountOperation {

    UserAccount userAccount;

    void TryResetPassword() {
        account.ResetPassword(...);
    }

}

class AdminAccountOperation : BaseAccountOperation {

    AdminAccount adminAccount;

    override void TryResetPassword() {
        account.ResetPassword(...);
    }

    void TrySendMail() {
        account.SendMail(...);
    }
}

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 dynamicSchlü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.


1
Würde es funktionieren, nur immer die "Execute ()" - Methode zu implementieren, aber manchmal nichts zu tun? Es gibt einen Bool zurück, also schätze ich, dass es das bedeutet oder nicht. Das gibt es an den Client zurück: "Ich habe versucht, das Passwort zurückzusetzen, aber es ist nicht geschehen (aus welchem ​​Grund auch immer), also sollte ich jetzt ..."

4
Die Methoden "canX ()" und "doX ()" sind ein Antipattern: Wenn eine Schnittstelle eine "doX ()" - Methode verfügbar macht, ist es besser, X zu tun, selbst wenn X ein No-Op bedeutet (z. B. Null- Objektmuster) ). Es sollte den Benutzern einer Schnittstelle niemals obliegen, sich mit der zeitlichen Kopplung zu befassen (Methode A vor Methode B aufrufen), da dies zu einfach ist, Fehler zu machen und Probleme zu verursachen.

1
Das Vorhandensein von Schnittstellen hat keinen Einfluss auf die Verwendung von Komposition gegenüber Vererbung. Sie stellen lediglich eine verfügbare Funktionalität dar. Wie diese Funktionalität ins Spiel kommt, ist unabhängig von der Verwendung der Schnittstelle. Was Sie hier getan haben, ist die Verwendung einer Ad-hoc-Implementierung einer Schnittstelle ohne die Vorteile.
Miyamoto Akira

Interessant: Die am häufigsten gewählte Antwort sagt im Grunde das aus, was mein Kommentar oben sagt. An manchen Tagen hat man einfach Glück.

@nocomprende - ja, ich habe meine Antwort anhand mehrerer Kommentare aktualisiert. Vielen Dank für die Rückmeldungen :)
Emerson Cardoso

0

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):

interface IAccount {
  bool CanResetPassword();

  void ResetPassword();

  // Other Account operations as needed
}

public class Resetable : IAccount {
  public bool CanResetPassword() {
    return true;
  }

  public void ResetPassword() {
    /* RESET PASSWORD */
  }
}

public class NotResetable : IAccount {
  public bool CanResetPassword() {
    return false;
  }

  public void ResetPassword() {
    Print("Not allowed to reset password with this account type!");}
  }

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.


Nicht alle Konten können zurückgesetzt werden. Hat ein Konto möglicherweise mehr Funktionen, als zurückgesetzt werden zu können?
TheCatWhisperer

2
"Nicht alle Konten können zurückgesetzt werden." Ich bin mir nicht sicher, ob dies vor der Bearbeitung geschrieben wurde. Zweite Klasse ist 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.
JimmyJames

1
Diese Lösung geht in die richtige Richtung, denke ich, da sie innerhalb von OO eine gute Lösung darstellt. Wahrscheinlich würde der Name des Interfaces in IPasswordReseter (oder etwas in diesem Sinne) geändert, um Interfaces zu trennen. IAccount kann als mehrere andere Schnittstellen unterstützend deklariert werden. Das würde Ihnen die Option geben, Klassen mit der Implementierung zu haben, die dann zusammengesetzt und von der Delegierung für die Domänenobjekte verwendet werden. @JimmyJames, minor nitpick, in C # müssen beide Klassen angeben, dass sie IAccount implementieren. Ansonsten sieht der Code etwas seltsam aus ;-)
Miyamoto Akira

1
Überprüfen Sie auch einen Kommentar von Snowman in einer der anderen Antworten zu CanX () und DoX (). Es ist jedoch nicht immer ein Anti-Pattern, wie es verwendet wird (zum Beispiel zum Verhalten der Benutzeroberfläche)
Miyamoto Akira

1
Diese Antwort fängt gut an: das ist schlecht OOP. Leider ist Ihre Lösung genauso schlecht, weil sie auch das Typsystem untergräbt, aber eine Ausnahme auslöst. Eine gute Lösung sieht anders aus: Sie haben keine nicht implementierten Methoden für einen Typ. Das .NET Framework verwendet Ihren Ansatz tatsächlich für einige Typen, aber das war eindeutig ein Fehler.
Konrad Rudolph

0

Antworten

Meine leicht einschätzige Antwort auf Ihre Frage lautet:

Die Fähigkeiten eines Objekts sollten durch sich selbst identifiziert werden, nicht durch die Implementierung einer Schnittstelle. Eine statisch stark typisierte Sprache schränkt diese Möglichkeit jedoch ein (beabsichtigt).

Nachtrag:

Das Verzweigen auf Objekttyp ist böse.

Wandern

Ich sehe, dass Sie das c#Tag nicht hinzugefügt haben , aber in einem allgemeinen object-orientedKontext 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_missingMethode, die dies noch flexibler macht.

In einer solchen Sprache (z. B. Ruby) lautet Ihr Code also:

account.reset_password

Zeitraum.

Das accountwird 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:

account.reset_password  if account.can? :reset_password

(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 ...


Schön, dass Sie Ihre Perspektive hier haben! Wir auf der Java / C # -Seite neigen dazu zu vergessen, dass ein anderer Zweig von OOP existiert. Wenn ich meinen Kollegen sage, dass JS (für all seine Probleme) in vielerlei Hinsicht mehr OO als C # ist, lachen sie mich aus!
TheCatWhisperer

Ich liebe C #, und ich denke, es ist eine gute Allzwecksprache , aber ich persönlich bin der Meinung, dass es in Bezug auf OO ziemlich schlecht ist. Sowohl in den Merkmalen als auch in der Praxis fördert es tendenziell die prozedurale Programmierung.
TheCatWhisperer

2
Das Testen auf das Vorhandensein einer Methode in einer Sprache vom Typ "Ente" ist wahrscheinlich dasselbe wie das Testen auf die Implementierung einer Schnittstelle vom Typ "Statisch": Sie führen eine Laufzeitprüfung durch, um festzustellen, ob das Objekt über eine bestimmte Funktion verfügt. Jede Methodendeklaration ist im Grunde genommen eine eigene Schnittstelle, die nur durch den Methodennamen identifiziert wird. Ihre Existenz kann nicht dazu verwendet werden, andere Informationen über das Objekt abzuleiten.
IMSoP

0

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 CanResetPasswordKlausel eine grundlegende Geschäftsanforderung darstellt, die Teil der Abstraktion des Kontos ist, ist Ihre zweite Option in Ordnung.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.