Quale stile è meglio (variabile di istanza vs. valore di ritorno) in Java


32

Mi trovo spesso in difficoltà a decidere quale di questi due modi utilizzare quando devo utilizzare dati comuni attraverso alcuni metodi nelle mie classi. Quale sarebbe una scelta migliore?

In questa opzione, posso creare una variabile di istanza per evitare la necessità di dichiarare variabili aggiuntive e anche per definire i parametri del metodo, ma potrebbe non essere chiaro dove siano istanziate / modificate tali variabili:

public class MyClass {
    private int var1;

    MyClass(){
        doSomething();
        doSomethingElse();
        doMoreStuff();
    }

    private void doSomething(){
        var1 = 2;
    }

    private void doSomethingElse(){
        int var2 = var1 + 1;
    }

    private void doMoreStuff(){
        int var3 = var1 - 1;
    }
}

O semplicemente istanziare le variabili locali e passarle come argomenti?

public class MyClass {  
    MyClass(){
        int var1 = doSomething();
        doSomethingElse(var1);
        doMoreStuff(var1);
    }

    private int doSomething(){
        int var = 2;
        return var;
    }

    private void doSomethingElse(int var){
        int var2 = var + 1;
    }

    private void doMoreStuff(int var){
        int var3 = var - 1;
    }
}

Se la risposta è che sono entrambi corretti, quale viene visto / usato più spesso? Inoltre, se è possibile fornire ulteriori vantaggi / svantaggi per ciascuna opzione sarebbe molto utile.



1
Non credo che nessuno abbia ancora sottolineato che l'inserimento di risultati intermedi nelle variabili di istanza può rendere più difficile la concorrenza, a causa della possibilità di contesa tra thread per queste variabili.
sdenham,

Risposte:


107

Sono sorpreso che questo non sia ancora stato menzionato ...

Dipende se var1fa effettivamente parte dello stato dell'oggetto .

Supponi che entrambi questi approcci siano corretti e che sia solo una questione di stile. Tui hai torto.

Questo è interamente su come modellare correttamente.

Allo stesso modo, privateesistono metodi di istanza per mutare lo stato dell'oggetto . Se non è quello che sta facendo il tuo metodo, dovrebbe essere private static.


7
@mucaho: transientnon ha nulla a che fare con questo, perché transientriguarda lo stato persistente , come in alcune parti dell'oggetto che vengono salvate quando si fa qualcosa come serializzare l'oggetto. Ad esempio, il backing store di un ArrayList è transientanche se è assolutamente cruciale per lo stato di ArrayList, perché quando si serializza un ArrayList, si desidera salvare solo la parte del backing store che contiene elementi ArrayList effettivi, non lo spazio libero alla fine riservato per ulteriori aggiunte di elementi.
user2357112 supporta Monica il

4
Aggiungendo alla risposta - se var1è necessario per un paio di metodi ma non fa parte dello stato di MyClass, potrebbe essere il momento di mettere var1e quei metodi in un'altra classe da utilizzare MyClass.
Mike Partridge,

5
@CandiedOrange Stiamo parlando della modellazione corretta qui. Quando diciamo "parte dello stato dell'oggetto", non stiamo parlando dei pezzi letterali nel codice. Stiamo discutendo una nozione concettuale di stato, quale dovrebbe essere lo stato dell'oggetto per modellare correttamente i concetti. La "vita utile" è probabilmente il principale fattore decisivo in questo.
jpmc26,

4
@CandiedOrange Next e Previous sono ovviamente metodi pubblici. Se il valore di var1 è rilevante solo attraverso una sequenza di metodi privati, chiaramente non fa parte dello stato persistente dell'oggetto e quindi deve essere passato come argomento.
Taemyr,

2
@CandiedOrange Hai chiarito cosa intendevi, dopo due commenti. Sto dicendo che potresti facilmente avere nella prima risposta. Inoltre, sì, ho dimenticato che non c'è solo l'oggetto; Sono d'accordo che a volte i metodi di istanza privata hanno uno scopo. Non riuscivo a ricordarmene. EDIT: Ho appena realizzato che non eri la prima persona a rispondermi. Colpa mia. Ero confuso sul perché una risposta fosse una brusca frase, una frase, una non risposta, mentre la successiva spiegava effettivamente le cose.
Finanzi la causa di Monica il

17

Non so quale sia la più diffusa, ma farei sempre quest'ultima. Comunica più chiaramente il flusso e la durata dei dati e non gonfia ogni istanza della classe con un campo la cui unica durata rilevante è durante l'inizializzazione. Direi che il primo è solo confuso e rende la revisione del codice significativamente più difficile, perché devo considerare la possibilità che qualsiasi metodo possa modificare var1.


