Ist es ein Anti-Pattern, wenn eine Klasseneigenschaft eine neue Instanz einer Klasse erstellt und zurückgibt?


24

Ich habe eine Klasse namens Heading, die ein paar Dinge tut, aber sie sollte auch das Gegenteil des aktuellen Heading-Werts zurückgeben können, der schließlich verwendet werden muss, indem eine neue Instanz der HeadingKlasse selbst erstellt wird.

Ich kann eine einfache Eigenschaft haben, die aufgerufen wird reciprocal, um die entgegengesetzte Überschrift des aktuellen Werts zurückzugeben und dann manuell eine neue Instanz der Heading-Klasse zu erstellen, oder ich kann eine Methode createReciprocalHeading()erstellen, die automatisch eine neue Instanz der Heading-Klasse erstellt und an die zurückgibt Benutzer.

Allerdings ist einer meiner Kollegen empfahl mich nur eine Klasse erstellen Eigenschaft genannt , reciprocaldass gibt eine neue Instanz der Klasse selbst über seine Getter - Methode.

Meine Frage ist: Ist es nicht ein Anti-Pattern für eine Eigenschaft einer Klasse, sich so zu verhalten?

Ich finde das besonders weniger intuitiv, weil:

  1. Meiner Meinung nach sollte eine Eigenschaft einer Klasse keine neue Instanz einer Klasse zurückgeben
  2. Der Name der Eigenschaft reciprocalhilft dem Entwickler nicht, ihr Verhalten vollständig zu verstehen, ohne Hilfe von der IDE zu erhalten oder die Getter-Signatur zu überprüfen.

Bin ich zu streng darüber, was eine Klasseneigenschaft tun soll, oder ist es ein berechtigtes Anliegen? Ich habe immer versucht, den Status der Klasse über ihre Felder und Eigenschaften und ihr Verhalten über ihre Methoden zu verwalten, und ich verstehe nicht, wie dies zur Definition der Klasseneigenschaft passt.


6
Wie @Ewan im letzten Absatz seiner Antwort sagt, ist es eine gute Praxis , nicht nur ein Anti-Muster zu sein, sondern einen Headingunveränderlichen Typ zu haben und reciprocaleinen neuen zurückzugeben Heading. (mit der Einschränkung bei den beiden Aufrufen reciprocalsollte "das Gleiche" zurückgeben, dh sie sollten den Gleichheitstest bestehen.)
David Arno

20
"Meine Klasse ist nicht unveränderlich". Da ist dein wirkliches Anti-Muster, genau dort. ;)
David Arno

3
@DavidArno Nun, manchmal gibt es eine enorme Leistungseinbuße, wenn bestimmte Dinge unveränderlich gemacht werden, insbesondere wenn die Sprache dies nicht von Haus aus unterstützt, aber ansonsten bin ich der Meinung, dass Unveränderlichkeit in vielen Fällen eine gute Praxis ist.
53777A

2
@dcorking Die Klasse ist sowohl in C # als auch in TypeScript implementiert, aber ich halte diese Sprache eher für Agnostiker.
53777A

2
@Kaiserludi klingt wie eine Antwort (eine großartige Antwort) - Kommentare dienen der Klarheit
Dcorking

Antworten:


22

Es ist nicht unbekannt, Dinge wie Copy () oder Clone () zu haben, aber ich denke, Sie haben Recht, sich um diese zu sorgen.

Nehmen Sie zum Beispiel:

h2 = h1.reciprocal
h2.Name = "hello world"
h1.reciprocal.Name = ?

Es wäre schön, eine Warnung zu haben, dass die Eigenschaft jedes Mal ein neues Objekt war.

Sie könnten auch annehmen, dass:

h2.reciprocal == h1

Jedoch. Wenn Ihre Überschriftenklasse ein Typ mit unveränderlichem Wert wäre, könnten Sie diese Beziehungen implementieren, und umgekehrt könnte ein guter Name für die Operation sein


