it-swarm.com.de

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

Mit dem Operator is von C # und dem Operator instanceof von Java können Sie auf der Schnittstelle (oder, genauer gesagt, auf der Basisklasse) verzweigen, die eine Objektinstanz implementiert hat.

Ist es angemessen, diese Funktion für Verzweigungen auf hoher Ebene zu verwenden, basierend auf den Funktionen, die eine Schnittstelle bietet?

Oder sollte eine Basisklasse boolesche Variablen bereitstellen, um eine Schnittstelle zur Beschreibung der Funktionen eines Objekts bereitzustellen?

Beispiel:

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

vs.

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

Gibt es Fallstricke bei der Verwendung der Schnittstellenimplementierung zur Identifizierung von Fähigkeiten?


Dieses Beispiel war genau das, ein Beispiel. Ich wundere mich über eine allgemeinere Anwendung.

36
TheCatWhisperer

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

  1. sie lassen das Typsystem für Sie arbeiten. Wenn im ersten Beispiel is ausgelöst wird, funktioniert der Downcast definitiv. Im zweiten Formular müssen Sie manuell sicherstellen, dass nur Objekte, die IResetsPassword implementieren, 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 etwas verbessern können. Sie könnten beispielsweise dafür sorgen, dass die Basisklasse über eine Reihe veröffentlichter Schnittstellen verfügt, in die Sie die Aufnahme überprüfen können. Sie implementieren jedoch nur manuell einen Teil des vorhandenen Typsystems, der redundant ist.

Übrigens ist meine natürliche Präferenz 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, die bereits vorhandene zu verwenden, da der Compiler Ihnen dabei hilft.

7
Alex

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

Die Fähigkeiten eines Objekts sollten überhaupt nicht identifiziert werden.

Der Client, der ein Objekt verwendet, sollte nicht wissen müssen, wie es funktioniert. Der Client sollte nur Dinge wissen, die er dem Objekt mitteilen kann. Was das Objekt tut, wenn es einmal gesagt wurde, ist nicht das Problem des Kunden.

Also eher 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);

weil account bereits weiß, ob dies funktionieren wird. Warum fragen? Sagen Sie ihm einfach, was Sie wollen, und lassen Sie es tun, was auch immer es tun wird.

Stellen Sie sich vor, dies geschieht so, dass es dem Kunden egal ist, ob es funktioniert oder nicht, weil dies ein anderes Problem ist. Die Aufgabe des Kunden bestand nur darin, den Versuch zu machen. Das ist jetzt erledigt und es gibt noch andere Dinge zu erledigen. Dieser Stil hat viele Namen, aber der Name, den ich am meisten mag, ist sagen, nicht fragen .

Es ist sehr verlockend, wenn Sie dem Kunden schreiben, dass Sie alles im Auge behalten und so alles zu sich ziehen müssen, was Sie zu wissen glauben. Wenn Sie das tun, drehen Sie Objekte um. Schätzen Sie Ihre Unwissenheit. Schieben Sie die Details weg und lassen Sie die Objekte damit umgehen. Je weniger Sie wissen, desto besser.

89
candied_orange

Hintergrund

Vererbung ist ein leistungsfähiges Werkzeug, das einen Zweck in der objektorientierten Programmierung erfüllt. Es löst jedoch nicht jedes Problem elegant: 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 gibt, in dem angegeben ist, 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 geben, die aussehen, als wären sie wichtig, aber nicht. Es besteht auch die sehr reale Möglichkeit, dass Anforderungen falsch interpretiert werden.

Dies ist eine wichtige Fähigkeit, die selbst die erfahrensten von uns falsch machen: Anforderungen richtig identifizieren und in Maschinensprachen übersetzen.

Ihre Frage

Basierend auf dem, was Sie geschrieben haben, denke ich, dass Sie den allgemeinen Fall optionaler Aktionen, die Klassen ausführen können, möglicherweise falsch verstehen. Ja, ich weiß, dass Ihr Code nur ein Beispiel ist und Sie sich für den allgemeinen Fall interessieren. 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 account einen Kontotyp hat, bedeutet dies 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 verwenden, um eine Aktion auszuführen, aber diese Aktion kann möglicherweise nicht ausgeführt werden Anstatt Vererbung zu verwenden, würde ich einen Delegaten oder ein Sicherheitstoken verwenden.

