Cosa dovrebbe essere consentito all'interno di getter e setter?


45

Ho avuto un'interessante discussione su Internet sui metodi e sull'incapsulamento di getter e setter. Qualcuno ha detto che tutto ciò che dovrebbero fare è un incarico (setter) o un accesso variabile (getter) per mantenerli "puri" e garantire l'incapsulamento.

  • Ho ragione nel dire che ciò sconfiggerebbe completamente lo scopo di avere getter e setter in primo luogo e che la validazione e altre logiche (senza strani effetti collaterali ovviamente) dovrebbero essere consentite?
  • Quando dovrebbe avvenire la convalida?
    • Quando si imposta il valore, all'interno del setter (per proteggere l'oggetto dal non entrare mai in uno stato non valido - la mia opinione)
    • Prima di impostare il valore, al di fuori del setter
    • All'interno dell'oggetto, prima di ogni volta che viene utilizzato il valore
  • Un setter è autorizzato a modificare il valore (magari convertire un valore valido in una rappresentazione interna canonica)?

18
La cosa migliore di getter e setter è la possibilità di non averli , vale a dire lasciare il setter e hai una proprietà di sola lettura, lasciare il getter e hai un'opzione di configurazione il cui valore attuale non è affare di nessuno.
Michael Borgwardt,

7
@MichaelBorgwardt Lascia entrambi fuori per avere un'interfaccia “dillo, non chiedere” pulita. Proprietà = potenziale odore di codice.
Konrad Rudolph,


2
I setter possono anche essere usati per organizzare un evento .
Giovanni Isaia Carmona,

@KonradRudolph Concordo con la tua affermazione, anche se vorrei davvero sottolineare la parola "potenziale".
Phil,

Risposte:


37

Ricordo di aver avuto una discussione simile con il mio professore durante l'apprendimento del C ++ all'università. Non riuscivo a vedere il punto di usare getter e setter quando potevo rendere pubblica una variabile. Ora capisco meglio con anni di esperienza e ho imparato una ragione migliore del semplice dire "mantenere l'incapsulamento".

Definendo i getter e i setter, fornirai un'interfaccia coerente in modo che se desideri modificare l'implementazione, è meno probabile che tu rompa il codice dipendente. Ciò è particolarmente importante quando le classi vengono esposte tramite un'API e utilizzate in altre app o da terze parti. E che dire delle cose che vanno nel getter o nel setter?

I getter sono generalmente meglio implementati come un semplice passthrough attenuato per l'accesso a un valore perché ciò rende prevedibile il loro comportamento. Dico in generale, perché ho visto casi in cui i getter sono stati usati per accedere a valori manipolati dal calcolo o anche dal codice condizionale. Generalmente non è così buono se stai creando componenti visivi da utilizzare in fase di progettazione, ma apparentemente utile in fase di esecuzione. Tuttavia, non esiste alcuna reale differenza tra questo e l'utilizzo di un metodo semplice, tranne per il fatto che quando si utilizza un metodo, è generalmente più probabile che si denomini un metodo in modo più appropriato in modo che la funzionalità del "getter" sia più evidente durante la lettura del codice.

Confronta quanto segue:

int aValue = MyClass.Value;

e

int aValue = MyClass.CalculateValue();

La seconda opzione chiarisce che il valore viene calcolato, mentre il primo esempio ti dice che stai semplicemente restituendo un valore senza conoscere nulla sul valore stesso.

Potresti forse sostenere che quanto segue sarebbe più chiaro:

int aValue = MyClass.CalculatedValue;

Il problema tuttavia è che stai assumendo che il valore sia già stato manipolato altrove. Quindi, nel caso di un getter, mentre potresti voler supporre che potrebbe succedere qualcos'altro quando stai restituendo un valore, è difficile chiarire tali cose nel contesto di una proprietà e i nomi delle proprietà non dovrebbero mai contenere verbi in caso contrario, risulta difficile capire a colpo d'occhio se il nome utilizzato deve essere decorato con parentesi quando si accede.

I setter sono un caso leggermente diverso. È del tutto appropriato che un setter fornisca un'ulteriore elaborazione al fine di convalidare i dati inviati a una proprietà, generando un'eccezione se l'impostazione di un valore violerebbe i confini definiti della proprietà. Il problema che alcuni sviluppatori hanno nell'aggiungere l'elaborazione ai setter è che c'è sempre la tentazione di far fare un po 'di più al setter, come eseguire un calcolo o una manipolazione dei dati in qualche modo. Qui è possibile ottenere effetti collaterali che in alcuni casi possono essere imprevedibili o indesiderati.

