Quanta logica in Getters


46

I miei colleghi mi dicono che dovrebbe esserci la minima logica possibile nei getter e nei setter.

Tuttavia, sono convinto che molte cose possano essere nascoste in getter e setter per proteggere utenti / programmatori dai dettagli di implementazione.

Un esempio di ciò che faccio:

public List<Stuff> getStuff()
{
   if (stuff == null || cacheInvalid())
   {
       stuff = getStuffFromDatabase();
   }
   return stuff;
}

Un esempio di come il lavoro mi dice di fare le cose (citano "Clean Code" di zio Bob):

public List<Stuff> getStuff()
{
    return stuff;
}

public void loadStuff()
{
    stuff = getStuffFromDatabase();
}

Quanta logica è appropriata in un setter / getter? A che cosa servono getter e setter vuoti tranne una violazione del nascondimento dei dati?


6
A me sembra più simile a TryGetStuff () ...
Bill Michell,

16
Questo non è un "getter". Questo termine viene utilizzato per l'accesso in lettura di una proprietà, non per un metodo in cui si inserisce accidentalmente un "get" nel nome.
Boris Yankov,

6
Non so se quel secondo esempio sia un buon esempio di questo libro di codice pulito di cui parli, o qualcuno che ottenga la parte sbagliata della chiavetta al riguardo, ma una cosa che il disordine fragile non è, è il codice pulito.
Jon Hanna,

@BorisYankov Bene ... il secondo metodo è. public List<Stuff> getStuff() { return stuff; }
R. Schmitz,

A seconda del caso d'uso esatto, mi piace separare la cache in una classe separata. Crea StuffGetterun'interfaccia, implementa una StuffComputerche esegue i calcoli e la avvolge all'interno di un oggetto StuffCacher, che è responsabile dell'accesso alla cache o dell'inoltro delle chiamate a StuffComputerquella che avvolge.
Alexander

Risposte:


71

Il modo in cui il lavoro ti dice di fare le cose è zoppo.

Come regola generale, il modo in cui faccio le cose è il seguente: se ottenere le cose è computazionalmente economico (o se la maggior parte delle probabilità è che si troverà nella cache), allora il tuo stile di getStuff () va bene. Se si sa che ottenere le cose è costoso dal punto di vista computazionale, così costoso che pubblicizzare la sua costanza è necessario all'interfaccia, allora non lo chiamerei getStuff (), lo chiamerei calcoliStuff () o qualcosa del genere, in modo da indicare che ci sarà del lavoro da fare.

In entrambi i casi, il modo in cui il lavoro ti dice di fare le cose è lame, perché getStuff () esploderà se loadStuff () non è stato chiamato in anticipo, quindi essenzialmente vogliono che tu complichi la tua interfaccia introducendo la complessità dell'ordine delle operazioni ad esso. L'ordine delle operazioni riguarda praticamente il peggior tipo di complessità che mi viene in mente.


23
+1 per menzionare la complessità dell'ordine delle operazioni. Per ovviare al problema, forse lavoro mi chiederà di chiamare sempre loadStuff () nel costruttore, ma sarebbe un problema anche perché significa che dovrà sempre essere caricato. Nel primo esempio, i dati vengono caricati pigramente solo quando necessario, il che è buono come può essere.
Laurent,

6
Di solito seguo la regola di "se è davvero economico, usa un getter di proprietà. Se è costoso, usa una funzione". Questo di solito mi serve bene, e nominare in modo appropriato come hai indicato per enfatizzare mi sembra buono.
Denis Troller,

3
se può fallire, non è un getter. In questo caso cosa succede se il collegamento DB non è attivo?
Martin Beckett,

6
+1, sono un po 'scioccato da quante risposte sbagliate sono state pubblicate. Getter / setter esistono per nascondere i dettagli di implementazione, altrimenti la variabile dovrebbe essere resa pubblica.
Izkata,