7

Dovresti ridurre l' ambito delle tue variabili il più possibile (e ragionevole). Non solo nei metodi, ma in generale.

Per la tua domanda ciò significa che dipende se la variabile fa parte dello stato dell'oggetto. Se sì, è OK usarlo in quell'ambito, cioè l'intero oggetto. In questo caso vai con la prima opzione. In caso contrario, scegli la seconda opzione in quanto riduce la visibilità delle variabili e quindi la complessità complessiva.


5

Quale stile è meglio (variabile di istanza vs. valore di ritorno) in Java

C'è un altro stile: usa un contesto / stato.

public static class MyClass {
    // Hold my state from one call to the next.
    public static final class State {
        int var1;
    }

    MyClass() {
        State state = new State();
        doSomething(state);
        doSomethingElse(state);
        doMoreStuff(state);
    }

    private void doSomething(State state) {
        state.var1 = 2;
    }

    private void doSomethingElse(State state) {
        int var2 = state.var1 + 1;
    }

    private void doMoreStuff(State state) {
        int var3 = state.var1 - 1;
    }
}

Ci sono una serie di vantaggi in questo approccio. Gli oggetti di stato possono cambiare indipendentemente dall'oggetto, ad esempio, dando molto spazio per il futuro.

Questo è un modello che funziona bene anche in un sistema distribuito / server in cui alcuni dettagli devono essere conservati attraverso le chiamate. È possibile memorizzare i dettagli dell'utente, le connessioni al database, ecc state. Nell'oggetto.


3

Si tratta di effetti collaterali.

Chiedere se var1fa parte dello stato manca il punto di questa domanda. Certo se var1deve persistere, deve essere un'istanza. Entrambi gli approcci possono essere fatti per funzionare se la persistenza è necessaria o meno.

L'approccio agli effetti collaterali

Alcune variabili di istanza vengono utilizzate solo per comunicare tra metodi privati ​​da una chiamata all'altra. Questo tipo di variabile di istanza può essere refactored dall'esistenza ma non deve esserlo. A volte le cose sono più chiare con loro. Ma questo non è senza rischi.

Stai lasciando uscire una variabile dal suo ambito perché è usata in due diversi ambiti privati. Non perché è necessario nell'ambito che lo stai posizionando. Questo può essere fonte di confusione. I "globi sono cattivi!" livello di confusione. Questo può funzionare ma non si adatta bene. Funziona solo nel piccolo. Nessun grande oggetto. Nessuna catena di eredità lunga. Non causare un yo-yo effetto .

L'approccio funzionale

Ora, anche se var1deve persistere, nulla dice che devi usare se per ogni valore transitorio potrebbe assumere prima di raggiungere lo stato che desideri conservare tra le chiamate pubbliche. Ciò significa che puoi ancora impostare avar1 un'istanza utilizzando nient'altro che metodi più funzionali.

Quindi, parte dello stato o no, puoi comunque usare entrambi gli approcci.

In questi esempi 'var1' è incapsulato in modo che nulla oltre al debugger sappia che esiste. Immagino che tu l'abbia fatto deliberatamente perché non vuoi influenzarci. Fortunatamente non mi interessa quale.

Il rischio di effetti collaterali

Detto questo, so da dove viene la tua domanda. Ho lavorato con miserabile yo yo 'ing eredità che muta una variabile di istanza a più livelli in diversi metodi e andato scoiattoli cercando di seguirlo. Questo è il rischio.

Questo è il dolore che mi spinge ad un approccio più funzionale. Un metodo può documentare le sue dipendenze e l'output nella sua firma. Questo è un approccio potente e chiaro. Ti consente inoltre di modificare ciò che passi al metodo privato rendendolo più riutilizzabile all'interno della classe.

Il lato positivo degli effetti collaterali

È anche limitante. Le funzioni pure non hanno effetti collaterali. Questa può essere una buona cosa ma non è orientata agli oggetti. Una grande parte dell'orientamento agli oggetti è la capacità di fare riferimento a un contesto esterno al metodo. Farlo senza far trapelare i globi da queste parti e scomparire è la forza di OOP. Ottengo la flessibilità di un globale ma è ben contenuto nella classe. Posso chiamare un metodo e mutare ogni variabile di istanza contemporaneamente se mi piace. Se lo faccio, sono obbligato almeno a dare al metodo un nome che chiarisca cosa sta facendo in modo che le persone non siano sorprese quando ciò accade. Anche i commenti possono aiutare. A volte questi commenti sono formalizzati come "condizioni post".