Nel caso dei setter, applico sempre una semplice regola empirica, che consiste nel fare il meno possibile ai dati. Ad esempio, di solito permetterò sia i test al contorno, sia gli arrotondamenti in modo da poter sollevare eccezioni, se del caso, o evitare eccezioni inutili dove possono essere sensibilmente evitate. Le proprietà in virgola mobile sono un buon esempio in cui potresti voler arrotondare posizioni decimali eccessive per evitare di sollevare un'eccezione, consentendo comunque di inserire valori nell'intervallo con alcune posizioni decimali aggiuntive.

Se si applica una sorta di manipolazione dell'input del setter, si ha lo stesso problema con il getter, che è difficile consentire ad altri di sapere cosa sta facendo il setter semplicemente nominandolo. Per esempio:

MyClass.Value = 12345;

Questo ti dice qualcosa su ciò che accadrà al valore quando viene dato al setter?

Che ne dite di:

MyClass.RoundValueToNearestThousand(12345);

Il secondo esempio ti dice esattamente cosa succederà ai tuoi dati, mentre il primo non ti farà sapere se il tuo valore verrà modificato arbitrariamente. Durante la lettura del codice, il secondo esempio sarà molto più chiaro in termini di scopo e funzione.

Ho ragione nel dire che ciò sconfiggerebbe completamente lo scopo di avere getter e setter in primo luogo e che la validazione e altre logiche (senza strani effetti collaterali ovviamente) dovrebbero essere consentite?

Avere getter e setter non riguarda l'incapsulamento per motivi di "purezza", ma l'incapsulamento per consentire al codice di essere facilmente riformulato senza rischiare una modifica all'interfaccia della classe che altrimenti spezzerebbe la compatibilità della classe con il codice chiamante. La convalida è del tutto appropriata in un setter, tuttavia c'è un piccolo rischio è che una modifica alla convalida potrebbe interrompere la compatibilità con il codice chiamante se il codice chiamante si basa sulla convalida che si verifica in un modo particolare. Questa è una situazione generalmente rara e relativamente a basso rischio, ma va notato per completezza.

Quando dovrebbe avvenire la convalida?

La convalida dovrebbe avvenire nel contesto del setter prima di impostare effettivamente il valore. Questo assicura che se viene generata un'eccezione, lo stato dell'oggetto non cambierà e potenzialmente invaliderà i suoi dati. In genere trovo migliore delegare la convalida a un metodo separato che sarebbe la prima cosa chiamata all'interno del setter, al fine di mantenere il codice setter relativamente ordinato.

Un setter è autorizzato a modificare il valore (magari convertire un valore valido in una rappresentazione interna canonica)?

In casi molto rari, forse. In generale, probabilmente è meglio non farlo. Questo è il genere di cose che è meglio lasciare ad un altro metodo.


ri: modifica il valore, potrebbe essere ragionevole impostarlo su -1 o un token flag NULL se al setter viene passato un valore illegale
Martin Beckett,

1
Ci sono un paio di problemi con questo. L'impostazione arbitraria di un valore crea un effetto collaterale deliberato e poco chiaro. Inoltre, non consente al codice chiamante di ricevere feedback che potrebbero essere utilizzati per gestire meglio i dati illegali. Ciò è particolarmente importante con i valori a livello di interfaccia utente. Ad essere sinceri, un'eccezione a cui ho appena pensato potrebbe essere se consentire più formati di data come input, memorizzando la data in un formato data / ora standard. A un certo punto si potrebbe sostenere che questo particolare "effetto collaterale" è una normalizzazione dei dati al momento della convalida, a condizione che i dati di input siano legali.
S.Robins,

Sì, una delle giustificazioni di un setter è che dovrebbe restituire vero / falso se il valore potesse essere impostato.
Martin Beckett,

1
"Le proprietà in virgola mobile sono un buon esempio in cui potresti voler arrotondare cifre decimali eccessive per evitare di sollevare un'eccezione" In quale circostanza avere troppe cifre decimali genera un'eccezione?
Mark Byers,

