Problema relativo allo stile di codifica: dovremmo avere funzioni che accettano un parametro, lo modificano e quindi INVIO quel parametro?


19

Sto discutendo un po 'con il mio amico se queste due pratiche sono semplicemente due facce della stessa medaglia o se si è veramente migliori.

Abbiamo una funzione che accetta un parametro, ne compila un membro e quindi lo restituisce:

Item predictPrice(Item item)

Credo che, poiché funziona sullo stesso oggetto che viene passato, non ha senso restituire l'articolo. In effetti, semmai, dal punto di vista del chiamante, confonde le cose in quanto ci si potrebbe aspettare che restituisca un nuovo elemento che non lo fa.

Afferma che non fa alcuna differenza, e non avrebbe nemmeno importanza se creasse un nuovo oggetto e lo restituisse. Sono fortemente in disaccordo, per i seguenti motivi:

  • Se sono presenti più riferimenti all'elemento (o puntatori o altro), l'allocazione di un nuovo oggetto e la sua restituzione sono di importanza materiale poiché tali riferimenti non saranno corretti.

  • Nei linguaggi gestiti senza memoria, la funzione che alloca una nuova istanza rivendica la proprietà della memoria, e quindi dovremmo implementare un metodo di pulizia che viene chiamato ad un certo punto.

  • L'allocazione sull'heap è potenzialmente costosa ed è quindi importante sapere se la funzione chiamata lo fa.

Quindi, credo che sia molto importante poter vedere tramite una firma dei metodi se modifica un oggetto o ne assegna uno nuovo. Di conseguenza, credo che poiché la funzione modifica semplicemente l'oggetto passato, la firma dovrebbe essere:

void predictPrice(Item item)

In ogni base di codice (certamente basi di codice C e C ++, non Java che è il linguaggio in cui stiamo lavorando) con cui ho lavorato, lo stile sopra è stato sostanzialmente rispettato e rispettato da programmatori considerevolmente più esperti. Afferma che, poiché la mia dimensione di esempio di basi di codice e colleghi è piccola rispetto a tutte le possibili basi di codice e colleghi, e quindi la mia esperienza non è un vero indicatore del fatto che uno sia superiore.

Quindi, qualche pensiero?


strcat modifica un parametro sul posto e lo restituisce comunque. Strcat dovrebbe restituire il vuoto?
Jerry Jeremiah,

Java e C ++ hanno un comportamento molto diverso rispetto al passaggio dei parametri. Se Itemè class Item ...e non typedef ...& Item, il locale itemè già una copia
Caleth

google.github.io/styleguide/cppguide.html#Output_Parameters Google preferisce utilizzare il valore RETURN per l'output
Firegun,

Risposte:


26

Questa è davvero una questione di opinione, ma per quello che vale, trovo fuorviante restituire l'articolo modificato se l'articolo viene modificato in posizione. Separatamente, se predictPricesta per modificare l'elemento, dovrebbe avere un nome che indica che lo farà (come setPredictPriceo alcuni di questi).

Preferirei (in ordine)

  1. Quello predictPriceera un metodo diItem (modulo una buona ragione per non esserlo, cosa che potrebbe benissimo esserci nel tuo disegno complessivo) che neanche

    • Restituito il prezzo previsto, o

    • Aveva un nome come setPredictedPriceinvece

  2. Ciò predictPrice non è stato modificato Item, ma ha restituito il prezzo previsto

  3. Quello predictPriceera un voidmetodo chiamatosetPredictedPrice

  4. Ciò ha predictPricerestituito this(per il concatenamento di metodi con altri metodi in qualunque istanza di cui fa parte) (ed è stato chiamato setPredictedPrice)


1
Grazie per la risposta. Per quanto riguarda 1, non siamo in grado di prevedere il prezzo all'interno dell'articolo. 2 è un buon suggerimento - penso che sia probabilmente la soluzione corretta qui.
Squimmy,

2
@Squimmy: E certo, ho appena trascorso 15 minuti a gestire un bug causato da qualcuno che utilizzava esattamente il modello contro il quale stavi litigando: a = doSomethingWith(b) modificato b e restituito con le modifiche, che in seguito hanno fatto esplodere il mio codice pensando che sapesse cosa baveva dentro. :-)
TJ Crowder,

11

All'interno di un paradigma progettuale orientato agli oggetti, le cose non dovrebbero modificare gli oggetti al di fuori dell'oggetto stesso. Qualsiasi modifica allo stato di un oggetto deve essere effettuata tramite metodi sull'oggetto.

