Perché i metodi getter e setter sono malvagi

Non avevo intenzione di iniziare una serie "is evil", ma diversi lettori mi hanno chiesto di spiegare perché ho detto che dovresti evitare i metodi get / set nella colonna del mese scorso, "Why extends Is Evil".

Sebbene i metodi getter / setter siano comuni in Java, non sono particolarmente orientati agli oggetti (OO). In effetti, possono danneggiare la manutenibilità del codice. Inoltre, la presenza di numerosi metodi getter e setter è una bandiera rossa che il programma non è necessariamente ben progettato da una prospettiva OO.

Questo articolo spiega perché non dovresti usare getter e setter (e quando puoi usarli) e suggerisce una metodologia di progettazione che ti aiuterà a uscire dalla mentalità getter / setter.

Sulla natura del design

Prima di lanciarmi in un'altra colonna relativa al design (con un titolo provocatorio, nientemeno), voglio chiarire alcune cose.

Sono rimasto sbalordito da alcuni commenti dei lettori che sono scaturiti dalla colonna del mese scorso, "Why extends Is Evil" (vedi Talkback nell'ultima pagina dell'articolo). Alcune persone credevano che avessi sostenuto che l'orientamento agli oggetti è negativo semplicemente perché extendsha problemi, come se i due concetti fossero equivalenti. Non è certamente quello che pensavo di aver detto, quindi lasciatemi chiarire alcuni meta-problemi.

Questa colonna e l'articolo del mese scorso riguardano il design. Il design, per sua natura, è una serie di compromessi. Ogni scelta ha un lato positivo e uno negativo e tu fai la tua scelta nel contesto di criteri generali definiti dalla necessità. Bene e male non sono assoluti, comunque. Una buona decisione in un contesto potrebbe essere cattiva in un altro.

Se non capisci entrambi i lati di un problema, non puoi fare una scelta intelligente; infatti, se non capisci tutte le ramificazioni delle tue azioni, non stai progettando affatto. Stai inciampando nel buio. Non è un caso che ogni capitolo del libro Design Patterns di Gang of Four includa una sezione "Conseguenze" che descrive quando e perché usare un pattern è inappropriato.

Affermare che alcune funzionalità del linguaggio o idiomi di programmazione comuni (come le funzioni di accesso) hanno problemi non è la stessa cosa che dire che non dovresti mai usarli in nessuna circostanza. E solo perché una funzione o un linguaggio è comunemente usato non significa che dovresti usarlo. I programmatori disinformati scrivono molti programmi e il semplice fatto di essere impiegati da Sun Microsystems o Microsoft non migliora magicamente le capacità di programmazione o di progettazione di qualcuno. I pacchetti Java contengono un sacco di ottimo codice. Ma ci sono anche parti di quel codice che sono sicuro che gli autori sono imbarazzati ad ammettere di aver scritto.

Allo stesso modo, il marketing o gli incentivi politici spesso spingono gli idiomi del design. A volte i programmatori prendono decisioni sbagliate, ma le aziende vogliono promuovere ciò che la tecnologia può fare, quindi de-enfatizzano che il modo in cui lo fai è tutt'altro che ideale. Traggono il meglio da una brutta situazione. Di conseguenza, agisci in modo irresponsabile quando adotti una pratica di programmazione semplicemente perché "è così che dovresti fare le cose". Molti progetti EJB (Enterprise JavaBeans) falliti dimostrano questo principio. La tecnologia basata su EJB è un'ottima tecnologia se utilizzata in modo appropriato, ma può letteralmente far crollare un'azienda se utilizzata in modo inappropriato.

Il punto è che non dovresti programmare alla cieca. Devi capire il caos che una caratteristica o un linguaggio può provocare. In tal modo, sei in una posizione molto migliore per decidere se utilizzare quella funzione o idioma. Le tue scelte dovrebbero essere sia informate che pragmatiche. Lo scopo di questi articoli è di aiutarti ad affrontare la tua programmazione con gli occhi aperti.

Astrazione dei dati

Un precetto fondamentale dei sistemi OO è che un oggetto non deve esporre nessuno dei suoi dettagli di implementazione. In questo modo è possibile modificare l'implementazione senza modificare il codice che utilizza l'oggetto. Ne consegue quindi che nei sistemi OO dovresti evitare le funzioni getter e setter poiché forniscono principalmente l'accesso ai dettagli di implementazione.

Per capire perché, si consideri che potrebbero esserci 1.000 chiamate a un getX()metodo nel programma e ogni chiamata presuppone che il valore restituito sia di un tipo particolare. È possibile memorizzare getX()il valore di ritorno di in una variabile locale, ad esempio, e quel tipo di variabile deve corrispondere al tipo di valore di ritorno. Se hai bisogno di cambiare il modo in cui l'oggetto è implementato in modo tale che il tipo di X cambi, sei in grossi guai.

