Problem mit dem Codierungsstil: Sollten wir Funktionen haben, die einen Parameter annehmen, ändern und dann diesen Parameter ZURÜCKKEHREN?


19

Ich habe eine kleine Debatte mit meinem Freund darüber, ob diese beiden Praktiken nur zwei Seiten derselben Medaille sind oder ob eine wirklich besser ist.

Wir haben eine Funktion, die einen Parameter nimmt, einen Member davon ausfüllt und ihn dann zurückgibt:

Item predictPrice(Item item)

Ich glaube, dass es keinen Sinn macht, das Objekt zurückzugeben , da es mit demselben Objekt funktioniert , das übergeben wird. Aus Sicht des Anrufers ist dies eher verwirrend, da Sie davon ausgehen können, dass ein neues Objekt zurückgegeben wird, das dies nicht tut.

Er behauptet, dass es keinen Unterschied macht, und es wäre nicht einmal wichtig, wenn es einen neuen Gegenstand erschaffen und diesen zurückgeben würde. Ich stimme aus folgenden Gründen überhaupt nicht zu:

  • Wenn Sie mehrere Verweise auf das Element haben, das übergeben wird (oder Zeiger oder was auch immer), ist es von wesentlicher Bedeutung, ein neues Objekt zuzuweisen und zurückzugeben, da diese Verweise falsch sind.

  • In nicht speicherverwalteten Sprachen beansprucht die Funktion, die eine neue Instanz zuweist, den Besitz des Speichers, und daher müssten wir eine Bereinigungsmethode implementieren, die irgendwann aufgerufen wird.

  • Das Zuweisen auf dem Heap ist möglicherweise teuer, und daher ist es wichtig, ob die aufgerufene Funktion dies tut.

Daher halte ich es für sehr wichtig, über eine Methodensignatur erkennen zu können, ob ein Objekt geändert oder ein neues zugewiesen wird. Aus diesem Grund glaube ich, dass die Signatur lauten sollte, da die Funktion lediglich das übergebene Objekt ändert:

void predictPrice(Item item)

In jeder Codebasis (zugegebenermaßen in C- und C ++ - Codebasen, nicht in Java, der Sprache, in der wir arbeiten), mit der ich gearbeitet habe, wurde der obige Stil im Wesentlichen eingehalten und von wesentlich erfahreneren Programmierern eingehalten. Er behauptet, dass meine Stichprobengröße von Codebasen und Kollegen von allen möglichen Codebasen und Kollegen klein ist und daher meine Erfahrung kein wahrer Indikator dafür ist, ob man überlegen ist.

Also irgendwelche Gedanken?


strcat ändert einen vorhandenen Parameter und gibt ihn dennoch zurück. Sollte strcat ungültig zurückkehren?
Jerry Jeremiah

Java und C ++ verhalten sich hinsichtlich der Übergabe von Parametern sehr unterschiedlich. Wenn Itemist class Item ...und nicht typedef ...& Item, ist die lokale itembereits eine Kopie
Caleth

google.github.io/styleguide/cppguide.html#Output_Parameters Google bevorzugt den RETURN-Wert für die Ausgabe
Firegun

Antworten:


26

Dies ist wirklich eine Ansichtssache, aber für das, was es wert ist, finde ich es irreführend, den geänderten Artikel zurückzugeben, wenn der Artikel an Ort und Stelle geändert wurde. Wenn predictPricedas Objekt geändert werden soll, sollte es einen Namen haben, der angibt, dass es dies tun wird (wie setPredictPriceoder so).

Ich würde es vorziehen (in der Reihenfolge)

  1. Das predictPricewar eine Methode vonItem (modulo ein guter Grund dafür, dass es nicht so ist, wie es in Ihrem Gesamtdesign sein könnte), die auch

    • Hat den vorhergesagten Preis zurückgegeben, oder

    • Hatte setPredictedPricestattdessen einen Namen wie

  2. Das predictPrice hat nicht geändert Item, sondern den vorhergesagten Preis zurückgegeben

  3. Das predictPricewar eine voidMethode namenssetPredictedPrice

  4. Das Ergebnis wurde predictPricezurückgegeben this(für die Verkettung von Methoden mit anderen Methoden, zu welcher Instanz auch immer es gehört) (und wurde aufgerufen setPredictedPrice).


