Sollte ein Getter eine Ausnahme auslösen, wenn sein Objekt einen ungültigen Status hat?

55

Ich stoße oft auf dieses Problem, besonders in Java, auch wenn ich denke, dass es sich um ein allgemeines OOP-Problem handelt. Das heißt: Das Auslösen einer Ausnahme zeigt ein Konstruktionsproblem auf.

Angenommen, ich habe eine Klasse mit einem String nameFeld und einem String surnameFeld.

In diesen Feldern wird dann der vollständige Name einer Person angegeben, um ihn auf einem Dokument, beispielsweise einer Rechnung, anzuzeigen.

public void String name;
public void String surname;

public String getCompleteName() {return name + " " + surname;}

public void displayCompleteNameOnInvoice() {
    String completeName = getCompleteName();
    //do something with it....
}

Jetzt möchte ich das Verhalten meiner Klasse stärken, indem ich einen Fehler auslöse, wenn der displayCompleteNameOnInvoiceaufgerufen wird, bevor der Name vergeben wurde. Es scheint eine gute Idee zu sein, nicht wahr?

Ich kann der getCompleteNameMethode einen Code hinzufügen, der Ausnahmen auslöst . Auf diese Weise verstoße ich jedoch gegen einen "impliziten" Vertrag mit dem Klassenbenutzer. Im Allgemeinen dürfen Getter keine Ausnahmen auslösen, wenn ihre Werte nicht festgelegt sind. Ok, dies ist kein Standard-Getter, da kein einzelnes Feld zurückgegeben wird, aber aus Sicht des Benutzers ist die Unterscheidung möglicherweise zu subtil, um darüber nachzudenken.

Oder ich kann die Ausnahme von innen werfen displayCompleteNameOnInvoice. Aber dazu sollte ich direkt testen nameoder surnameFelder und damit würde ich die durch dargestellte Abstraktion verletzen getCompleteName. Es liegt in der Verantwortung dieser Methode, den vollständigen Namen zu überprüfen und zu erstellen. Es könnte sogar auf der Grundlage anderer Daten entscheiden, dass es in einigen Fällen ausreicht, die surname.

So die einzige Möglichkeit scheint die semantischen des Verfahrens zu ändern , getCompleteNamezu composeCompleteName, dass ein ‚aktives‘ legt nahe , Verhalten und damit auch die Fähigkeit , eine Ausnahme zu werfen.

Ist das die bessere Designlösung? Ich bin immer auf der Suche nach der besten Balance zwischen Einfachheit und Korrektheit. Gibt es eine Designreferenz für dieses Problem?

AgostinoX
quelle
8
"Im Allgemeinen dürfen Getter keine Ausnahmen auslösen, wenn ihre Werte nicht festgelegt sind." - Was sollen sie dann tun?
Rotem
2
@scriptin Wenn Sie dieser Logik folgen, displayCompleteNameOnInvoicekönnen Sie einfach eine Ausnahme auslösen, wenn sie getCompleteNamezurückgegeben wird null, nicht wahr ?
Rotem
2
@Rotem kann es. Die Frage ist, ob es sollte.
Skript
22
Zu diesem Thema ist Falsehoods Programmers Believe About Names eine sehr gute Lektüre darüber, warum 'Vorname' und 'Nachname' sehr enge Begriffe sind.
Lars Viklund
9
Wenn Ihr Objekt einen ungültigen Status haben kann, haben Sie es bereits vermasselt.
user253751

Antworten:

151

Lassen Sie nicht zu, dass Ihre Klasse ohne Zuweisung eines Namens erstellt wird.

DeadMG
quelle
9
Es ist interessant, aber eine radikale Lösung. Oft müssen Ihre Klassen flüssiger sein, was Zustände zulässt, die nur dann zu einem Problem werden, wenn bestimmte Methoden anderweitig aufgerufen werden. Dies führt zu einer Vielzahl kleiner Klassen, deren Verwendung zu viel Erklärung erfordert.
AgostinoX
71
Dies ist kein Kommentar. Wenn er den Rat in der Antwort anwendet, hat er das beschriebene Problem nicht mehr. Es ist eine Antwort.
DeadMG
14
@AgostinoX: Du solltest deine Klassen nur flüssig machen, wenn es absolut notwendig ist. Ansonsten sollten sie so unveränderlich wie möglich sein.
DeadMG
25
@gnat: Du machst hier eine großartige Arbeit, indem du die Qualität jeder Frage und jeder Antwort auf PSE in Frage stellst, aber manchmal bist du meiner Meinung nach ein bisschen zu schlau.
Doc Brown
9
Wenn sie gültig optional sein können, gibt es keinen Grund für ihn, an erster Stelle zu werfen.
DeadMG
75