1
Beachten Sie einfach, dass Copy()und Clone()Methoden sind, keine Eigenschaften. Ich glaube, das OP bezieht sich auf Eigenschafts-Getter, die neue Instanzen zurückgeben.
Lynn

6
Ich kenne. Eigenschaften sind aber (irgendwie) auch Methoden in c #
Ewan

4
In diesem Fall hat C # viel mehr zu bieten: String.Substring(), Array.Reverse(), Int32.GetHashCode()etc.
Lynn

If your heading class was an immutable value type- Ich denke, Sie sollten hinzufügen, dass Eigenschaftssetzer für unveränderliche Objekte immer die neuen Instanzen zurückgeben sollten (oder zumindest, wenn der neue Wert nicht mit dem alten Wert übereinstimmt), da dies die Definition für unveränderliche Objekte ist. :)
Ivan Kolmychek

17

Eine Schnittstelle einer Klasse veranlasst den Benutzer der Klasse, Annahmen darüber zu treffen, wie sie funktioniert.

Wenn viele dieser Annahmen richtig und wenige falsch sind, ist die Schnittstelle gut.

Wenn viele dieser Annahmen falsch und wenige richtig sind, ist die Schnittstelle Müll.

Eine verbreitete Annahme zu Properties ist, dass das Aufrufen der Funktion get billig ist. Eine weitere verbreitete Annahme zu Eigenschaften ist, dass das zweimalige Aufrufen der get-Funktion in Folge dasselbe zurückgibt.


Sie können dies umgehen, indem Sie die Konsistenz verwenden, um die Erwartungen zu ändern. Zum Beispiel für eine kleine 3D-Bibliothek, wo Sie brauchen Vector, Ray Matrixusw. Sie können es so machen, dass Getter wie Vector.normalund Matrix.inverseso ziemlich immer teuer sind. Leider , auch wenn Ihre Schnittstellen konsequent teuer Eigenschaften verwenden, werden die Schnittstellen bestenfalls so intuitiv wie man , dass Anwendungen Vector.create_normal()und Matrix.create_inverse()- aber ich kenne kein starkes Argument , die gemacht werden können , dass Eigenschaften schaffen eine intuitive Schnittstelle, auch nach den Erwartungen ändern.


10

Eigenschaften sollten sehr schnell zurückkehren, bei wiederholten Aufrufen denselben Wert zurückgeben und das Abrufen des Werts sollte keine Nebenwirkungen haben. Was Sie hier beschreiben, sollte nicht als Eigenschaft implementiert werden.

Ihre Intuition ist weitgehend richtig. Eine Factory-Methode gibt bei wiederholten Aufrufen nicht denselben Wert zurück. Es wird jedes Mal eine neue Instanz zurückgegeben. Dies disqualifiziert es so ziemlich als Eigentum. Es ist auch nicht schnell, wenn eine spätere Entwicklung der Instanzerstellung Gewicht verleiht, z. B. Netzwerkabhängigkeiten.

Eigenschaften sollten im Allgemeinen sehr einfache Operationen sein, die einen Teil des aktuellen Status der Klasse abrufen / festlegen. Fabrikähnliche Vorgänge erfüllen diese Kriterien nicht.


1
Die DateTime.NowEigenschaft wird häufig verwendet und verweist auf ein neues Objekt. Ich denke, der Name einer Eigenschaft kann dabei helfen, die Unklarheit zu beseitigen, ob jedes Mal dasselbe Objekt zurückgegeben wird oder nicht. Es ist alles im Namen und wie es verwendet wird.
Greg Burghardt

3
DateTime.Now wird als Fehler angesehen. Siehe stackoverflow.com/questions/5437972/…
Brad Thomas

@GregBurghardt Der Nebeneffekt eines neuen Objekts ist möglicherweise kein Problem an sich, da DateTimees sich um einen Werttyp handelt und kein Konzept der Objektidentität vorliegt. Wenn ich das richtig verstehe, ist das Problem, dass es nicht deterministisch ist und jedes Mal einen neuen Wert zurückgibt.
Zev Spitz