2
Vedo cambiare il valore in una rappresentazione canonica come una forma di validazione. Valori mancanti predefiniti (ad esempio, la data corrente se un timestamp contiene solo l'ora) o ridimensionare un valore (.93275 -> 93,28%) per garantire che la coerenza interna sia corretta, ma qualsiasi manipolazione di questo tipo dovrebbe essere esplicitamente richiamata nella documentazione dell'API , soprattutto se sono un linguaggio insolito all'interno dell'API.
TMN,

20

Se il getter / setter rispecchia semplicemente il valore, allora non ha senso averli o renderli privati. Non c'è nulla di sbagliato nel rendere pubbliche alcune variabili membro se si ha una buona ragione. Se stai scrivendo una classe di punti 3d, avere pubblico .x, .y, .z ha perfettamente senso.

Come ha detto Ralph Waldo Emerson, "Una folle coerenza è il folletto delle piccole menti, adorato dai piccoli statisti e filosofi e dai progettisti di Java".

I getter / setter sono utili dove potrebbero esserci effetti collaterali, dove è necessario aggiornare altre variabili interne, ricalcolare i valori memorizzati nella cache e proteggere la classe da input non validi.

La solita giustificazione per loro, che nascondono la struttura interna, è generalmente la meno utile. per esempio. Ho questi punti memorizzati come 3 float, potrei decidere di memorizzarli come stringhe in un database remoto, quindi farò nasconder / setter per nasconderli, come se potessi farlo senza avere alcun altro effetto sul codice del chiamante.


1
Wow, i progettisti di Java sono divini? </jk>

@delnan - ovviamente no - o farebbero rima meglio ;-)
Martin Beckett il

Sarebbe valido creare un punto 3d in cui x, y, z sono tutti Float.NAN?
Andrew T Finnell,

4
Non sono d'accordo con il tuo punto che "la solita giustificazione" (nascondere la struttura interna) è la proprietà meno utile di getter e setter. In C #, è sicuramente utile rendere la proprietà un'interfaccia la cui struttura sottostante può essere cambiata - per esempio, se vuoi solo una proprietà disponibile per l'enumerazione, è molto meglio dire IEnumerable<T>che forzarla a qualcosa di simile List<T>. Il tuo esempio di accesso al database direi che sta violando la singola responsabilità - mescolando la rappresentazione del modello con il modo in cui è persistito.
Brandon Linton,

@AndrewFinnell - potrebbe essere un buon modo di contrassegnare i punti come impossibili / non validi / cancellati ecc.
Martin Beckett,

8

Principio di accesso uniforme di Meyer: "Tutti i servizi offerti da un modulo dovrebbero essere disponibili attraverso una notazione uniforme, che non tradisce se sono implementati attraverso l'archiviazione o il calcolo." è il motivo principale dietro i getter / setter, ovvero Proprietà.

Se decidi di memorizzare nella cache o di calcolare pigramente un campo di una classe, puoi modificarlo in qualsiasi momento se hai esposto solo gli accessi alle proprietà e non i dati concreti.

Oggetti di valore, strutture semplici non hanno bisogno di questa astrazione, ma secondo me una classe a tutti gli effetti.


2

Una strategia comune per la progettazione di classi, introdotta attraverso il linguaggio Eiffel, è la separazione comandi-query . L'idea è che un metodo dovrebbe neanche dire qualcosa circa l'oggetto o dire l'oggetto di fare qualcosa, ma non per fare entrambe le cose.

Ciò riguarda solo l'interfaccia pubblica della classe, non la rappresentazione interna. Considerare un oggetto modello dati supportato da una riga nel database. È possibile creare l'oggetto senza caricare i dati, quindi la prima volta che si chiama un getter esegue effettivamente il SELECT. Va bene, potresti cambiare alcuni dettagli interni su come l'oggetto è rappresentato ma non stai cambiando il modo in cui appare ai client di quell'oggetto. Dovresti essere in grado di chiamare i getter più volte e ottenere comunque gli stessi risultati, anche se fanno un lavoro diverso per restituire quei risultati.

Allo stesso modo un setter sembra, contrattualmente, come se cambiasse semplicemente lo stato di un oggetto. Potrebbe farlo in qualche modo contorto: scrivendo un UPDATEin un database o passando il parametro su un oggetto interno. Va bene, ma fare qualcosa di estraneo allo stabilire lo stato sarebbe sorprendente.

Meyer (il creatore di Eiffel) aveva anche cose da dire sulla convalida. In sostanza, ogni volta che un oggetto è in pausa dovrebbe essere in uno stato valido. Quindi, subito dopo che il costruttore ha terminato, prima e dopo (ma non necessariamente durante) ogni chiamata di metodo esterna, lo stato dell'oggetto dovrebbe essere coerente.