2
Non dimenticare che richiedere che la loadStuff()funzione venga chiamata prima della getStuff()funzione significa anche che la classe non sta appropriando in modo corretto ciò che sta accadendo sotto il cofano.
rjzii,

23

La logica nei getter va benissimo.

Ma ottenere dati da un database è molto più che "logica". Implica una serie di operazioni molto costose in cui molte cose possono andare storte e in modo non deterministico. Esiterei a farlo implicitamente in un modo migliore.

D'altra parte, la maggior parte degli ORM supporta il caricamento lento delle raccolte, che è sostanzialmente esattamente quello che stai facendo.


18

Penso che secondo "Clean Code" dovrebbe essere suddiviso il più possibile, in qualcosa del tipo:

public List<Stuff> getStuff() {
   if (hasStuff()) {
       return stuff;
   }
   loadStuff();
   return stuff;
}

private boolean hasStuff() {
    if (stuff == null) {
       return false;
    }
    if (cacheInvalid()) {
       return false;        
    }
    return true;
} 

private void loadStuff() {
    stuff = getStuffFromDatabase();
}

Naturalmente, questa è una totale assurdità, dato che la bella forma, che hai scritto, fa la cosa giusta con una frazione di codice che chiunque capisce a colpo d'occhio:

public List<Stuff> getStuff() {
   if (stuff == null || cacheInvalid()) {
       stuff = getStuffFromDatabase();
   }
   return stuff;
}

Non dovrebbe essere il mal di testa del chiamante come la roba viene messa sotto il cofano, e in particolare non dovrebbe essere il mal di testa del chiamante a ricordare di chiamare le cose in un arbitrario "giusto ordine".


8
-1. Il vero mal di testa sarà quando il chiamante è bloccato a capire perché una semplice chiamata getter ha provocato un accesso al database lento come l'inferno.
Domenic,

14
@Domenic: l'accesso al database deve essere fatto comunque, non si sta salvando le prestazioni di nessuno non farlo. Se ne hai bisogno List<Stuff>, c'è solo un modo per ottenerlo.
DeadMG,

4
@lukas: Grazie, non mi sono ricordato di tutti i trucchi utilizzati nel codice 'Clean' per rendere banali bit di codice ancora una riga in più ;-) Risolto ora.
Joonas Pulakka,

2
Stai calunniando Robert Martin. Non espanderebbe mai una semplice disgiunzione booleana in una funzione a nove righe. La tua funzione hasStuffè l'opposto del codice pulito.
Kevin Cline,

2
Ho letto l'inizio di questa risposta, e stavo per aggirarla, pensando "ecco un altro adoratore del libro", e poi la parte "Certo, questa è una totale assurdità" ha attirato la mia attenzione. Ben detto! C -: =
Mike Nakis,

8

Mi dicono che dovrebbe esserci la minima logica possibile in getter e setter.

Ci deve essere tutta la logica necessaria per soddisfare i bisogni della classe. La mia preferenza personale è il meno possibile, ma quando si mantiene il codice, di solito è necessario lasciare l'interfaccia originale con i getter / setter esistenti, ma inserire molta logica in essi per correggere la logica aziendale più recente (ad esempio, un "cliente "Il getter in un ambiente post-911 deve rispettare le norme" Conosci il tuo cliente "e OFAC , combinate con una politica aziendale che proibisce la comparsa di clienti provenienti da determinati paesi [come Cuba o Iran]).

Nel tuo esempio, preferisco il tuo e non mi piace l'esempio di "zio bob" poiché la versione di "zio bob" richiede che gli utenti / manutentori ricordino di chiamare loadStuff()prima di chiamare getStuff()- questa è una ricetta per il disastro se qualcuno dei tuoi manutentori dimentica (o peggio, mai saputo). La maggior parte dei posti in cui ho lavorato nell'ultimo decennio usano ancora codice che ha più di un decennio, quindi la facilità di manutenzione è un fattore critico da considerare.