L'aspetto negativo di metodi privati ​​funzionali

L'approccio funzionale rende alcuni dipendenze. A meno che tu non sia in un linguaggio funzionale puro, non può escludere dipendenze nascoste. Non sai, guardando solo la firma di un metodo, che non ti nasconde un effetto collaterale nel resto del suo codice. Semplicemente no.

Post condizionali

Se tu e tutti gli altri membri del team documentate in modo affidabile gli effetti collaterali (condizioni pre / post) nei commenti, il vantaggio derivante dall'approccio funzionale è molto inferiore. Sì, lo so, continua a sognare.

Conclusione

Personalmente tendo verso metodi privati ​​funzionali in entrambi i casi, se possibile, ma onestamente è principalmente perché quei commenti sugli effetti collaterali pre / post condizionali non causano errori del compilatore quando sono obsoleti o quando i metodi vengono chiamati fuori servizio. A meno che non abbia davvero bisogno della flessibilità degli effetti collaterali, preferirei solo sapere che le cose funzionano.


1
"Alcune variabili di istanza vengono utilizzate solo per comunicare tra metodi privati ​​da una chiamata all'altra." È un odore di codice. Quando le variabili di istanza vengono utilizzate in questo modo, è un segno che la classe è troppo grande. Estrai variabili e metodi in una nuova classe.
Kevin Cline,

2
Questo non ha davvero senso. Mentre l'OP potrebbe scrivere il codice nel paradigma funzionale (e questo sarebbe generalmente una buona cosa), questo ovviamente non è il contesto della domanda. Cercare di dire all'OP che possono evitare di memorizzare lo stato dell'oggetto modificando i paradigmi non è davvero rilevante ...
jpmc26

@kevincline l'esempio del PO ha solo una variabile di istanza e 3 metodi. In sostanza, è già stato estratto in una nuova classe. Non che l'esempio faccia qualcosa di utile. La dimensione della classe non ha nulla a che fare con il modo in cui i tuoi metodi di supporto privati ​​comunicano tra loro.
MagicWindow,

@ jpmc26 OP ha già dimostrato che possono evitare di essere archiviati var1come variabile di stato modificando i paradigmi. L'ambito della classe NON è solo dove è archiviato lo stato. È anche un ambito di applicazione. Ciò significa che ci sono due possibili motivazioni per mettere una variabile a livello di classe. Puoi affermare di farlo solo per lo scopo racchiuso e non lo stato è male, ma dico che è un compromesso con l' arità che è anche malvagia. Lo so perché ho dovuto mantenere il codice per farlo. Alcuni sono stati fatti bene. Alcuni è stato un incubo. La linea tra i due non è stato. È leggibilità.
MagicWindow,

La regola dello stato migliora la leggibilità: 1) evitare che i risultati intermedi delle operazioni assomiglino allo stato 2) evitare di nascondere le dipendenze dei metodi. Se l'arità di un metodo è elevata, allora rappresenta irriducibilmente e sinceramente la complessità di quel metodo o il progetto attuale ha complessità inutili.
sdenham,

1

La prima variante sembra non intuitiva e potenzialmente pericolosa per me (immagina per qualsiasi motivo che qualcuno renda pubblico il tuo metodo privato).

Preferirei molto istanziare le tue variabili sulla costruzione di una classe, o passarle come argomenti. Quest'ultimo ti dà la possibilità di usare modi di dire funzionali e di non fare affidamento sullo stato dell'oggetto contenitore.


0

Ci sono già risposte che parlano dello stato dell'oggetto e di quando si preferisce il secondo metodo. Voglio solo aggiungere un caso d'uso comune per il primo modello.

Il primo modello è perfettamente valido quando tutto ciò che fa la tua classe è che incapsula un algoritmo . Un caso d'uso per questo è che se si scrivesse l'algoritmo in un singolo metodo sarebbe troppo grande. Quindi lo scomponi in metodi più piccoli, lo rendi una classe e rendi i metodi secondari privati.

Ora passare tutto lo stato dell'algoritmo tramite parametri potrebbe diventare noioso, quindi usi i campi privati. È anche in accordo con la regola del primo paragrafo perché è fondamentalmente uno stato dell'istanza. Devi solo tenere a mente e documentare correttamente che l'algoritmo non è rientrante se usi campi privati ​​per questo . Questo non dovrebbe essere un problema per la maggior parte del tempo, ma può forse morderti.