Die von DeadMG bereitgestellte Antwort nagelt es ziemlich genau, aber lassen Sie es mich etwas anders formulieren: Vermeiden Sie Objekte mit ungültigem Status. Ein Objekt sollte im Kontext der Aufgabe, die es erfüllt, "ganz" sein. Oder es sollte nicht existieren.

Wenn Sie also Fluidität wünschen, verwenden Sie so etwas wie das Builder-Muster . Oder irgendetwas anderes, das eine separate Verifizierung eines Objekts in der Konstruktion im Gegensatz zu der "realen Sache" ist, wobei letzterer garantiert einen gültigen Zustand hat und derjenige ist, der tatsächlich die für diesen Zustand definierten Operationen verfügbar macht (z getCompleteName. B. ).

back2dos
quelle
7
So if you want fluidity, use something like the builder pattern- Fluidität oder Unveränderlichkeit (es sei denn, Sie meinen das irgendwie). Wenn Sie unveränderliche Objekte erstellen möchten, aber das Festlegen einiger Werte verschieben müssen, müssen Sie diese nacheinander für ein Builder-Objekt festlegen und erst dann ein unveränderliches Objekt erstellen, wenn Sie alle für das richtige Objekt erforderlichen Werte erfasst haben Zustand ...
Konrad Morawski
1
@ KonradMorawski: Ich bin verwirrt. Erklären oder widersprechen Sie meiner Aussage? ;)
back2dos
3
Ich habe versucht, es zu ergänzen ...
Konrad Morawski
40

Kein Objekt mit ungültigem Status.

Der Zweck eines Konstruktors oder Generators besteht darin, den Status des Objekts auf etwas Konsistentes und Verwendbares festzulegen. Ohne diese Garantie treten bei Objekten Probleme mit einem nicht ordnungsgemäß initialisierten Zustand auf. Dieses Problem tritt auf, wenn Sie sich mit Parallelität befassen und etwas auf das Objekt zugreifen kann, bevor Sie es vollständig eingerichtet haben.

Die Frage für diesen Teil lautet also: "Warum lässt du zu, dass der Name oder der Nachname null ist?" Wenn dies in der Klasse nicht gültig ist, damit es ordnungsgemäß funktioniert, lassen Sie es nicht zu. Verfügen Sie über einen Konstruktor oder Builder, der die Erstellung des Objekts ordnungsgemäß überprüft, und werfen Sie das Problem an diesem Punkt auf, wenn es nicht richtig ist. Möglicherweise möchten Sie auch eine der vorhandenen @NotNullAnmerkungen verwenden , um mitzuteilen, dass "dies nicht null sein kann", und sie in Codierung und Analyse durchzusetzen.

Mit dieser Garantie wird es viel einfacher, über den Code nachzudenken, was er tut, und keine Ausnahmen an ungewöhnlichen Stellen auszulösen oder übermäßige Überprüfungen der Getter-Funktionen vorzunehmen.

Getter, die mehr können.

Es gibt eine Menge zu diesem Thema. Du hast wie viel Logik in Getter und Was soll innerhalb Getter und Setter erlaubt sein? von hier und Gettern und Setzern, die zusätzliche Logik für Stack Overflow ausführen . Dies ist ein Thema, das im Klassendesign immer wieder auftaucht.

Der Kern davon kommt von:

Im Allgemeinen dürfen Getter keine Ausnahmen auslösen, wenn ihre Werte nicht festgelegt sind

Und du hast recht. Sie sollten nicht. Sie sollten für den Rest der Welt "dumm" aussehen und das tun, was erwartet wird. Zu viel Logik führt zu Problemen, bei denen das Gesetz des geringsten Erstaunens verletzt wird. Und seien try catchwir ehrlich, Sie möchten die Getter wirklich nicht mit einem einwickeln, weil wir wissen, wie sehr wir das im Allgemeinen lieben.