Se X era un int, ma ora deve essere a long, otterrai 1.000 errori di compilazione. Se risolvi in ​​modo errato il problema eseguendo il cast del valore restituito a int, il codice verrà compilato in modo pulito, ma non funzionerà. (Il valore restituito potrebbe essere troncato.) È necessario modificare il codice che circonda ciascuna di queste 1.000 chiamate per compensare la modifica. Di certo non voglio fare così tanto lavoro.

Un principio di base dei sistemi OO è l'astrazione dei dati . È necessario nascondere completamente il modo in cui un oggetto implementa un gestore di messaggi dal resto del programma. Questo è uno dei motivi per cui dovrebbero esserlo tutte le variabili di istanza (i campi non costanti di una classe) private.

Se crei una variabile di istanza public, non puoi modificare il campo man mano che la classe si evolve nel tempo perché interromperesti il ​​codice esterno che utilizza il campo. Non vuoi cercare 1000 usi di una classe semplicemente perché cambi quella classe.

Questo principio che nasconde l'implementazione porta a un buon test della qualità di un sistema OO: puoi apportare enormi modifiche alla definizione di una classe, persino buttare via l'intera cosa e sostituirla con un'implementazione completamente diversa, senza influire sul codice che la utilizza oggetti della classe? Questo tipo di modularizzazione è la premessa centrale dell'orientamento agli oggetti e semplifica notevolmente la manutenzione. Senza nascondere l'implementazione, non ha molto senso usare altre funzionalità OO.

I metodi getter e setter (noti anche come accessors) sono pericolosi per lo stesso motivo per cui i publiccampi sono pericolosi: forniscono accesso esterno ai dettagli di implementazione. Cosa succede se è necessario modificare il tipo di campo a cui si accede? È inoltre necessario modificare il tipo di ritorno della funzione di accesso. Questo valore restituito viene utilizzato in numerose posizioni, quindi è necessario modificare anche tutto il codice. Voglio limitare gli effetti di una modifica a una singola definizione di classe. Non voglio che si diffondano nell'intero programma.

Poiché le funzioni di accesso violano il principio di incapsulamento, si può ragionevolmente sostenere che un sistema che utilizza in modo pesante o inappropriato le funzioni di accesso semplicemente non è orientato agli oggetti. Se segui un processo di progettazione, anziché solo codifica, non troverai quasi nessun accessorio nel tuo programma. Il processo è importante. Ho altro da dire su questo argomento alla fine dell'articolo.

La mancanza di metodi getter / setter non significa che alcuni dati non fluiscano attraverso il sistema. Tuttavia, è meglio ridurre al minimo il più possibile lo spostamento dei dati. La mia esperienza è che la manutenibilità è inversamente proporzionale alla quantità di dati che si spostano tra gli oggetti. Anche se potresti non vedere ancora come, puoi effettivamente eliminare la maggior parte di questo movimento di dati.

Progettando attentamente e concentrandoti su ciò che devi fare piuttosto che su come lo farai, elimini la stragrande maggioranza dei metodi getter / setter nel tuo programma. Non chiedere le informazioni necessarie per svolgere il lavoro; chiedi all'oggetto che ha le informazioni per fare il lavoro per te.La maggior parte delle funzioni di accesso trova la sua strada nel codice perché i progettisti non stavano pensando al modello dinamico: agli oggetti runtime e ai messaggi che si inviano l'un l'altro per fare il lavoro. Iniziano (in modo errato) progettando una gerarchia di classi e quindi cercano di inserire quelle classi nel modello dinamico. Questo approccio non funziona mai. Per creare un modello statico, è necessario scoprire le relazioni tra le classi e queste relazioni corrispondono esattamente al flusso di messaggi. Esiste un'associazione tra due classi solo quando gli oggetti di una classe inviano messaggi agli oggetti dell'altra. Lo scopo principale del modello statico è acquisire queste informazioni sull'associazione durante la modellazione dinamica.

Senza un modello dinamico chiaramente definito, stai solo indovinando come utilizzerai gli oggetti di una classe. Di conseguenza, i metodi di accesso spesso finiscono nel modello perché devi fornire il maggior accesso possibile poiché non puoi prevedere se ne avrai bisogno o meno. Questo tipo di strategia di progettazione indovinando è inefficiente nella migliore delle ipotesi. Perdi tempo a scrivere metodi inutili (o ad aggiungere capacità non necessarie alle classi).

Anche gli accessori finiscono nei design per forza dell'abitudine. Quando i programmatori procedurali adottano Java, tendono a iniziare creando un codice familiare. I linguaggi procedurali non hanno classi, ma hanno il C struct(pensa: classe senza metodi). Sembra naturale, quindi, imitare a structcostruendo definizioni di classe praticamente senza metodi e nient'altro che publiccampi. Questi programmatori procedurali leggono da qualche parte che i campi dovrebbero essere private, tuttavia, quindi creano i campi privatee forniscono publicmetodi di accesso. Ma hanno solo complicato l'accesso del pubblico. Certamente non hanno reso il sistema orientato agli oggetti.