È interessante notare che in quella lingua, la sintassi per chiamare una procedura e per leggere un attributo esposto sembrano entrambe uguali. In altre parole, un chiamante non può dire se sta usando un metodo o lavora direttamente con una variabile di istanza. È solo nelle lingue che non nascondono questo dettaglio dell'implementazione che sorge anche la domanda: se i chiamanti non potevano dirlo, si potrebbe passare da un ivar pubblico agli accessor senza perdere la modifica del codice client.


1

Un'altra cosa accettabile da fare è la clonazione. In alcuni casi, è necessario assicurarsi che, dopo che qualcuno dà la tua classe ad es. un elenco di qualcosa, non può cambiarlo all'interno della tua classe (né cambiare l'oggetto in quell'elenco). Pertanto si esegue una copia approfondita del parametro nel setter e si restituisce la copia profonda in getter. (L'utilizzo di tipi immutabili come parametri è un'altra opzione, ma sopra presuppone che non sia possibile) Ma non clonare in accessori se non è necessario. È facile pensare (ma non in modo appropriato) a proprietà / getter e setter come operazioni a costo costante, quindi questa è una mina delle prestazioni che attende l'utente di api.


1

Non esiste sempre una mappatura 1-1 tra gli accessori di proprietà e gli ivar che memorizzano i dati.

Ad esempio, una classe di vista potrebbe fornire una centerproprietà anche se non esiste un ivar che memorizza il centro della vista; L'impostazione centerprovoca modifiche ad altri ivar, come origino transformqualsiasi altra cosa, ma i clienti della classe non sanno o si preoccupano di come center vengono memorizzati a condizione che funzioni correttamente. Ciò che non dovrebbe accadere, tuttavia, è che l'impostazione centerfa sì che le cose accadano al di là di tutto ciò che è necessario per salvare il nuovo valore, comunque sia fatto.


0

La parte migliore di setter e getter è che rendono facile modificare le regole di un'API senza cambiare l'API. Se rilevi un bug, è molto più probabile che tu possa correggere il bug nella libreria e non far aggiornare ad ogni consumatore la sua base di codice.


-4

Tendo a credere che setter e getter siano malvagi e dovrebbero essere usati solo in classi gestite da un framework / container. Un design di classe adeguato non dovrebbe produrre getter e setter.

Modifica: un articolo ben scritto su questo argomento .

Edit2: i campi pubblici sono un'assurdità in un approccio OOP; dicendo che getter e setter sono cattivi non intendo che dovrebbero essere sostituiti da campi pubblici.


1
Penso che ti sfugga il punto dell'articolo, che suggerisce di usare parser / setter con parsimonia e di evitare di esporre i dati di classe se non necessario. Questo IMHO è un principio di progettazione ragionevole che dovrebbe essere applicato deliberatamente dallo sviluppatore. In termini di creazione di proprietà su una classe, è possibile semplicemente esporre una variabile, ma ciò rende più difficile fornire la convalida quando la variabile è impostata o trarre il valore da una fonte alternativa quando "ottenuto", essenzialmente violando l'incapsulamento e potenzialmente bloccandoti in un design di interfaccia difficile da mantenere.
S.Robins,

@ S.Robins A mio avviso, getter e setter pubblici hanno torto; i campi pubblici sono più sbagliati (se questo è comparabile).
m3th0dman,

@methodman Sì, sono d'accordo che un campo pubblico è sbagliato, tuttavia una proprietà pubblica può essere utile. Tale proprietà può essere utilizzata per fornire un luogo per la convalida o eventi relativi all'impostazione o alla restituzione dei dati, a seconda del requisito specifico in quel momento. Getter e setter stessi non sono di per sé sbagliati. D'altra parte, come e quando vengono usati o abusati possono essere visti come scarsi in termini di design e manutenibilità a seconda delle circostanze. :)
S.Robins,

@ S.Robins Pensa alle basi di OOP e modellazione; gli oggetti dovrebbero copiare entità della vita reale. Ci sono entità nella vita reale che hanno questo tipo di operazioni / proprietà come getter / setter?
m3th0dman,

Per evitare di avere troppi commenti qui, porterò questa conversazione in questa chat e indirizzerò lì il tuo commento.
S.Robins,
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.