Meine Lösung

Stellen Sie sich eine Kontoklasse vor, für die mehrere optionale Aktionen ausgeführt werden können. Lassen Sie das Konto nicht ein Delegatenobjekt (Kennwort-Resetter, Formularübermittler usw.) oder ein Zugriffstoken zurückgeben, anstatt "Aktion X kann über Vererbung ausführen" zu definieren.

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

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

Der Vorteil der letzten Option besteht darin, dass ich meine Kontoanmeldeinformationen verwenden möchte, um die (-) anderer Personen zurückzusetzen Passwort?

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

Das System ist nicht nur flexibler, ich habe nicht nur Typumwandlungen entfernt, sondern das Design ist auch ausdrucksstärker . Ich kann dies lesen und leicht verstehen, was es tut und wie es verwendet werden kann.


TL; DR: Dies liest sich wie ein XY-Problem . Im Allgemeinen lohnt es sich, einen Schritt zurückzutreten und zu überlegen, was ich wirklich hier wirklich erreichen möchte. Sollte ich wirklich Denken Sie darüber nach, wie Sie die Typografie weniger hässlich machen können, oder sollte ich nach Möglichkeiten suchen, die Typografie vollständig zu entfernen? "

17
user22815

Bill Venner sagt, dass Ihr Ansatz absolut in Ordnung ist ; Wechseln Sie zum Abschnitt "Wann wird die Instanz von verwendet?". Sie übertragen auf einen bestimmten Typ/eine bestimmte Schnittstelle, die nur dieses bestimmte Verhalten implementiert, sodass Sie absolut Recht haben, selektiv zu sein.

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 Basis-Account-Klasse eine resetPassword() -Methode haben sollten, die alle Accounts auf ihre eigene Weise implementieren. Die Frage ist, wie sich diese Methode verhalten soll, wenn die Funktion nicht vorhanden ist.

Es kann eine Leere zurückgeben und stillschweigend vervollständigen, ob das Kennwort zurückgesetzt wurde oder nicht, und möglicherweise die Verantwortung für das interne Ausdrucken der Nachricht übernehmen, wenn das Kennwort nicht zurückgesetzt wird. Nicht die beste Idee.

Es könnte einen Booleschen Wert zurückgeben, der angibt, ob das Zurücksetzen erfolgreich war. Wenn Sie diesen Booleschen Wert einschalten, können Sie feststellen, dass das Zurücksetzen des Kennworts fehlgeschlagen ist.

Es könnte eine Zeichenfolge zurückgeben, die das Ergebnis des Kennwortrücksetzversuchs angibt. Die Zeichenfolge kann weitere Details dazu enthalten, warum ein Zurücksetzen fehlgeschlagen ist und ausgegeben werden kann.

Es könnte ein ResetResult-Objekt zurückgeben, das mehr Details übermittelt und alle vorherigen Rückgabeelemente kombiniert.

Es könnte eine Leere zurückgeben und stattdessen eine Ausnahme auslösen, wenn Sie versuchen, ein Konto zurückzusetzen, das nicht über diese Funktion verfügt (tun Sie dies nicht, da die Verwendung der Ausnahmebehandlung für die normale Flusskontrolle aus verschiedenen Gründen eine schlechte Praxis ist).

Eine passende canResetPassword() -Methode zu haben, scheint vielleicht nicht das Schlimmste auf der Welt zu sein, da es sich fiktiv um eine statische Funktion handelt, die in die Klasse integriert war, als sie geschrieben wurde. Dies ist jedoch ein Hinweis darauf, warum der Methodenansatz eine schlechte Idee ist, da er darauf hindeutet, dass die Fähigkeit dynamisch ist und dass sich canResetPassword() möglicherweise ändert, was auch die zusätzliche Möglichkeit schafft, dass sie sich zwischen der Anfrage ändert Erlaubnis und Anruf tätigen. Erzählen Sie, wie an anderer Stelle erwähnt, anstatt um Erlaubnis zu bitten.