Disegna te stesso

Una ramificazione dell'incapsulamento dell'intero campo è nella costruzione dell'interfaccia utente (UI). Se non puoi usare le funzioni di accesso, non puoi fare in modo che una classe del generatore dell'interfaccia utente chiami un getAttribute()metodo. Invece, le classi hanno elementi come drawYourself(...)metodi.

Un getIdentity()metodo può anche funzionare, ovviamente, a condizione che restituisca un oggetto che implementa l' Identityinterfaccia. Questa interfaccia deve includere un metodo drawYourself()(o dammi-un- JComponentche-rappresenta-la-tua-identità). Sebbene getIdentityinizi con "get", non è una funzione di accesso perché non restituisce solo un campo. Restituisce un oggetto complesso che ha un comportamento ragionevole. Anche quando ho un Identityoggetto, non ho ancora idea di come un'identità sia rappresentata internamente.

Ovviamente, una drawYourself()strategia significa che io (senza fiato!) Metto il codice dell'interfaccia utente nella logica aziendale. Considera cosa succede quando i requisiti dell'interfaccia utente cambiano. Diciamo che voglio rappresentare l'attributo in un modo completamente diverso. Oggi una "identità" è un nome; domani è un nome e un numero di identificazione; il giorno dopo è un nome, un numero di identificazione e un'immagine. Limito l'ambito di queste modifiche a un punto del codice. Se ho una JComponentclasse dammi- che-rappresenta-la-tua-identità, allora ho isolato il modo in cui le identità sono rappresentate dal resto del sistema.

Tieni presente che in realtà non ho inserito alcun codice dell'interfaccia utente nella logica aziendale. Ho scritto il livello dell'interfaccia utente in termini di AWT (Abstract Window Toolkit) o ​​Swing, che sono entrambi livelli di astrazione. Il codice dell'interfaccia utente effettivo è nell'implementazione AWT / Swing. Questo è il punto centrale di un livello di astrazione: isolare la logica di business dalla meccanica di un sottosistema. Posso facilmente portare in un altro ambiente grafico senza modificare il codice, quindi l'unico problema è un po 'di confusione. È possibile eliminare facilmente questo disordine spostando tutto il codice dell'interfaccia utente in una classe interna (o utilizzando il modello di progettazione Façade).

JavaBeans

Potreste obiettare dicendo: "Ma per quanto riguarda JavaBeans?" Che ne pensi di loro? Puoi certamente costruire JavaBeans senza getter e setter. Il BeanCustomizer, BeanInfoe BeanDescriptortutte le classi esistono proprio per questo scopo. I progettisti delle specifiche JavaBean hanno messo in scena l'idioma getter / setter perché pensavano che sarebbe stato un modo semplice per creare rapidamente un bean, qualcosa che puoi fare mentre impari a farlo bene. Sfortunatamente, nessuno l'ha fatto.

Le funzioni di accesso sono state create esclusivamente come un modo per contrassegnare determinate proprietà in modo che un programma di creazione di interfacce utente o equivalente possa identificarle. Non dovresti chiamare questi metodi da solo. Esistono per uno strumento automatizzato da utilizzare. Questo strumento utilizza le API di introspezione nella Classclasse per trovare i metodi ed estrapolare l'esistenza di determinate proprietà dai nomi dei metodi. In pratica, questo idioma basato sull'introspezione non ha funzionato. Ha reso il codice decisamente troppo complicato e procedurale. I programmatori che non comprendono l'astrazione dei dati chiamano effettivamente le funzioni di accesso e, di conseguenza, il codice è meno gestibile. Per questo motivo, una funzionalità di metadati verrà incorporata in Java 1.5 (prevista per metà 2004). Quindi invece di:

proprietà privata int; public int getProperty () {proprietà di ritorno; } public void setProperty (int value} {property = value;}

Sarai in grado di usare qualcosa come:

proprietà privata @property int; 

Lo strumento di costruzione dell'interfaccia utente o equivalente utilizzerà le API di introspezione per trovare le proprietà, anziché esaminare i nomi dei metodi e dedurre l'esistenza di una proprietà da un nome. Pertanto, nessuna funzione di accesso runtime danneggia il codice.

Quando va bene un accessorio?

Innanzitutto, come ho discusso in precedenza, va bene che un metodo restituisca un oggetto in termini di un'interfaccia che l'oggetto implementa perché quell'interfaccia ti isola dalle modifiche alla classe di implementazione. Questo tipo di metodo (che restituisce un riferimento all'interfaccia) non è realmente un "getter" nel senso di un metodo che fornisce solo l'accesso a un campo. Se si modifica l'implementazione interna del provider, è sufficiente modificare la definizione dell'oggetto restituito per adattarsi alle modifiche. Proteggi comunque il codice esterno che utilizza l'oggetto tramite la sua interfaccia.