6

Hai ragione, i tuoi colleghi hanno torto.

Dimentica le regole empiriche di tutti su ciò che un metodo get dovrebbe o non dovrebbe fare. Una classe dovrebbe presentare un'astrazione di qualcosa. La tua classe è leggibile stuff. In Java è convenzionale usare i metodi 'get' per leggere le proprietà. Miliardi di righe di framework sono state scritte in attesa di leggere stuffchiamando getStuff. Se nominate la vostra funzione fetchStuffo qualcosa di diverso getStuff, la vostra classe sarà incompatibile con tutti quei framework.

Potresti indicarli a Hibernate, dove 'getStuff ()' può fare alcune cose molto complicate e getta una RuntimeException in caso di fallimento.


Hibernate è un ORM, quindi il pacchetto stesso esprime l'intento. Questo intento non è così facilmente comprensibile se il pacchetto stesso non è un ORM.
FMJaguar,

@FMJaguar: è perfettamente comprensibile. L'ibernazione consente di estrarre le operazioni del database per presentare una rete di oggetti. L'OP sta estraendo un'operazione di database per presentare un oggetto con una proprietà denominata stuff. Entrambi nascondono i dettagli per facilitare la scrittura del codice chiamante.
Kevin Cline,

Se quella classe è una classe ORM, allora l'intento è già espresso, in altri contesti: la domanda rimane: "Come fa un altro programmatore a conoscere gli effetti collaterali della chiamata getter?". Se il programma contiene classi 1k e 10k getter, una politica che consente le chiamate al database in ognuna di esse potrebbe essere un problema
FMJaguar

4

Sembra che questo potrebbe essere un po 'un dibattito purista contro l'applicazione che potrebbe essere influenzato da come preferisci controllare i nomi delle funzioni. Dal punto di vista applicato, preferirei di gran lunga vedere:

List<String> names = clientRoster.getNames();
List<String> emails = clientRoster.getEmails();

Al contrario di:

myObject.load();
List<String> names = clientRoster.getNames();
List<String> emails = clientRoster.getEmails();

O peggio ancora:

myObject.loadNames();
List<String> names = clientRoster.getNames();
myOjbect.loadEmails();
List<String> emails = clientRoster.getEmails();

Il che tende a rendere altri codici molto più ridondanti e difficili da leggere perché devi iniziare a guadare attraverso tutte le chiamate simili. Inoltre, la chiamata a funzioni del caricatore o simili interrompe l'intero scopo persino di utilizzare OOP in quanto non si viene più sottratti ai dettagli di implementazione dell'oggetto con cui si sta lavorando. Se hai un clientRosteroggetto, non dovresti preoccuparti di come getNamesfunziona, come se dovessi chiamare un loadNames, dovresti solo sapere che getNamesti dà un List<String>con i nomi dei clienti.

Quindi, sembra che il problema riguardi più la semantica e il nome migliore per la funzione per ottenere i dati. Se la società (e altri) ha un problema con il prefisso gete set, allora che ne dici di chiamare la funzione in modo simile retrieveNames? Dice cosa sta succedendo ma non implica che l'operazione sarebbe istantanea come ci si potrebbe aspettare da un getmetodo.

In termini di logica in un metodo accessor, tienilo al minimo in quanto sono generalmente impliciti come istantanei con solo l'interazione nominale che si verifica con la variabile. Tuttavia, ciò vale anche generalmente solo per tipi semplici, tipi di dati complessi (es. List) Che trovo più difficile da incapsulare correttamente in una proprietà e generalmente utilizzo altri metodi per interagire con essi invece di un semplice mutatore e accessore.


2

Chiamare un getter dovrebbe mostrare lo stesso comportamento della lettura di un campo:

  • Dovrebbe essere economico recuperare il valore
  • Se si imposta un valore con il setter e lo si legge con il getter, il valore dovrebbe essere lo stesso
  • Ottenere il valore non dovrebbe avere effetti collaterali
  • Non dovrebbe generare un'eccezione