1
Danke für die Antwort. In Bezug auf 1 ist es uns nicht möglich, predictPrice innerhalb von Item zu haben. 2 ist ein guter Vorschlag - ich denke, das ist hier wahrscheinlich die richtige Lösung.
Squimmy

2
@Squimmy: Und sicher, ich habe gerade 15 Minuten damit verbracht, mich mit einem Fehler zu befassen, der von jemandem verursacht wurde, der genau das Muster verwendet, gegen das Sie argumentiert haben: a = doSomethingWith(b) geändert b und mit den Änderungen zurückgegeben, die meinen Code später in die Luft gesprengt haben, weil ich dachte, er wüsste, was bpassiert ist drin. :-)
TJ Crowder

11

Innerhalb eines objektorientierten Entwurfsparadigmas sollten Dinge keine Objekte außerhalb des Objekts selbst modifizieren. Alle Änderungen am Status eines Objekts sollten über Methoden für das Objekt vorgenommen werden.

Daher ist die void predictPrice(Item item)Funktion eines Mitglieds einer anderen Klasse falsch . Konnte in den Tagen von C akzeptabel sein, aber für Java und C ++ implizieren die Änderungen eines Objekts eine tiefere Kopplung an das Objekt, die wahrscheinlich zu anderen Entwurfsproblemen führen würde (wenn Sie die Klasse umgestalten und ihre Felder ändern, Jetzt muss "predictPrice" in eine andere Datei geändert werden.

Zurückgeben eines neuen Objekts, ohne damit verbundene Nebenwirkungen, der übergebene Parameter wird nicht geändert. Sie (die predictPrice-Methode) wissen nicht, wo dieser Parameter sonst verwendet wird. Ist Itemein Schlüssel zu einem Hash irgendwo? Haben Sie dabei den Hash-Code geändert? Hält jemand anderes daran fest und erwartet, dass es sich nicht ändert?

Diese Entwurfsprobleme deuten nachdrücklich darauf hin, dass Sie Objekte nicht modifizieren sollten (ich würde in vielen Fällen für Unveränderlichkeit eintreten), und wenn Sie dies tun, sollten die Änderungen am Status eher vom Objekt selbst als von etwas anderem enthalten und gesteuert werden sonst außerhalb der Klasse.


Schauen wir uns an, was passiert, wenn man mit den Feldern von etwas in einem Hash spielt. Nehmen wir einen Code:

import java.util.*;

public class Main {
    public static void main (String[] args) {
        Set set = new HashSet();
        Data d = new Data(1,"foo");
        set.add(d);
        set.add(new Data(2,"bar"));
        System.out.println(set.contains(d));
        d.field1 = 2;
        System.out.println(set.contains(d));
    }

    public static class Data {
        int field1;
        String field2;

        Data(int f1, String f2) {
            field1 = f1;
            field2 = f2;
        }

        public int hashCode() {
            return field2.hashCode() + field1;
        }

        public boolean equals(Object o) {
            if(!(o instanceof Data)) return false;
            Data od = (Data)o;
            return od.field1 == this.field1 && od.field2.equals(this.field2);

        }
    }
}

ideone

Und ich gebe zu, dass dies nicht der größte Code ist (der direkt auf Felder zugreift), aber er dient dazu, ein Problem zu demonstrieren, bei dem veränderbare Daten als Schlüssel für eine HashMap verwendet werden oder in diesem Fall einfach in eine HashSet.

Die Ausgabe dieses Codes ist:

true
false

Was passiert ist, ist, dass der Hashcode, der beim Einfügen verwendet wurde, sich dort befindet, wo sich das Objekt im Hash befindet. Durch Ändern der zur Berechnung des Hashcodes verwendeten Werte wird der Hash selbst nicht neu berechnet. Es besteht die Gefahr, dass ein veränderliches Objekt als Schlüssel für einen Hash verwendet wird.

Zurück zum ursprünglichen Aspekt der Frage: Die aufgerufene Methode "weiß" nicht, wie das Objekt, das sie als Parameter erhält, verwendet wird. Wenn Sie die Option "Lässt das Objekt mutieren" als einzige Möglichkeit angeben, kann dies dazu führen, dass sich eine Reihe von subtilen Fehlern eingeschlichen haben. Wie das Verlieren von Werten in einem Hash ... es sei denn, Sie fügen mehr hinzu und der Hash wird erneut aufbereitet - vertrauen Sie mir , das ist ein bösen Fehler auf der Jagd nach (ich den Wert verloren , bis ich 20 weitere Elemente in den Hashmap hinzufügen und dann plötzlich zeigt es wieder).

  • Es sei denn , es gibt sehr gute Gründe , das Objekt zu ändern, ein neues Objekt der Rückkehr ist die sicherste Sache zu tun.
  • Wenn es ist ein guter Grund , um das Objekt zu ändern, dass eine Modifikation soll durch das Objekt selbst (Aufruf Methoden) eher erfolgen als durch irgendeine externe Funktion, die ihre Felder twiddle kann.
    • Dadurch kann das Objekt mit geringeren Wartungskosten umgestaltet werden.
    • Auf diese Weise kann das Objekt sicherstellen, dass sich die Werte, die seinen Hashcode berechnen , nicht ändern (oder nicht Teil seiner Hashcode-Berechnung sind).

Verwandte Themen : Überschreiben und Zurückgeben des Werts des Arguments, das als Bedingung für eine if-Anweisung verwendet wird, innerhalb derselben if-Anweisung


3
Das klingt nicht richtig. Höchstwahrscheinlich predictPricesollte man nicht direkt mit Itemden Mitgliedern herumspielen , aber was ist falsch daran, Methoden dafür aufzurufen Item? Wenn dies das einzige Objekt ist, das geändert wird, können Sie vielleicht argumentieren, dass es eine Methode für das zu ändernde Objekt sein soll, aber zu anderen Zeiten werden mehrere Objekte (die definitiv nicht derselben Klasse angehören sollten) geändert, insbesondere in eher übergeordnete Methoden.

@delnan Ich gebe zu, dass dies eine (Über-) Reaktion auf einen Fehler sein kann, den ich einmal ausfindig machen musste, als ein Wert in einem Hash verschwand und wieder auftauchte - jemand hat nach dem Einfügen mit den Feldern des Objekts im Hash herumgespielt anstatt eine Kopie des Objekts für seine Verwendung zu erstellen. Es gibt defensive Programmiermaßnahmen, die man ergreifen kann (z. B. unveränderliche Objekte), aber Dinge wie das Neuberechnen von Werten im Objekt selbst, wenn Sie nicht der "Eigentümer" des Objekts sind, oder das Objekt selbst ist etwas, das sich leicht wieder zubeißen lässt Sie schwer.

Wissen Sie, es gibt ein gutes Argument dafür, mehr freie Funktionen anstelle von Klassenfunktionen zu verwenden, um die Kapselung zu verbessern. Natürlich sollte eine Funktion, die ein konstantes Objekt erhält (ob implizit oder als Parameter), es nicht auf beobachtbare Weise ändern (einige konstante Objekte können Dinge zwischenspeichern).
Deduplizierer

2

Ich folge immer der Praxis , das Objekt eines anderen nicht zu mutieren . Das heißt, nur mutierende Objekte, die der Klasse / struct / gehören, in der Sie sich befinden.

Ausgehend von der folgenden Datenklasse:

class Item {
    public double price = 0;
}

Das ist in Ordnung:

class Foo {
    private Item bar = new Item();

    public void predictPrice() {
        bar.price = bar.price + 5; // or something
    }
}

// Elsewhere...

Foo foo = Foo();
foo.predictPrice();
System.out.println(foo.bar.price);
// Since I called predictPrice on Foo, which takes and returns nothing, it's
// apparent something might've changed

Und das ist ganz klar:

class Foo {
    private Item bar = new Item();
}
class Baz {
    public double predictPrice(Item item) {
        return item.price + 5; // or something
    }
}

// Elsewhere...

Foo foo = Foo();
Baz baz = Baz();
foo.bar.price = baz.predictPrice(foo.bar);
System.out.println(foo.bar.price);
// Since I explicitly set foo.bar.price to the return value of predictPrice, it's
// obvious foo.bar.price might've changed

Aber das ist viel zu schlammig und mysteriös für meinen Geschmack:

class Foo {
    private Item bar = new Item();
}
class Baz {
    public void predictPrice(Item item) {
        item.price = item.price + 5; // or something
    }
}

// Elsewhere...

Foo foo = Foo();
Baz baz = Baz();
baz.predictPrice(foo.bar);
System.out.println(foo.bar.price);
// In my head, it doesn't appear that to foo, bar, or price changed. It seems to
// me that, if anything, I've placed a predicted price into baz that's based on
// the value of foo.bar.price

Bitte begleite alle Downvotes mit einem Kommentar, damit ich in Zukunft bessere Antworten schreiben kann :)
Ben Leggiero