0

Proviamo un esempio che fa qualcosa. Perdonami dato che questo è javascript non java. Il punto dovrebbe essere lo stesso.

Visita https://blockly-games.appspot.com/pond-duck?lang=en , fai clic sulla scheda javascript e incolla questo:

hunt = lock(-90,1), 
heading = -135;

while(true) {
  hunt()  
  heading += 2
  swim(heading,30)
}

function lock(direction, width) {
  var dir = direction
  var wid = width
  var dis = 10000

  //hunt
  return function() {
    //randomize() //Calling this here makes the state of dir meaningless
    scanLock()
    adjustWid()
    if (isSpotted()) {
      if (inRange()) {
        if (wid <= 4) {
          cannon(dir, dis)
        }
      }
    } else {
      if (!left()) {
        right()
      }
    }
  }

  function scanLock() {
    dis = scan(dir, wid);
  }

  function adjustWid() {
    if (inRange()) {
      if (wid > 1)
        wid /= 2;
    } else {
      if (wid < 16) {
        wid *= 2; 
      }
    }
  }

  function isSpotted() {
    return dis < 1000;
  }

  function left() {
    dir += wid;
    scanLock();
    return isSpotted();
  }

  function right() {
    dir -= wid*2;
    scanLock();
    return isSpotted()
  }

  function inRange() {
    return dis < 70;
  }

  function randomize() {
    dir = Math.random() * 360
  }
}

Dovresti notare che dir, wide disnon stai passando molto in giro. Dovresti anche notare che il codice nella funzione che lock()restituisce assomiglia molto allo pseudo codice. Bene, è un vero codice. Eppure è molto facile da leggere. Potremmo aggiungere passaggi e assegnazioni, ma ciò aggiungerebbe disordine che non vedresti mai nello pseudo codice.

Se vuoi sostenere che non fare l'assegnazione e il passaggio va bene perché quei tre var sono uno stato persistente, considera una riprogettazione che assegna dirun valore casuale ad ogni ciclo. Non persistente ora, vero?

Certo, ora potremmo cadere dir di portata ora, ma non senza essere costretti a ingombrare il nostro codice pseudo simile con il passaggio e l'impostazione.

Quindi no, lo stato non è il motivo per cui decidi di utilizzare o meno gli effetti collaterali piuttosto che passare e tornare. Né gli effetti collaterali da soli significano che il tuo codice è illeggibile. Non si ottengono i vantaggi del puro codice funzionale . Ma ben fatti, con buoni nomi, possono effettivamente essere belli da leggere.

Ciò non significa che non possano trasformarsi in spaghetti come un incubo. Ma allora cosa non può?

Buona caccia alle anatre.


1
Le funzioni interne / esterne sono un paradigma diverso da quello che OP descrive. - Concordo sul fatto che il compromesso tra variabili locali in una funzione esterna contra passato argomenti alle funzioni interne è simile alle variabili private su una classe che passa argomenti contro funzioni private. Tuttavia, se si esegue questo confronto, la durata dell'oggetto corrisponderebbe a una singola esecuzione della funzione, quindi le variabili nella funzione esterna fanno parte dello stato persistente.
Taemyr,

@Taemyr l'OP descrive un metodo privato chiamato all'interno di un costruttore pubblico che sembra molto simile a quello che c'è dentro di me. Lo stato non è veramente 'persistente' se lo passi ogni volta che entri in una funzione. A volte lo stato viene utilizzato come luogo condiviso in cui i metodi possono scrivere e leggere. Non significa che debba essere persistito. Il fatto che sia persistito comunque non è qualcosa su cui DEVE dipendere.
candied_orange,

1
È persistente anche se lo calpesti ogni volta che elimini l'oggetto.
Taemyr,

1
Aspetti positivi, ma esprimerli in JavaScript offusca davvero la discussione. Inoltre, se togli la sintassi JS,
finisci

1
@sdenham Sto sostenendo che esiste una differenza tra gli attributi di una classe e i risultati intermedi transitori di un'operazione. Non sono equivalenti. Ma uno può essere memorizzato nell'altro. Certamente non deve esserlo. La domanda è se mai dovrebbe essere. Sì, ci sono problemi semantici e permanenti. Ci sono anche problemi di arity. Non pensi che siano abbastanza importanti. Va bene. Ma dire che l'uso del livello di classe per qualsiasi cosa diversa dallo stato dell'oggetto è una modellazione errata insiste su un modo di modellizzazione. Ho mantenuto il codice professionale che ha fatto diversamente.
candied_orange,
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.