2
Non sono completamente d'accordo su questo. Sono d'accordo che non dovrebbe avere effetti collaterali, ma penso che sia perfettamente corretto implementarlo in un modo che lo differenzia da un campo. Guardando il .Net BCL, InvalidOperationException è ampiamente usato quando si guardano i getter. Inoltre, vedi la risposta di MikeNakis sull'ordine delle operazioni.
Max

Concordo con tutti i punti tranne l'ultimo. È certamente possibile che ottenere un valore comporti l'esecuzione di un calcolo o di qualche altra operazione che dipende da altri valori o risorse che potrebbero non essere stati impostati. In quei casi mi aspetterei che il getter lanci una sorta di eccezione.
TMN il

1
@TMN: nel migliore dei casi la classe dovrebbe essere organizzata in modo tale che i getter non debbano eseguire operazioni in grado di generare eccezioni. Ridurre al minimo i luoghi che possono generare eccezioni porta a sorprese meno impreviste.
hugomg,

8
Ho intenzione di essere in disaccordo con il secondo punto con un esempio specifico: foo.setAngle(361); bar = foo.getAngle(). barpotrebbe essere 361, ma potrebbe anche legittimamente essere 1se gli angoli sono legati a un intervallo.
zzzzBov,

1
-1. (1) è economico in questo esempio - dopo il caricamento lento. (2) Al momento non v'è alcuna "setter" nell'esempio, ma se qualcuno aggiunge uno dopo, e solo set stuff, il getter si restituiscono lo stesso valore. (3) Il caricamento lento come mostrato nell'esempio non produce effetti collaterali "visibili". (4) è discutibile, forse un punto valido, dal momento che l'introduzione del "caricamento lento" in seguito può modificare il precedente contratto API - ma bisogna prendere in considerazione quel contratto per prendere una decisione.
Doc Brown,

2

Un getter che invoca altre proprietà e metodi per calcolare il proprio valore implica anche una dipendenza. Ad esempio, se la proprietà deve essere in grado di calcolare se stessa e, in tal caso, è necessario impostare un altro membro, è necessario preoccuparsi di riferimenti null accidentali se si accede alla proprietà nel codice di inizializzazione in cui tutti i membri non sono necessariamente impostati.

Ciò non significa "non accedere mai a un altro membro che non sia il campo di supporto delle proprietà all'interno del getter", significa solo prestare attenzione a ciò che stai implicando su quale sia lo stato richiesto dell'oggetto e se corrisponde al contesto che ti aspetti questa proprietà a cui si accede.

Tuttavia, nei due esempi concreti che hai fornito, il motivo per cui sceglierei uno sull'altro è completamente diverso. Il tuo getter viene inizializzato al primo accesso, ad esempio Lazy Initialization . Si presume che il secondo esempio sia inizializzato in un punto precedente, ad es . Inizializzazione esplicita .

Quando si verifica esattamente l'inizializzazione può o meno essere importante.

Ad esempio, potrebbe essere molto molto lento e deve essere eseguito durante una fase di caricamento in cui l'utente si aspetta un ritardo, piuttosto che il singhiozzo delle prestazioni inaspettatamente quando l'utente attiva per la prima volta l'accesso (ovvero, clic con il tasto destro dell'utente, viene visualizzato il menu di scelta rapida, utente ha già fatto nuovamente clic con il pulsante destro del mouse).

Inoltre, a volte c'è un punto evidente nell'esecuzione in cui si verifica tutto ciò che può influenzare / sporcare il valore della proprietà memorizzata nella cache. Potresti anche verificare che nessuna delle dipendenze cambi e lanciare eccezioni in seguito. In questa situazione ha senso anche memorizzare nella cache il valore a quel punto, anche se non è particolarmente costoso da calcolare, solo per evitare di rendere l'esecuzione del codice più complessa e più difficile da seguire mentalmente.