Die Zusammensetzung über der Vererbung könnte eine Option sein: Sie könnten ein letztes passwordResetter -Feld (oder einen äquivalenten Getter) und äquivalente Klassen haben, auf die Sie vor dem Aufruf auf null prüfen können. Während es sich ein bisschen so verhält, als würde man um Erlaubnis bitten, könnte die Endgültigkeit jede abgeleitete dynamische Natur vermeiden.

Sie könnten überlegen, die Funktionalität in eine eigene Klasse zu verlagern, die einen Parameter berücksichtigt und darauf einwirkt (z. B. resetter.reset(account)), obwohl dies häufig auch eine schlechte Praxis ist (eine gängige Analogie ist, dass ein Ladenbesitzer darauf zugreift) Ihre Brieftasche, um Bargeld zu bekommen).

In Sprachen mit dieser Funktion können Sie Mixins oder Merkmale verwenden. Sie befinden sich jedoch an der Stelle, an der Sie begonnen haben und an der Sie möglicherweise prüfen, ob diese Funktionen vorhanden sind.

1
Danikov

Für dieses spezielle Beispiel von " kann ein Passwort zurückgesetzt werden ", ich würde empfehlen, Komposition über Vererbung zu verwenden (in diesem Fall , Vererbung einer Schnittstelle/eines Vertrags). Denn auf diese Weise:

class Foo : IResetsPassword {
    //...
}

Sie geben sofort (zur Kompilierungszeit) an, dass Ihre Klasse ' ein Passwort zurücksetzen kann '. Wenn jedoch in Ihrem Szenario die Anwesenheit der Funktion von Bedingungen abhängig ist und von anderen Dingen abhängt, können Sie zur Kompilierungszeit keine Dinge mehr angeben. Dann schlage ich vor, dies zu tun:

class Foo {

    PasswordResetter passwordResetter;

}

Jetzt können Sie zur Laufzeit überprüfen, ob myFoo.passwordResetter != null bevor Sie diesen Vorgang ausführen. Wenn Sie noch mehr Dinge entkoppeln möchten (und weitere 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...

UPDATE

Nachdem ich einige andere Antworten und Kommentare von OP gelesen hatte, verstand ich, dass es bei der Frage nicht um die kompositorische Lösung geht. Ich denke also, die Frage ist, wie man die Fähigkeiten von Objekten im Allgemeinen in einem Szenario wie dem folgenden besser identifizieren kann:

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, ...);
}