Es gibt auch Situationen , in denen Sie müssen ein verwenden getFooVerfahren, wie die Java Beans Rahmen und wenn Sie etwas von haben EL erwartet Aufruf eine Bohne zu bekommen (so dass <% bar.foo %>tatsächlich Anrufe getFoo()in der Klasse - abgesehen von der ‚soll die Bohne die Zusammensetzung tun oder sollte ist das der sicht überlassen? 'weil man leicht situationen finden kann, in denen das eine oder andere eindeutig die richtige antwort sein kann)

Beachten Sie auch, dass es möglich ist, dass ein bestimmter Getter in einer Schnittstelle angegeben wird oder Teil der zuvor offengelegten öffentlichen API für die Klasse ist, die umgestaltet wird (eine frühere Version hatte nur 'completeName', das zurückgegeben wurde, und ein Umgestaltungsfehler es in zwei Felder).

Am Ende des Tages...

Machen Sie das, worüber Sie am einfachsten nachdenken können. Sie verbringen mehr Zeit mit der Pflege dieses Codes als mit dem Entwerfen (je weniger Zeit Sie mit dem Entwerfen verbringen, desto mehr Zeit verbringen Sie mit der Pflege). Die ideale Wahl besteht darin, das Design so zu gestalten, dass die Wartung nicht so lange dauert, aber auch nicht tagelang darüber nachgedacht wird - es sei denn, es handelt sich tatsächlich um ein Design, das sich erst Tage später auswirken wird .

Der Versuch, sich an die semantische Reinheit des Getter-Wesens zu halten, private T foo; T getFoo() { return foo; }wird Sie irgendwann in Schwierigkeiten bringen. Manchmal passt der Code einfach nicht zu diesem Modell und die Verzerrungen, die man durchläuft, um ihn so zu halten, ergeben einfach keinen Sinn ... und erschweren letztendlich das Entwerfen.

Akzeptiere manchmal, dass die Ideale des Designs nicht so umgesetzt werden können, wie du es im Code willst. Richtlinien sind Richtlinien - keine Fesseln.

Gemeinschaft
quelle
2
Offensichtlich war mein Beispiel nur eine Ausrede, um den Codierungsstil durch Argumentation zu verbessern, und kein Blockierungsproblem. Aber ich bin ziemlich ratlos über die Wichtigkeit, die Sie und andere Konstrukteuren zu geben scheinen. In der Theorie halten sie ihr Versprechen. In der Praxis wird es umständlich, sie zu vererben, und sie können nicht verwendet werden, wenn Sie eine Schnittstelle aus einer Klasse extrahieren, die von Javabeans und anderen Frameworks wie spring nicht übernommen wird, wenn ich nichts falsch mache. Unter dem Dach einer Klassenidee leben viele verschiedene Kategorien von Objekten. Ein Wertobjekt kann durchaus einen validierenden Konstruktor haben, dies ist jedoch nur ein Typ.
AgostinoX
2
Die Fähigkeit, über Code zu argumentieren, ist der Schlüssel, um Code mithilfe einer API schreiben oder den Code verwalten zu können. Wenn eine Klasse einen Konstruktor hat, der zulässt, dass sie sich in einem ungültigen Zustand befindet, ist es sehr viel schwieriger, "Nun, was macht das?" denn jetzt muss man mehr situationen berücksichtigen als der designer der klasse gedacht oder gedacht hat. Diese zusätzliche konzeptionelle Belastung ist mit dem Nachteil verbunden, dass mehr Bugs und eine längere Zeit zum Schreiben von Code erforderlich sind. Auf der anderen Seite wird es viel einfacher, Code mit dem Code zu schreiben, wenn Sie sich den Code ansehen und sagen "Vor- und Nachname werden niemals null sein".
2
Unvollständig heißt nicht unbedingt ungültig. Eine Rechnung ohne eine Quittung kann in Bearbeitung sein, und die öffentliche API, die sie vorlegt, würde widerspiegeln, dass dies ein gültiger Status ist. Wenn surnameoder nameNull ist ein gültiger Zustand für das Objekt, dann behandeln Sie es entsprechend. Wenn es sich jedoch nicht um einen gültigen Status handelt, der dazu führt, dass Methoden in der Klasse Ausnahmen auslösen, sollte verhindert werden, dass sie sich ab dem Zeitpunkt der Initialisierung in diesem Status befinden. Sie können unvollständige Objekte weitergeben, aber es ist problematisch, ungültige Objekte weiterzugeben. Wenn ein ungültiger Nachname eine Ausnahme darstellt, versuchen Sie, ihn früher als später zu verhindern.
2
Zugehöriger Blogbeitrag: Entwerfen der Objektinitialisierung. Beachten Sie die vier Vorgehensweisen zum Initialisieren einer Instanzvariablen. Eine davon besteht darin, zuzulassen, dass der Status ungültig ist. Beachten Sie jedoch, dass eine Ausnahme ausgelöst wird. Wenn Sie versuchen, dies zu vermeiden, ist dieser Ansatz nicht gültig, und Sie müssen mit den anderen Ansätzen arbeiten, bei denen stets ein gültiges Objekt sichergestellt ist. Aus dem Blog: "Objekte, die ungültige Zustände haben können, sind schwerer zu verwenden und schwerer zu verstehen als solche, die immer gültig sind." und das ist der Schlüssel.
5
"Tun Sie das, worüber Sie am einfachsten nachdenken können." That
Ben
5

Ich bin kein Java-Typ, aber dies scheint sich an beide Einschränkungen zu halten, die Sie vorgestellt haben.

getCompleteNameLöst keine Ausnahme aus, wenn die Namen nicht initialisiert sind displayCompleteNameOnInvoice.

public String getCompleteName()
{
    if (name == null || surname == null)
    {
        return null;
    }
    return name + " " + surname;
}

public void displayCompleteNameOnInvoice() 
{
    String completeName = getCompleteName();
    if (completeName == null)
    {
        //throw an exception.
    }
    //do something with it....
}
Rotem
quelle
Um zu erweitern, warum dies eine korrekte Lösung ist: Auf diese Weise muss sich der Anrufer von getCompleteName()nicht darum kümmern, ob die Eigenschaft tatsächlich gespeichert oder im laufenden Betrieb berechnet wird.
Bart van Ingen Schenau
18
Um herauszufinden, warum diese Lösung verrückt ist, müssen Sie bei jedem einzelnen Anruf überprüfen, ob sie ungültig ist getCompleteName, was abscheulich ist.
DeadMG
Nein, @DeadMG, Sie müssen dies nur in Fällen überprüfen, in denen eine Ausnahme ausgelöst werden soll, wenn getCompleteName null zurückgibt. Wie soll es sein? Diese Lösung ist richtig.
Dawood ibn Kareem
1
@DeadMG Ja, aber wir wissen nicht, welche ANDEREN Verwendungen es getCompleteName()in der restlichen Klasse oder sogar in anderen Teilen des Programms gibt. In einigen Fällen kann die Rücksendung nullgenau das sein, was erforderlich ist. Aber während es EINE Methode gibt, deren Spezifikation "in diesem Fall eine Ausnahme auslösen" lautet, sollten wir genau das codieren.
Dawood ibn Kareem
4
Es mag Situationen geben, in denen dies die beste Lösung ist, aber ich kann mir keine vorstellen. In den meisten Fällen scheint dies zu kompliziert zu sein: Eine Methode wirft mit unvollständigen Daten, eine andere gibt null zurück. Ich fürchte, dies wird wahrscheinlich von Anrufern falsch verwendet.
sleske
4

Es scheint, als würde niemand Ihre Frage beantworten.
Im Gegensatz zu dem, was die Leute gerne glauben, ist "ungültige Objekte vermeiden" oft keine praktische Lösung.

Die Antwort ist:

Ja das sollte es.

Einfaches Beispiel: FileStream.Lengthin C # /. NET , das wirft, NotSupportedExceptionwenn die Datei nicht suchbar ist.

Mehrdad
quelle
Kommentare sind nicht für längere Diskussionen gedacht. Diese Unterhaltung wurde in den Chat verschoben .
maple_shaft
1

Sie haben die ganze Sache mit den überprüfenden Namen auf den Kopf gestellt.

getCompleteName()muss nicht wissen, welche Funktionen es verwenden werden. Somit ist es nicht in der Verantwortung , namebeim Anrufen displayCompleteNameOnInvoice()keinen zu haben getCompleteName().

Aber nameist eine klare Voraussetzung für displayCompleteNameOnInvoice(). Daher sollte es die Verantwortung für die Überprüfung des Status übernehmen (oder es sollte die Verantwortung für die Überprüfung an eine andere Methode delegieren , wenn Sie dies vorziehen).

sampathsris
quelle