Detto questo, Lazy Initialization ha molto senso in molte altre situazioni. Quindi, come spesso accade nella programmazione, è difficile ridurre a una regola, si riduce al codice concreto.


0

Fallo come ha detto @MikeNakis ... Se ottieni semplicemente le cose, allora va bene ... Se fai qualcos'altro crea una nuova funzione che fa il lavoro e lo rende pubblico.

Se la tua proprietà / funzione sta facendo solo ciò che dice il suo nome, non c'è più spazio per le complicazioni. La coesione è l'IMO chiave


1
Fai attenzione a questo, puoi finire per esporre troppo del tuo stato interno. Non si vuole, per concludere con un sacco di vuoti loadFoo()o preloadDummyReferences()o createDefaultValuesForUninitializedFields()metodi solo perché l'implementazione iniziale della classe aveva bisogno di loro.
TMN il

Certo ... stavo solo dicendo che se fai ciò che il nome afferma che non dovrebbero esserci molti problemi ... ma quello che dici è assolutamente vero ...
Ivan Crojach Karačić

0

Personalmente, esporrei il requisito di Stuff tramite un parametro nel costruttore, e consentirei a qualsiasi classe che stia istanziando roba di fare il lavoro per capire da dove dovrebbe provenire. Se stuff è null, dovrebbe restituire null. Preferisco non tentare soluzioni intelligenti come l'originale dell'OP perché è un modo semplice per nascondere i bug all'interno dell'implementazione in cui non è affatto ovvio cosa potrebbe andare storto quando qualcosa si rompe.


0

Ci sono questioni più importanti della "appropriatezza" qui e dovresti basare la tua decisione su quelle . Principalmente, la grande decisione qui è se vuoi consentire alle persone di bypassare la cache o meno.

  1. Innanzitutto, pensa se esiste un modo per riorganizzare il codice in modo che tutte le chiamate di caricamento necessarie e la gestione della cache vengano eseguite nel costruttore / inizializzatore. Se questo è possibile, puoi creare una classe il cui invariante ti consente di eseguire il semplice getter dalla parte 2 con la sicurezza del getter complesso dalla parte 1. (Uno scenario vantaggioso per entrambe le parti)

  2. Se non riesci a creare una tale classe, decidi se hai un compromesso e devi decidere se vuoi consentire al consumatore di saltare o meno il codice di controllo della cache.

    1. Se è importante che il consumatore non salti mai il controllo della cache e non ti dispiaccia le penalità di prestazione, allora abbina il controllo all'interno del getter e rendi impossibile al consumatore fare la cosa sbagliata.

    2. Se è OK saltare il controllo della cache o è molto importante garantire prestazioni O (1) nel getter, utilizzare chiamate separate.

Come avrai già notato, non sono un grande fan della filosofia "clean-code", "dividi tutto in minuscole funzioni". Se hai un sacco di funzioni ortogonali che possono essere chiamate in qualsiasi ordine, suddividerle ti darà più potere espressivo a basso costo. Tuttavia, se le tue funzioni hanno dipendenze dell'ordine (o sono davvero utili solo in un ordine particolare), dividerle aumenta solo il numero di modi in cui puoi fare cose sbagliate, aggiungendo al contempo pochi vantaggi.


-1, i costruttori dovrebbero costruire, non inizializzare. Mettere la logica del database in un costruttore rende quella classe completamente non testabile e se ne hai più di una manciata, il tempo di avvio dell'applicazione diventerà immenso. E questo è solo per cominciare.
Domenic,

@Domenic: questo è un problema semantico e dipendente dalla lingua. Il punto in cui un oggetto è adatto all'uso e fornisce gli invarianti appropriati dopo, e solo dopo, è completamente costruito.
hugomg,

0

