it-swarm.com.de

Problem mit dem Codierungsstil: Sollten wir Funktionen haben, die einen Parameter übernehmen, ändern und dann diesen Parameter zurückgeben?

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, ein Mitglied ausfüllt und es dann zurückgibt:

Item predictPrice(Item item)

Ich glaube, da es auf dem übergebenen same -Objekt funktioniert, macht es keinen Sinn, das Objekt zurückzugeben. Wenn überhaupt, verwirrt dies aus Sicht des Anrufers die Dinge, da Sie erwarten können, dass es ein neues Element zurückgibt, das es nicht gibt.

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

  • Wenn Sie mehrere Verweise auf das übergebene Element haben (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, anhand einer Methodensignatur erkennen zu können, ob ein Objekt geändert oder ein neues zugewiesen wird. Daher glaube ich, dass die Signatur lauten sollte, da die Funktion lediglich das übergebene Objekt ändert:

void predictPrice(Item item)

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

Also irgendwelche Gedanken?

20
Squimmy

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 wird. Wenn predictPrice das Element ändern soll, sollte es einen Namen haben, der angibt, dass es dies tun wird (wie setPredictPrice oder ähnliches).

Ich würde es vorziehen (in der Reihenfolge)

  1. Das predictPrice war ein Methode von Item (Modulo ein guter Grund dafür, dass es nicht so ist, wie es in Ihrem Gesamtdesign durchaus sein könnte), welches auch nicht

    • Den vorhergesagten Preis zurückgegeben, oder

    • Hatte stattdessen einen Namen wie setPredictedPrice

  2. Das predictPricenicht modifiziere Item, gab aber den vorhergesagten Preis zurück

  3. Dieses predictPrice war eine void Methode namens setPredictedPrice

  4. Das predictPrice gab this zurück (für die Verkettung von Methoden mit anderen Methoden auf jeder Instanz, zu der es gehört) (und hieß setPredictedPrice)

26
T.J. Crowder

Innerhalb eines objektorientierten Designparadigmas 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.

Somit ist void predictPrice(Item item) als Mitgliedsfunktion einer anderen Klasse falsch . Könnte in den Tagen von C akzeptabel gewesen sein, aber für Java und C++ implizieren die Änderungen eines Objekts eine tiefere Kopplung an das Objekt, die wahrscheinlich später zu anderen Designproblemen führen würde (wann) Sie überarbeiten die Klasse und ändern ihre Felder, jetzt, da 'PredictPrice' in einer anderen Datei geändert werden muss.

Wenn Sie ein neues Objekt zurückgeben, keine Nebenwirkungen haben, wird der übergebene Parameter nicht geändert. Sie (die PredictPrice-Methode) wissen nicht, wo dieser Parameter sonst noch verwendet wird. Ist Item irgendwo ein Schlüssel zu einem Hash? Haben Sie dabei den Hash-Code geändert? Hält jemand anderes daran fest und erwartet, dass es sich nicht ändert?

Diese Entwurfsprobleme deuten stark darauf hin, dass Sie keine Objekte ändern sollten (ich würde in vielen Fällen für Unveränderlichkeit argumentieren), und wenn Sie dies tun, sollten die Änderungen des Status eher vom Objekt selbst als von etwas 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 beste Code ist (direkter Zugriff auf Felder), aber er dient dazu, ein Problem zu demonstrieren, bei dem veränderbare Daten als Schlüssel für eine HashMap verwendet oder in diesem Fall einfach in eine HashMap eingefügt werden HashSet.

Die Ausgabe dieses Codes lautet:

true
false

Was passiert ist, ist, dass sich der beim Einfügen verwendete Hashcode 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. Dies ist die Gefahr, dass jedes veränderbare Objekt als Schlüssel für einen Hash verwendet wird.

Zurück zum ursprünglichen Aspekt der Frage: Die aufgerufene Methode "weiß" also nicht, wie das Objekt, das sie als Parameter erhält, verwendet wird. Die Angabe "Lässt das Objekt mutieren" als einzige Möglichkeit, dies zu tun, bedeutet, dass es eine Reihe subtiler Fehler gibt, die sich einschleichen können. Als ob Sie Werte in einem Hash verlieren würden ... es sei denn, Sie fügen weitere hinzu und der Hash wird erneut aufbereitet - vertrauen Sie mir, das ist ein böser Fehler, den Sie suchen müssen (ich habe verloren) den Wert, bis ich der HashMap 20 weitere Elemente hinzufüge und er dann plötzlich wieder angezeigt wird).

  • Sofern es keinen sehr guten Grund gibt, das Objekt zu ändern, ist die Rückgabe eines neuen Objekts am sichersten.
  • Wenn ein guter Grund ist, das Objekt zu ändern, sollte diese Änderung vom Objekt selbst (Aufrufen von Methoden) und nicht von einer externen Funktion vorgenommen werden, die dies kann zwitschere seine Felder.
    • 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