1
@GregBurghardt DateTime.Newist statisch, und daher können Sie nicht erwarten, dass es einen Zustand eines Objekts darstellt.
Tsahi Asher

5

Ich glaube nicht, dass es eine sprachunabhängige Antwort darauf gibt, da das, was ein „Eigentum“ ausmacht, eine sprachspezifische Frage ist und was ein Anrufer eines „Eigentums“ erwartet, auch eine sprachspezifische Frage ist. Ich denke, die fruchtbarste Art, darüber nachzudenken, besteht darin, darüber nachzudenken, wie es aus Sicht des Anrufers aussieht.

In C # unterscheiden sich Eigenschaften dadurch, dass sie (konventionell) groß geschrieben werden (wie Methoden), aber keine Klammern (wie öffentliche Instanzvariablen) enthalten. Was erwarten Sie, wenn der folgende Code ohne Dokumentation angezeigt wird?

var reciprocalHeading = myHeading.Reciprocal;

Als relativer C # -Anfänger, der jedoch die Microsoft Property Usage Guidelines gelesen hat , würde ich Reciprocalunter anderem Folgendes erwarten :

  1. ein logisches Datenelement der HeadingKlasse sein
  2. kostengünstig anrufen, sodass ich den Wert nicht zwischenspeichern muss
  3. es fehlen beobachtbare Nebenwirkungen
  4. Das gleiche Ergebnis erzielen, wenn zweimal hintereinander aufgerufen wird
  5. (vielleicht) eine ReciprocalChangedVeranstaltung anbieten

Von diesen Annahmen sind (3) und (4) wahrscheinlich richtig (unter der Annahme, dass Headinges sich um einen unveränderlichen Wertetyp handelt, wie in Ewans Antwort ), (1) ist umstritten, (2) ist unbekannt, aber auch umstritten, und (5) ist unwahrscheinlich machen semantischen Sinn (obwohl , was hat eine Überschrift vielleicht haben sollte HeadingChangedEreignis). Dies legt für mich den Schluss nahe, dass in einer C # -API "Get or Calculate the Reciprocal" nicht als Eigenschaft implementiert werden sollte. Insbesondere, wenn die Berechnung billig und Headingunveränderlich ist, handelt es sich um einen Grenzfall.

(Beachten Sie jedoch, dass keine dieser Bedenken damit zu tun hat, ob durch den Aufruf der Eigenschaft eine neue Instanz erstellt wird , nicht einmal (2). Das Erstellen von Objekten in der CLR an sich ist recht kostengünstig.)

In Java sind Eigenschaften eine Namenskonvention für Methoden. Wenn ich sehe

Heading reciprocalHeading = myHeading.getReciprocal();

Meine Erwartungen sind ähnlich wie die oben genannten (wenn auch weniger explizit dargelegt): Ich erwarte, dass der Anruf billig, idempotent und ohne Nebenwirkungen ist. Außerhalb des JavaBeans-Frameworks ist das Konzept einer "Eigenschaft" in Java jedoch nicht so aussagekräftig, und insbesondere, wenn eine unveränderliche Eigenschaft ohne Entsprechung betrachtet wird setReciprocal(), ist die getXXX()Konvention inzwischen etwas altmodisch. Aus Effective Java , zweite Ausgabe (bereits über acht Jahre alt):

Methoden, die eine Nichtfunktion booleanoder ein Attribut des Objekts zurückgeben, für das sie aufgerufen werden, werden normalerweise mit einem Substantiv, einer Substantivphrase oder einer Verbphrase, die mit dem Verb beginnt, benannt get. Es gibt ein Stimmkontingent, das behauptet, dass nur die dritte Form (beginnend mit get) akzeptabel ist, aber es gibt wenig Grundlage für diese Behauptung. Die ersten beiden Formen führen normalerweise zu besser lesbarem Code… (S. 239)