1

Die Syntax unterscheidet sich zwischen verschiedenen Sprachen, und es ist bedeutungslos, einen Codeteil zu zeigen, der nicht für eine Sprache spezifisch ist, da er in verschiedenen Sprachen völlig verschiedene Dinge bedeutet.

In Java Itemist es ein Referenztyp - es ist der Typ von Zeigern auf Objekte, von denen Instanzen sind Item. In C ++ ist dieser Typ wie folgt geschrieben Item *(die Syntax für dasselbe unterscheidet sich zwischen den Sprachen). Also Item predictPrice(Item item)in Java ist gleichbedeutend mit Item *predictPrice(Item *item)in C ++. In beiden Fällen ist es eine Funktion (oder Methode), die einen Zeiger auf ein Objekt nimmt und einen Zeiger auf ein Objekt zurückgibt.

In C ++ ist Item(ohne alles andere) ein Objekttyp (vorausgesetzt, es Itemhandelt sich um den Namen einer Klasse). Ein Wert dieses Typs "ist" ein Objekt und Item predictPrice(Item item)deklariert eine Funktion, die ein Objekt übernimmt und ein Objekt zurückgibt. Da &es nicht verwendet wird, wird es übergeben und als Wert zurückgegeben. Das heißt, das Objekt wird bei der Übergabe kopiert und bei der Rückgabe kopiert. In Java gibt es keine Entsprechung, da Java keine Objekttypen hat.