Secondo me, Getters non dovrebbe avere molta logica in essi. Non dovrebbero avere effetti collaterali e non dovresti mai ottenere un'eccezione da loro. A meno che, naturalmente, tu sappia cosa stai facendo. La maggior parte dei miei getter non ha alcuna logica e va semplicemente in un campo. Ma la notevole eccezione era rappresentata da un'API pubblica che doveva essere il più semplice possibile da utilizzare. Quindi ho avuto un getter che avrebbe fallito se non fosse stato chiamato un altro getter. La soluzione? Una riga di codice come var throwaway=MyGetter;nel getter che dipendeva da esso. Non ne vado fiero, ma non vedo ancora un modo più pulito per farlo


0

Sembra una lettura dalla cache con caricamento lento. Come altri hanno notato, il controllo e il caricamento possono appartenere ad altri metodi. Potrebbe essere necessario sincronizzare il caricamento in modo da non ricevere venti thread tutti in caricamento contemporaneamente.

Potrebbe essere appropriato utilizzare un nome getCachedStuff()per il getter in quanto non avrà un tempo di esecuzione coerente.

A seconda di come funziona la cacheInvalid()routine, il controllo per null potrebbe non essere necessario. Non mi aspetto che la cache sia valida se non stufffosse stata popolata dal database.


0

La logica principale che mi aspetto di vedere nei getter che restituiscono un elenco è la logica per assicurarsi che l'elenco non sia modificabile. Allo stato attuale entrambi i tuoi esempi possono potenzialmente rompere l'incapsulamento.

qualcosa di simile a:

public List<Stuff> getStuff()
{
    return Collections.unmodifiableList(stuff);
}

per quanto riguarda la memorizzazione nella cache nel getter, penso che questo sarebbe OK ma potrei essere tentato di spostare la logica della cache se la creazione della cache richiedesse un tempo significativo. cioè dipende.


0

A seconda del caso d'uso esatto, mi piace separare la cache in una classe separata. Crea StuffGetterun'interfaccia, implementa una StuffComputerche esegue i calcoli e la avvolge all'interno di un oggetto StuffCacher, che è responsabile dell'accesso alla cache o dell'inoltro delle chiamate a StuffComputerquella che avvolge.

interface StuffGetter {
     public List<Stuff> getStuff();
}

class StuffComputer implements StuffGetter {
     public List<Stuff> getStuff() {
         getStuffFromDatabase()
     }
}

class StuffCacher implements StuffGetter {
     private stuffComputer; // DI this
     private Cache<List<Stuff>> cache = new Cache<>();

     public List<Stuff> getStuff() {
         if cache.hasStuff() {
             return cache.getStuff();
         }

         List<Stuffs> stuffs = stuffComputer.getStuff();
         cache.store(stuffs);
         return stuffs;
     }
}

Questo design consente di aggiungere facilmente la memorizzazione nella cache, rimuovere la memorizzazione nella cache, modificare la logica di derivazione sottostante (ad esempio, accedere a un DB e restituire dati falsi), ecc. È un po 'prolisso, ma vale la pena per progetti sufficientemente avanzati.


-1

IMHO è molto semplice se si utilizza un progetto per contratto. Decidi cosa dovrebbe fornire il tuo getter e semplicemente codifica di conseguenza (codice semplice o qualche logica complessa che potrebbe essere coinvolta o delegata da qualche parte).


+1: sono d'accordo con te! Se l'oggetto ha solo lo scopo di contenere alcuni dati, i getter dovrebbero solo restituire il contenuto corrente dell'oggetto. In questo caso è responsabilità di qualche altro oggetto caricare i dati. Se il contratto afferma che l'oggetto è il proxy di un record del database, il getter dovrebbe recuperare i dati al volo. Può essere ancora più complicato se i dati sono stati caricati ma non sono aggiornati: l'oggetto dovrebbe essere informato delle modifiche nel database? Penso che non ci sia una risposta unica a questa domanda.
Giorgio,
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.