Eine zeitgemäße, flüssigere API würde ich also erwarten

Heading reciprocalHeading = myHeading.reciprocal();

- was wiederum darauf hindeuten würde, dass der Anruf billig, idempotent und ohne Nebenwirkungen ist, aber nichts darüber aussagen würde, ob eine neue Berechnung durchgeführt oder ein neues Objekt erstellt wird. Das ist in Ordnung; In einer guten API sollte es mich nicht interessieren.

In Ruby gibt es keine Immobilien. Es gibt "Attribute", aber wenn ich sehe

reciprocalHeading = my_heading.reciprocal

Ich habe keine unmittelbare Möglichkeit zu wissen, ob ich @reciprocalüber eine attr_readeroder eine einfache Accessormethode auf eine Instanzvariable zugreife oder ob ich eine Methode aufrufe, die eine teure Berechnung durchführt. Die Tatsache, dass der Methodenname ein einfaches Substantiv ist calcReciprocal, lässt jedoch nicht sagen , dass der Aufruf zumindest billig ist und wahrscheinlich keine Nebenwirkungen hat.

In Scala ist die Namenskonvention, dass Methoden mit Nebenwirkungen Klammern verwenden und Methoden ohne Klammern

val reciprocal = heading.reciprocal

könnte sein:

// immutable public value initialized at creation time
val reciprocal: Heading = … 

// immutable public value initialized on first use
lazy val reciprocal: Heading = … 

// public method, probably recalculating on each invocation
def reciprocal: Heading = …

// as above, with parentheses that, by convention, the caller
// should only omit if they know the method has no side effects
def reciprocal(): Heading = …

(Beachten Sie, dass Scala ermöglicht verschiedene Dinge , die dennoch sind entmutigt durch den Style - Guide . Dies ist eine meiner großen Belästigungen mit Scala ist.)

Das Fehlen von Klammern sagt mir, dass der Anruf keine Nebenwirkungen hat. der name legt wiederum nahe, dass der anruf relativ billig sein sollte. Darüber hinaus ist es mir egal, wie es mir den Wert bringt.

Kurz gesagt: Kennen Sie die Sprache, die Sie verwenden, und wissen Sie, welche Erwartungen andere Programmierer an Ihre API stellen. Alles andere ist ein Implementierungsdetail.


1
+1 eine meiner Lieblingsantworten, danke, dass Sie sich die Zeit genommen haben, dies aufzuschreiben.
53777A

2

Wie andere bereits gesagt haben, ist es ein weit verbreitetes Muster, Instanzen derselben Klasse zurückzugeben.

Die Benennung sollte mit den Benennungskonventionen der Sprache einhergehen.

Zum Beispiel würde ich in Java wahrscheinlich erwarten, dass es aufgerufen wird getReciprocal();

Davon abgesehen würde ich als veränderlich gegenüber unveränderlichen Objekten betrachten.

Bei unveränderlichen sind die Dinge ziemlich einfach, und es tut nicht weh, wenn Sie dasselbe Objekt zurückgeben oder nicht.

Bei veränderlichen kann dies sehr beängstigend werden.

b = a.reciprocal
a += 1
b = what?

Worauf bezieht sich b nun? Der Kehrwert des ursprünglichen Wertes von a? Oder das Gegenteil des Veränderten? Dies mag ein zu kurzes Beispiel sein, aber Sie verstehen es.

Suchen Sie in diesen Fällen nach einer besseren Benennung, die mitteilt, was passiert createReciprocal().

Aber es kommt auch wirklich auf den Kontext an.


Meinten Sie mutable ?
Carsten S

@CarstenS Ja, natürlich danke. Keine Ahnung, wo mein Kopf war. :)
Eiko