Jetzt 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 empfängt eine Basiskontoklasse (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 macht, dort zu sein.

OP erstes Beispiel :

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

Um die Frage zu beantworten, ist dies angemessener als die vorherige Option, aber abhängig von der Implementierung wird das L-Prinzip des Solid gebrochen, und wahrscheinlich würden solche Überprüfungen über den Code verteilt und die weitere Wartung erschweren.

CandiedOranges Anser :

account.ResetPassword(authority);

Wenn diese Methode ResetPassword in die Klasse BaseAccount eingefügt wird, verschmutzt sie auch die Basisklasse mit unangemessenem Code, wie im zweiten Beispiel von OP

Antwort des Schneemanns :

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, geht es hier vermutlich um Polymorphismus und statisch definierte Klassen (obwohl das Beispiel von Konten intuitiv auf das dynamische Szenario hinweist). EG: In diesem AccountManager Beispiel wäre die Prüfung auf Berechtigung Abfragen an DB; In der OP-Frage sind die Prüfungen Versuche, die Objekte zu gießen.

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 Casts und unangemessene Eigenschaften/Methoden zu vermeiden, die die Basisklasse verschmutzen.

//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 die Operation mithilfe eines Wörterbuchs/einer Hashtabelle an die entsprechende Kontoklasse binden oder Laufzeitoperationen mit Erweiterungsmethoden ausführen, das Schlüsselwort dynamic verwenden oder als letzte Option nur eine Umwandlung verwenden = um das Kontoobjekt an die Operation zu übergeben (in diesem Fall beträgt die Anzahl der Casts zu Beginn der Operation nur eine).

0
Emerson Cardoso

Keine der von Ihnen vorgestellten Optionen ist eine gute OO. Wenn Sie if-Anweisungen um den Typ eines Objekts schreiben, machen Sie höchstwahrscheinlich OO falsch (es gibt Ausnahmen, dies ist keine.) Hier ist das einfache OO Antwort auf Ihre Frage (möglicherweise nicht gültig C #):

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. Basierend auf einigen Kommentaren scheint es, dass die Leute sich darüber aufregen, ob dies hier der "richtige" spezifische Code ist. Darum geht es in diesem Beispiel nicht. Die gesamte polymorphe Überladung besteht im Wesentlichen darin, verschiedene Implementierungen der Logik basierend auf dem 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 entfernen und die Möglichkeit zum Zurücksetzen als boolesche Eigenschaft des Kontotyps festlegen (andere Funktionen der Untertypen werden ignoriert).

Ohne eine breitere Sicht auf das Design ist es unmöglich zu sagen, ob dies eine gute Lösung für Ihr spezielles System ist. Es ist einfach und wenn es für das funktioniert, was Sie tun, müssen Sie wahrscheinlich nie wieder viel darüber nachdenken, es sei denn, jemand kann CanResetPassword () nicht überprüfen, bevor er ResetPassword () aufruft. Sie können auch einen Booleschen Wert zurückgeben oder im Hintergrund fehlschlagen (nicht empfohlen). Es hängt wirklich von den Besonderheiten des Designs ab.

0
JimmyJames

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. Höchstwahrscheinlich bricht es The Open Closed Principle , was zu einer engeren Kopplung führt.

Ihre zweite Option könnte aus der Perspektive von OOP] 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? Nehmen Sie also an, dass Ihr CanResetPassword stellt eine grundlegende Geschäftsanforderung dar, die Teil der Abstraktion des Kontos ist. Ihre zweite Option ist OK.

0
Zapadlo

Antworten

Meine leicht meinungsgebundene 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 wird diese Möglichkeit jedoch einschränken (beabsichtigt).

Nachtrag:

Das Verzweigen nach Objekttyp ist böse.

Wandern

Ich sehe, dass Sie das Tag c# Nicht hinzugefügt haben, sondern in einem allgemeinen Kontext object-oriented Fragen.

Was Sie beschreiben, ist eine Technik statischer/stark typisierter Sprachen und nicht spezifisch für "OO" im Allgemeinen. Ihr Problem ergibt sich unter anderem aus der Tatsache, dass Sie Methoden in diesen Sprachen nicht 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 Varianten in diesen Sprachen gemacht, und beide scheinen mir heutzutage hässlich zu sein.

In dynamisch typisierten OO Sprachen) tritt dieses Problem aufgrund eines Konzepts namens "Ententypisierung" nicht auf. Kurz gesagt bedeutet dies, dass Sie den Typ eines Objekts grundsätzlich nirgendwo deklarieren (außer wenn) 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 einen generischen Sammelbegriff method_missing Methode, die dies noch flexibler macht.

In einer solchen Sprache (z. B. Ruby) wird Ihr Code zu:

account.reset_password

Zeitraum.

Das account setzt entweder das Passwort zurück, löst eine "verweigerte" Ausnahme aus oder löst eine "weiß nicht, wie man mit 'reset_password' behandelt" -Ausnahme aus.

Wenn Sie aus irgendeinem Grund explizit verzweigen müssen, tun Sie dies immer noch für die Funktion, nicht für die Klasse:

account.reset_password  if account.can? :reset_password

(Wobei can? Nur eine Methode ist, die prüft, ob das Objekt eine Methode ausführen kann, ohne sie aufzurufen.)

Beachten Sie, dass meine persönliche Präferenz derzeit dynamisch typisierte Sprachen sind, dies jedoch nur eine persönliche Präferenz ist. Statisch typisierte Sprachen haben auch etwas zu bieten. Nehmen Sie dieses Streifzug also nicht als Schlag gegen statisch typisierte Sprachen ...

0
AnoE