Pertanto, void predictPrice(Item item)poiché una funzione membro di un'altra classe è errata . Avrebbe potuto essere accettabile ai tempi di C, ma per Java e C ++, le modifiche di un oggetto implicano un accoppiamento più profondo con l'oggetto che probabilmente porterebbe ad altri problemi di progettazione lungo la strada (quando si refactoring la classe e si cambiano i suoi campi, ora che 'predictPrice' deve essere cambiato in qualche altro file.

Restituendo un nuovo oggetto, non ha effetti collaterali associati, il parametro passato non viene modificato. Tu (il metodo predictPrice) non sai dove altro quel parametro viene utilizzato. La Itemchiave di un hash è da qualche parte? hai cambiato il suo codice hash nel fare questo? Qualcun altro si sta aggrappando ad esso aspettandosi che non cambi?

Questi problemi di progettazione suggeriscono che non dovresti modificare gli oggetti (direi per l'immutabilità in molti casi) e, se lo fai, le modifiche allo stato dovrebbero essere contenute e controllate dall'oggetto stesso piuttosto che da qualcosa altro al di fuori della classe.


Vediamo cosa succede se si armeggia con i campi di qualcosa in un hash. Prendiamo un po 'di codice:

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

E ammetto che questo non è il codice più grande (accedendo direttamente ai campi), ma serve al suo scopo di dimostrare un problema con i dati mutabili usati come chiave per una HashMap, o in questo caso, semplicemente inseriti in un HashSet.

L'output di questo codice è:

true
false

Quello che è successo è che l'hashCode usato quando è stato inserito è dove l'oggetto è nell'hash. La modifica dei valori utilizzati per calcolare l'hashCode non ricalcola l'hash stesso. Questo è il pericolo di mettere qualsiasi oggetto mutevole come chiave per un hash.

Quindi, tornando all'aspetto originale della domanda, il metodo chiamato non "conosce" come viene utilizzato l'oggetto come parametro. Fornire "lascia mutare l'oggetto" come unico modo per farlo significa che ci sono un numero di bug sottili che possono insinuarsi. Come perdere valori in un hash ... a meno che tu non ne aggiunga di più e l'hash venga rimesso a nuovo - fidati di me , questo è un brutto bug da cacciare (ho perso il valore fino a quando non aggiungo altri 20 elementi all'hashMap e poi improvvisamente si presenta di nuovo).

  • A meno che non ci sia molto buone ragioni per modificare l'oggetto, restituisce un nuovo oggetto è la cosa più sicura da fare.
  • Quando v'è un buon motivo per modificare l'oggetto, che modifica dovrebbe essere fatta dall'oggetto stesso (metodi invocano) piuttosto che attraverso una funzione esterna che può girarsi suoi campi.
    • Ciò consente di refactoring dell'oggetto con un costo di manutenzione inferiore.
    • Ciò consente all'oggetto di assicurarsi che i valori che calcolano il suo codice hash non cambiano (o non fanno parte del suo calcolo del codice hash)

Correlati: sovrascrivere e restituire il valore dell'argomento utilizzato come condizionale di un'istruzione if, all'interno della stessa istruzione if


3
Non suona bene. Molto probabilmente predictPricenon dovrebbe giocherellare direttamente con Itemi membri, ma cosa c'è di sbagliato nel chiamare metodi di Itemciò? Se questo è l'unico oggetto che viene modificato, forse puoi argomentare che dovrebbe essere un metodo dell'oggetto che viene modificato, ma altre volte vengono modificati più oggetti (che sicuramente non dovrebbero appartenere alla stessa classe), specialmente in metodi piuttosto di alto livello.

@delnan Devo ammettere che questa potrebbe essere una (eccessiva) reazione a un bug che una volta ho dovuto rintracciare una volta di un valore che scompare e riapparire in un hash - qualcuno stava armeggiando con i campi dell'oggetto nell'hash dopo che è stato inserito piuttosto che fare una copia dell'oggetto per il suo uso. Ci sono misure di programmazione difensiva che si possono prendere (ad esempio oggetti immutabili) - ma cose come ricalcolare i valori nell'oggetto stesso quando non si è il "proprietario" dell'oggetto, né l'oggetto stesso è qualcosa che può facilmente tornare a mordere sei duro.

Sai, c'è un buon argomento per usare più funzioni libere invece di funzioni di classe per migliorare l'incapsulamento. Naturalmente, una funzione che ottiene un oggetto costante (implicito come questo o un parametro) non dovrebbe modificarlo in alcun modo osservabile (alcuni oggetti costanti potrebbero memorizzare oggetti nella cache).
Deduplicatore

2

Seguo sempre la pratica di non mutare l'oggetto di qualcun altro. Vale a dire, solo mutando oggetti di proprietà della classe / struct / whathaveyou in cui ti trovi.

Data la seguente classe di dati:

class Item {
    public double price = 0;
}

Questo va bene:

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

E questo è molto chiaro:

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

Ma questo è troppo fangoso e misterioso per i miei gusti:

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

Si prega di accompagnare eventuali downvotes con un commento, quindi so come scrivere risposte migliori in futuro :)
Ben Leggiero

1

La sintassi è diversa tra le diverse lingue ed è insignificante mostrare un pezzo di codice che non è specifico di una lingua perché significa cose completamente diverse in lingue diverse.

In Java, Itemè un tipo di riferimento - è il tipo di puntatori a oggetti che sono istanze di Item. In C ++, questo tipo è scritto come Item *(la sintassi per la stessa cosa è diversa tra le lingue). Quindi Item predictPrice(Item item)in Java è equivalente a Item *predictPrice(Item *item)in C ++. In entrambi i casi, è una funzione (o metodo) che accetta un puntatore a un oggetto e restituisce un puntatore a un oggetto.