Nicht einverstanden getReciprocal(), da dies wie ein "normaler" JavaBeans-Style-Getter "klingt". Bevorzugen Sie "createXXX ()" oder möglicherweise "berechneXXX ()", die anderen Programmierern anzeigen oder andeuten, dass etwas anderes passiert
user949300

@ user949300 Ich stimme Ihren Bedenken etwas zu - und erwähnte den Namen create ... weiter unten. Es gibt jedoch auch Nachteile. create ... impliziert neue Instanzen, die möglicherweise nicht immer der Fall sind (denken Sie an einzelne dedizierte Objekte für bestimmte Sonderfälle), berechne ... Details zur Implementierung von Lecks - was zu falschen Annahmen über das Preisschild führen kann.
Eiko

1

Im Interesse der Einfachverantwortung und Klarheit hätte ich eine ReverseHeadingFactory, die das Objekt entgegennimmt und seine Rückseite zurückgibt. Dies würde klarer machen, dass ein neues Objekt zurückgegeben wird, und bedeuten, dass der Code zur Erzeugung der Umkehrung nicht in anderem Code enthalten ist.


1
+1 für die Klarheit der Namen, aber was ist mit SRP in anderen Lösungen falsch?
53777A

3
Warum empfehlen Sie eine Factory-Klasse gegenüber einer Factory-Methode? (Meiner Meinung nach besteht eine nützliche Aufgabe eines Objekts häufig darin, Objekte wie sich selbst zu emittieren, z. B. iterator.next. Verursacht diese
geringfügige

2
@dcorking Eine Factory-Klasse im Vergleich zu einer Methode (meiner Meinung nach) ist normalerweise dieselbe Art von Frage wie "Ist das Objekt vergleichbar oder sollte ich einen Komparator erstellen?" Es ist immer in Ordnung , einen Komparator zu erstellen, aber es ist nur dann in Ordnung, sie vergleichbar zu machen, wenn es eine ziemlich universelle Art ist, sie zu vergleichen. (ZB sind größere Zahlen größer als kleinere, das ist gut für vergleichbare, aber man könnte sagen, alle Geschwindigkeiten sind größer als alle Gewinnchancen, ich würde das zu einem Vergleich machen.) - Daher würde ich denken, dass es nur eine gibt Möglichkeit, einen Header umzukehren, daher würde ich mich dazu neigen, eine Methode zu entwickeln, um eine zusätzliche Klasse zu vermeiden.
Captain Man

0

Es hängt davon ab, ob. Normalerweise erwarte ich, dass Eigenschaften Dinge zurückgeben, die Teil einer Instanz sind, daher wäre es etwas ungewöhnlich, eine andere Instanz derselben Klasse zurückzugeben.

Eine String-Klasse könnte beispielsweise die Eigenschaften "firstWord", "lastWord" oder, wenn sie Unicode "firstLetter", "lastLetter" verarbeitet, vollständige String-Objekte haben - normalerweise nur kleinere.


0

Da die anderen Antworten den Eigenschaftsteil abdecken, werde ich nur Ihre Nummer 2 zu reciprocalfolgenden Themen ansprechen :

Verwenden Sie nichts anderes als reciprocal. Es ist der einzig richtige Begriff für das, was Sie beschreiben (und es ist der formale Begriff). Schreiben Sie keine falsche Software, um Ihren Entwicklern das Erlernen der Domäne, in der sie arbeiten, zu ersparen. In der Navigation haben Begriffe sehr spezifische Bedeutungen und verwenden etwas, das scheinbar harmlos ist reverseoder oppositein bestimmten Kontexten zu Verwirrung führen kann.


0

Logischerweise ist es keine Eigenschaft einer Überschrift. Wenn es so wäre, könnte man auch sagen, dass das Negativ einer Zahl eine Eigenschaft dieser Zahl ist, und ehrlich gesagt würde dies dazu führen, dass "Eigenschaft" jede Bedeutung verliert, fast alles wäre eine Eigenschaft.

Das Reziproke ist eine reine Funktion einer Überschrift, genauso wie das Negative einer Zahl eine reine Funktion dieser Zahl ist.

Als Faustregel gilt, dass eine Einstellung, die keinen Sinn ergibt, wahrscheinlich keine Eigenschaft sein sollte. Das Einstellen kann natürlich immer noch nicht zugelassen werden, aber das Hinzufügen eines Einstellers sollte theoretisch möglich und sinnvoll sein.

In einigen Sprachen ist es möglicherweise sinnvoll, sie als eine im Kontext dieser Sprache angegebene Eigenschaft zu haben, wenn sie aufgrund der Mechanik dieser Sprache einige Vorteile bietet. Wenn Sie es beispielsweise in einer Datenbank speichern möchten, ist es wahrscheinlich sinnvoll, es als Eigenschaft zu definieren, wenn es die automatische Verarbeitung durch das ORM-Framework ermöglicht. Oder vielleicht ist es eine Sprache, in der es keinen Unterschied zwischen einer Eigenschaft und einer parameterlosen Elementfunktion gibt (ich kenne eine solche Sprache nicht, aber ich bin sicher, dass es einige gibt). Dann ist es Sache des API-Designers und -Dokumentators, zwischen Funktionen und Eigenschaften zu unterscheiden.


Bearbeiten: Für C # würde ich mich mit vorhandenen Klassen befassen, insbesondere mit denen der Standardbibliothek.

  • Es gibt BigIntegerund ComplexStrukturen. Sie sind jedoch Strukturen und haben nur statische Funktionen, und alle Eigenschaften sind von unterschiedlichem Typ. Also nicht viel Designhilfe für euch da HeadingKlasse.
  • Math.NET Numerics hat eine VectorKlasse , die nur sehr wenige Eigenschaften hat und Ihrer sehr nahe zu sein scheint Heading. Wenn Sie so vorgehen, dann haben Sie eine Funktion für Gegenseitigkeit und keine Eigenschaft.

Möglicherweise möchten Sie Bibliotheken anzeigen, die tatsächlich von Ihrem Code verwendet werden, oder vorhandene ähnliche Klassen. Versuchen Sie konsequent zu bleiben, das ist normalerweise das Beste, wenn ein Weg nicht eindeutig richtig und ein anderer falsch ist.


-1

Sehr interessante Frage. Ich sehe keine SOLID + DRY + KISS-Verletzung in dem, was du vorschlägst, aber trotzdem riecht es schlecht.

Eine Methode, die eine Instanz der Klasse zurückgibt, heißt Konstruktor , oder? Sie erstellen also eine neue Instanz mit einer Nicht-Konstruktormethode. Es ist nicht das Ende der Welt, aber als Kunde würde ich das nicht erwarten . Dies geschieht normalerweise unter bestimmten Umständen: einer Factory oder manchmal einer statischen Methode derselben Klasse (nützlich für Singletons und für ... nicht so objektorientierte Programmierer?)

Plus: Was passiert, wenn Sie getReciprocal()das zurückgegebene Objekt aufrufen ? Sie erhalten eine weitere Instanz der Klasse, die eine genaue Kopie der ersten sein könnte! Und wenn Sie sich getReciprocal()auf DIESES berufen? Gegenstände, die irgendjemanden ausbreiten?

Nochmals: Was ist, wenn der Aufrufer den reziproken Wert benötigt (als Skalar meine ich)? getReciprocal()->getValue()? Wir verletzen das Gesetz von Demeter wegen geringer Gewinne.


Gegenargumente vom Downvoter nehme ich gerne an, danke!
Dr. Gianluigi Zane Zanettini

1
Ich habe nicht abgelehnt, aber ich vermute, der Grund könnte die Tatsache sein, dass es den Rest der Antworten nicht wirklich bereichert, aber ich könnte mich irren.
53777A

2
SOLID und KISS sind oft Gegensätze.
Whatsisname
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.