11
user40980

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

Bei folgender 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 sehr 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
2
Ben Leggiero

Die Syntax unterscheidet sich zwischen verschiedenen Sprachen, und es ist bedeutungslos, einen Code anzuzeigen, der nicht sprachspezifisch ist, da er in verschiedenen Sprachen völlig unterschiedliche Bedeutungen hat.

In Java ist Item ein Referenztyp - es ist der Typ von Zeigern auf Objekte, die Instanzen von Item sind. In C++ wird dieser Typ als Item * Geschrieben (die Syntax für dasselbe unterscheidet sich zwischen den Sprachen). Also Item predictPrice(Item item) in Java entspricht Item *predictPrice(Item *item) in C++. In beiden Fällen ist es eine Funktion (oder Methode), die einen Zeiger auf eine nimmt Objekt und gibt einen Zeiger auf ein Objekt zurück.

In C++ ist Item (ohne irgendetwas anderes) ein Objekttyp (vorausgesetzt, Item ist der Name einer Klasse). Ein Wert dieses Typs "ist" ein Objekt, und Item predictPrice(Item item) deklariert eine Funktion, die ein Objekt nimmt und ein Objekt zurückgibt. Da & 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. Es gibt kein Äquivalent in Java weil Java hat keine Objekttypen.

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
user102008

Die Konvention, an die sich Codebasen halten, besteht darin, einen Stil zu bevorzugen, der sich aus der Syntax ergibt. Wenn die Funktion den Wert ändert, ziehen Sie es vor, den Zeiger zu übergeben

void predictPrice(Item * const item);

Dieser Stil führt zu:

  • Wenn Sie es nennen, sehen Sie:

    predictPrice (& my_item);

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

  • Andernfalls bevorzugen Sie die Übergabe von const ref oder value, wenn die Funktion die Instanz nicht ändern soll.
1
user27886

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 hat, wenn die Methode die Freiheit hat, andere Objekte zurückzugeben. Wenn Ihr Javadoc sagt @returns the same value of parameter a dann ist es nutzlos. Aber wenn dein Javadoc sagt @return an instance that holds the same data than parameter a, dann ist die Rückgabe derselben oder einer anderen Instanz ein Implementierungsdetail.

In meinem aktuellen Projekt (Java EE) habe ich beispielsweise eine Geschäftsschicht. Um in der Datenbank einen Mitarbeiter zu speichern (und eine automatische ID zuzuweisen), z. B. 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 dem Zuweisen 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. Führen Sie umfangreiche Reflexionsarbeiten durch und legen Sie die ID im Objekt Employee fest. Schmutzig, aber zumindest auf den Datensatz createEmployee beschränkt.

  3. Stellen Sie einen Konstruktor bereit, der das Original Employee kopiert und auch das zu setzende id 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 fü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 nach dem Prinzip, das Sie in Ihrer API festgelegt haben, dass der "echte" Wert der von der Methode zurückgegebene ist, haben Sie mehr Freiheit.

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

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

0
SJuan76

Beim Codieren in Java sollte versucht werden, die Verwendung jedes Objekts vom veränderlichen 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 Teil seines eigenen. Andere Entitäten können Referenzen enthalten, sollten die Referenz jedoch als identifizierend ein Objekt betrachten, das jemand anderem gehört.

  2. Obwohl das Objekt veränderbar ist, darf niemand, der einen Verweis darauf hat, es mutieren, wodurch die Instanz effektiv unveränderlich wird (beachten Sie, dass es aufgrund von Macken im Java-Speichermodell keine gute Möglichkeit gibt, ein effektiv unveränderliches Objekt mit einem Thread zu versehen. sichere unveränderliche Semantik, ohne jedem Zugriff eine zusätzliche Indirektionsebene hinzuzufügen). Jede Entität, die den Status mithilfe eines Verweises auf ein solches Objekt kapselt und den gekapselten Status ändern möchte, muss ein neues Objekt erstellen, das den richtigen Status enthält.

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

0
supercat