In C ++, Item(senza nient'altro) è un tipo di oggetto (supponendo che Itemsia il nome di una classe). Un valore di questo tipo "è" un oggetto e Item predictPrice(Item item)dichiara una funzione che accetta un oggetto e restituisce un oggetto. Poiché &non viene utilizzato, viene passato e restituito in base al valore. Ciò significa che l'oggetto viene copiato quando passato e copiato quando restituito. Non esiste un equivalente in Java perché Java non ha tipi di oggetti.

Quindi che cos'è? Molte delle cose che stai ponendo nella domanda (ad es. "Credo che funzioni sullo stesso oggetto che viene passato ...") dipende dalla comprensione esatta di ciò che viene passato e restituito qui.


1

La convenzione a cui ho visto aderire codebase è quella di preferire uno stile che sia chiaro dalla sintassi. Se la funzione cambia valore, preferisci passare per puntatore

void predictPrice(Item * const item);

Questo stile si traduce in:

  • Chiamandolo vedi:

    predictPrice (& my_item);

e sai che verrà modificato dalla tua aderenza allo stile.

  • Altrimenti preferisci passare per riferimento ref o valore quando non vuoi che la funzione modifichi l'istanza.

Va notato che, sebbene questa sia una sintassi molto più chiara, non è disponibile in Java.
Ben Leggiero,

0

Sebbene non abbia una forte preferenza, voterei che la restituzione dell'oggetto porta a una maggiore flessibilità per un'API.

Nota che ha senso solo mentre il metodo ha la libertà di restituire altri oggetti. Se il tuo javadoc dice @returns the same value of parameter ache è inutile. Ma se dice javadoc @return an instance that holds the same data than parameter a, restituire la stessa istanza o un'altra è un dettaglio di implementazione.

Ad esempio, nel mio progetto attuale (Java EE) ho un livello aziendale; per archiviare nel DB (e assegnare un ID automatico) un Dipendente, per esempio, un dipendente che ho qualcosa di simile

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

Naturalmente, nessuna differenza con questa implementazione perché JPA restituisce lo stesso oggetto dopo aver assegnato l'ID. Ora, se sono passato a JDBC

 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);
   ??????
 }

Ora a ????? Ho tre opzioni.

  1. Definisci un setter per Id e restituirò lo stesso oggetto. Brutto, perché setIdsarà disponibile ovunque.

  2. Esegui un intenso lavoro di riflessione e imposta l'id Employeenell'oggetto. Sporco, ma almeno è limitato al createEmployeerecord.

  3. Fornire un costruttore che copia l'originale Employeee accetta anche il idset.

  4. Recupera l'oggetto dal DB (forse necessario se alcuni valori di campo vengono calcolati nel DB o se desideri popolare una realtionship).

Ora, per 1. e 2. potresti restituire la stessa istanza, ma per 3. e 4. restituirai nuove istanze. Se dal principio che hai stabilito nella tua API che il valore "reale" sarà quello restituito dal metodo, avrai più libertà.

L'unico vero argomento a cui posso pensare è che, usando implementatios 1. o 2., le persone che usano la tua API potrebbero trascurare l'assegnazione del risultato e il loro codice si spezzerebbe se passi a implementazioni 3. o 4.).

Comunque, come puoi vedere, la mia preferenza si basa su altre preferenze personali (come vietare un setter per gli ID), quindi forse non si applica a tutti.


0

Quando si codifica in Java, si dovrebbe provare a fare in modo che l'utilizzo di ciascun oggetto di tipo mutabile corrisponda a uno dei due modelli:

  1. Esattamente un'entità è considerata il "proprietario" dell'oggetto mutabile e considera lo stato dell'oggetto come parte del suo. Altre entità possono contenere riferimenti, ma devono considerare il riferimento come identificativo di un oggetto di proprietà di qualcun altro.

  2. Nonostante l'oggetto sia mutabile, nessuno che ne detenga un riferimento è autorizzato a mutarlo, rendendo così l'istanza effettivamente immutabile (nota che a causa di stranezze nel modello di memoria di Java, non c'è un buon modo per rendere un oggetto effettivamente immutabile avere thread- semantica sicura immutabile senza aggiungere un ulteriore livello di riferimento indiretto a ciascun accesso). Qualsiasi entità che incapsula lo stato utilizzando un riferimento a tale oggetto e desidera modificare lo stato incapsulato deve creare un nuovo oggetto che contenga lo stato corretto.

Non mi piace che i metodi mutino l'oggetto sottostante e restituiscano un riferimento, dato che danno l'apparenza di adattarsi al secondo modello sopra. Ci sono alcuni casi in cui l'approccio può essere utile, ma il codice che sta per mutare gli oggetti dovrebbe sembrare che lo farà; il codice simile myThing = myThing.xyz(q);assomiglia molto meno a quello che muta l'oggetto identificato da myThingquanto sarebbe semplicemente myThing.xyz(q);.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.