Also was ist es? Viele der Dinge, die Sie in der Frage stellen (z. B. "Ich glaube, dass es auf demselben Objekt funktioniert, das übergeben wird ..."), hängen davon ab, genau zu verstehen, was hier übergeben und zurückgegeben wird.


1

Die Konvention, die ich bei Codebasen gesehen habe, besteht darin, einen Stil zu bevorzugen, der sich aus der Syntax ergibt. Wenn sich der Wert der Funktion ändert, ziehen Sie die Übergabe mit dem Zeiger vor

void predictPrice(Item * const item);

Dieser Stil führt zu:

  • Wenn Sie es so nennen, sehen Sie:

    predictPrice (& my_item);

und Sie wissen, dass es durch Ihre Einhaltung des Stils geändert wird.

  • Andernfalls übergeben Sie lieber const ref oder value, wenn die Funktion die Instanz nicht ändern soll.

Es sollte beachtet werden, dass dies in Java nicht verfügbar ist, obwohl es sich um eine viel klarere Syntax handelt.
Ben Leggiero

0

Obwohl ich keine starke Präferenz habe, würde ich dafür stimmen, dass die Rückgabe des Objekts zu einer besseren Flexibilität für eine API führt.

Beachten Sie, dass es nur Sinn macht, wenn die Methode die Freiheit hat, ein anderes Objekt zurückzugeben. Wenn dein Javadoc sagt, @returns the same value of parameter aist es nutzlos. Wenn Ihr Javadoc jedoch sagt @return an instance that holds the same data than parameter a, dass dieselbe oder eine andere Instanz zurückgegeben werden soll, handelt es sich um ein Implementierungsdetail.

