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.