In meinem aktuellen Projekt (Java EE) habe ich beispielsweise eine Business-Schicht. Um einen Mitarbeiter in der Datenbank zu speichern (und eine automatische ID zuzuweisen), sagen wir, einen Mitarbeiter, den ich habe

 public Employee createEmployee(Employee employeeData) {
   this.entityManager.persist(employeeData);
   return this.employeeData;
 }

Natürlich kein Unterschied zu dieser Implementierung, da JPA nach Zuweisung der ID dasselbe Objekt zurückgibt. Nun, wenn ich zu JDBC gewechselt bin

 public Employee createEmployee(Employee employeeData) {
   PreparedStatement ps = this.connection.prepareStatement("INSERT INTO EMPLOYEE(name, surname, data) VALUES(?, ?, ?)");
   ... // set parameters
   ps.execute();
   int id = getIdFromGeneratedKeys(ps);
   ??????
 }

Jetzt bei ????? Ich habe drei Möglichkeiten.

  1. Definieren Sie einen Setter für Id, und ich werde das gleiche Objekt zurückgeben. Hässlich, weil setIdüberall verfügbar sein wird.

  2. Machen Sie einige schwere Reflexionsarbeit und setzen Sie die ID in das EmployeeObjekt. Dreckig, aber zumindest ist es auf den createEmployeeRekord beschränkt.

  3. Geben Sie einen Konstruktor an, der das Original kopiert Employeeund auch das idzu setzende Objekt akzeptiert .

  4. Rufen Sie das Objekt aus der Datenbank ab (möglicherweise erforderlich, wenn einige Feldwerte in der Datenbank berechnet werden oder wenn Sie eine Beziehung auffüllen möchten).

Für 1. und 2. geben Sie möglicherweise dieselbe Instanz zurück, für 3. und 4. geben Sie jedoch neue Instanzen zurück. Wenn Sie in Ihrer API nach dem Prinzip festgelegt haben, dass der "echte" Wert der von der Methode zurückgegebene Wert ist, haben Sie mehr Freiheit.

Das einzige wirkliche Argument, das ich dagegen haben kann, ist, dass bei Verwendung von Implementierungen 1. oder 2. die Benutzer Ihrer API die Zuweisung des Ergebnisses möglicherweise übersehen und ihr Code beschädigt wird, wenn Sie zu Implementierungen 3. oder 4. wechseln.

Wie Sie sehen, basiert meine Präferenz auf anderen persönlichen Präferenzen (z. B. dem Verbot, IDs festzulegen), sodass sie möglicherweise nicht für alle gilt.


0

Wenn Sie in Java codieren, sollten Sie versuchen, die Verwendung jedes Objekts mit veränderlichem Typ mit einem von zwei Mustern abzugleichen:

  1. Genau eine Entität wird als "Eigentümer" des veränderlichen Objekts betrachtet und betrachtet den Zustand dieses Objekts als einen Teil seines eigenen. Andere Entitäten können Referenzen besitzen, sollten diese jedoch als Identifikation eines Objekts betrachten, das jemand anderem gehört.

  2. Obwohl das Objekt veränderlich ist, darf niemand, der einen Verweis darauf besitzt, es verändern, wodurch die Instanz effektiv unveränderlich wird (beachten Sie, dass es aufgrund von Macken in Javas Speichermodell keine gute Möglichkeit gibt, ein effektiv unveränderliches Objekt mit Thread zu versehen). sichere unveränderliche Semantik ohne zusätzliche Indirektionsebene für jeden Zugriff). Jede Entität, die einen Zustand unter Verwendung eines Verweises auf ein solches Objekt einkapselt und den eingekapselten Zustand ändern möchte, muss ein neues Objekt erstellen, das den richtigen Zustand enthält.

Ich möchte nicht, dass Methoden das zugrunde liegende Objekt mutieren und eine Referenz zurückgeben, da sie den Anschein erwecken, als würden sie dem zweiten Muster oben entsprechen. Es gibt einige Fälle, in denen der Ansatz hilfreich sein kann, aber Code, der Objekte mutiert, sollte so aussehen , als würde er dies tun. Code like myThing = myThing.xyz(q);sieht viel weniger so aus, myThingals würde er das durch identifizierte Objekt mutieren, als es einfach wäre myThing.xyz(q);.

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.