Altro su getter e setter

È un principio di 25 anni di progettazione orientata agli oggetti (OO) che non si dovrebbe esporre l'implementazione di un oggetto a nessun'altra classe nel programma. Il programma è inutilmente difficile da mantenere quando si espone l'implementazione, principalmente perché la modifica di un oggetto che espone la sua implementazione impone modifiche a tutte le classi che utilizzano l'oggetto.

Sfortunatamente, l'idioma getter / setter che molti programmatori pensano come orientato agli oggetti viola questo principio fondamentale dell'OO a picche. Considera l'esempio di una Moneyclasse che ha un getValue()metodo su di essa che restituisce il "valore" in dollari. Avrai codice come il seguente in tutto il tuo programma:

double orderTotal; Importo in denaro = ...; // ... orderTotal + = amount.getValue (); // orderTotal deve essere in dollari

Il problema con questo approccio è che il codice precedente fa un grande presupposto su come la Moneyclasse è implementata (che il "valore" è memorizzato in a double). Il codice che fa le ipotesi di implementazione si interrompe quando l'implementazione cambia. Se, ad esempio, devi internazionalizzare la tua applicazione per supportare valute diverse dal dollaro, getValue()non restituisce nulla di significativo. Potresti aggiungere un getCurrency(), ma ciò renderebbe tutto il codice che circonda la getValue()chiamata molto più complicato, specialmente se persisti nell'usare la strategia getter / setter per ottenere le informazioni necessarie per fare il lavoro. Un'implementazione tipica (imperfetta) potrebbe essere simile a questa:

Importo in denaro = ...; // ... value = amount.getValue (); valuta = importo.getCurrency (); conversione = CurrencyTable.getConversionFactor (valuta, USDOLLARS); totale + = valore * conversione; // ...

Questa modifica è troppo complicata per essere gestita dal refactoring automatico. Inoltre, dovresti apportare questo tipo di modifiche ovunque nel tuo codice.

La soluzione a livello di logica aziendale a questo problema è eseguire il lavoro nell'oggetto che dispone delle informazioni necessarie per eseguire il lavoro. Invece di estrarre il "valore" per eseguire alcune operazioni esterne su di esso, dovresti fare in modo che la Moneyclasse esegua tutte le operazioni relative al denaro, inclusa la conversione di valuta. Un oggetto adeguatamente strutturato gestirà il totale in questo modo:

Denaro totale = ...; Importo in denaro = ...; total.increaseBy (importo);

Il add()metodo calcola la valuta dell'operando, esegue qualsiasi conversione di valuta necessaria (che è, propriamente, un'operazione sul denaro ) e aggiorna il totale. Se hai usato questa strategia dell'oggetto che ha le informazioni che funzionano, la nozione di valuta potrebbe essere aggiunta alla Moneyclasse senza che siano richieste modifiche nel codice che utilizza gli Moneyoggetti. Cioè, il lavoro di refactoring di un dollaro in un'implementazione internazionale sarebbe concentrato in un unico luogo: la Moneyclasse.

Il problema

La maggior parte dei programmatori non ha difficoltà a cogliere questo concetto a livello di logica aziendale (sebbene possa essere necessario un certo sforzo per pensare in modo coerente in questo modo). I problemi iniziano a emergere, tuttavia, quando l'interfaccia utente (UI) entra in scena. Il problema non è che non puoi applicare tecniche come quella che ho appena descritto per costruire un'interfaccia utente, ma che molti programmatori sono bloccati in una mentalità getter / setter quando si tratta di interfacce utente. Do la colpa di questo problema a strumenti di costruzione di codice fondamentalmente procedurali come Visual Basic e i suoi cloni (inclusi i costruttori di interfacce utente Java) che ti costringono a questo modo di pensare procedurale, getter / setter.

(Digressione: alcuni di voi esiteranno alla dichiarazione precedente e urleranno che VB è basato sulla consacrata architettura Model-View-Controller (MVC), quindi è sacrosanto. Tenete presente che MVC è stato sviluppato quasi 30 anni fa. All'inizio Negli anni '70, il supercomputer più grande era alla pari con i desktop di oggi. La maggior parte delle macchine (come il DEC PDP-11) erano computer a 16 bit, con 64 KB di memoria e velocità di clock misurate in decine di megahertz. La tua interfaccia utente era probabilmente una pila di schede perforate. Se sei stato abbastanza fortunato da avere un terminale video, potresti aver utilizzato un sistema di input / output (I / O) di console basato su ASCII. Abbiamo imparato molto negli ultimi 30 anni. Java Swing ha dovuto sostituire MVC con un'architettura simile "separable-model", principalmente perché MVC puro non isola sufficientemente l'interfaccia utente e i livelli del modello di dominio.)

Quindi, definiamo il problema in poche parole:

Se un oggetto può non esporre le informazioni di implementazione (tramite i metodi get / set o con qualsiasi altro mezzo), è ovvio che un oggetto deve in qualche modo creare la propria interfaccia utente. Cioè, se il modo in cui sono rappresentati gli attributi di un oggetto è nascosto al resto del programma, non è possibile estrarre quegli attributi per costruire un'interfaccia utente.

Nota, a proposito, che non stai nascondendo il fatto che esiste un attributo. (Sto definendo l' attributo , qui, come una caratteristica essenziale dell'oggetto.) Sai che un Employeedeve avere un attributo stipendio o salario, altrimenti non sarebbe un Employee. (Sarebbe a Person, a Volunteer, a Vagrant, o qualcos'altro che non ha uno stipendio.) Quello che non sai - o vuoi sapere - è come lo stipendio è rappresentato all'interno dell'oggetto. Potrebbe essere un double, un String, un longdecimale in scala o con codice binario. Potrebbe essere un attributo "sintetico" o "derivato", che viene calcolato in fase di esecuzione (da un grado di retribuzione o da un titolo professionale, ad esempio, o recuperando il valore da un database). Sebbene un metodo get possa effettivamente nascondere alcuni di questi dettagli di implementazione,come abbiamo visto con ilMoney esempio, non può nascondersi abbastanza.

Quindi come fa un oggetto a produrre la propria interfaccia utente e rimanere gestibile? Solo gli oggetti più semplicistici possono supportare qualcosa come un displayYourself()metodo. Gli oggetti realistici devono:

  • Si visualizzano in diversi formati (XML, SQL, valori separati da virgole, ecc.).
  • Visualizza diverse visualizzazioni di se stesse (una visualizzazione potrebbe visualizzare tutti gli attributi; un'altra potrebbe visualizzare solo un sottoinsieme degli attributi; e una terza potrebbe presentare gli attributi in un modo diverso).
  • Vengono visualizzati in ambienti diversi (lato client ( JComponent) e servito al client (HTML), ad esempio) e gestiscono sia l'input che l'output in entrambi gli ambienti.

Alcuni dei lettori del mio precedente articolo getter / setter sono balzati alla conclusione che stavo sostenendo che si aggiungessero metodi all'oggetto per coprire tutte queste possibilità, ma quella "soluzione" è ovviamente priva di senso. Non solo l'oggetto pesante risultante è troppo complicato, dovrai modificarlo costantemente per gestire i nuovi requisiti dell'interfaccia utente. In pratica, un oggetto non può costruire da solo tutte le possibili interfacce utente, se non altro per molte di quelle UI non sono state nemmeno concepite quando la classe è stata creata.

Crea una soluzione

La soluzione di questo problema consiste nel separare il codice dell'interfaccia utente dall'oggetto aziendale principale inserendolo in una classe di oggetti separata. Cioè, dovresti suddividere alcune funzionalità che potrebbero essere nell'oggetto in un oggetto completamente separato.

Questa biforcazione dei metodi di un oggetto appare in diversi modelli di progettazione. Molto probabilmente hai familiarità con la strategia, che viene utilizzata con le varie java.awt.Containerclassi per eseguire il layout. Si potrebbe risolvere il problema di layout con una soluzione di derivazione: FlowLayoutPanel, GridLayoutPanel, BorderLayoutPanel, ecc, ma che i mandati troppe classi e un sacco di codice duplicato in quelle classi. Una singola soluzione di classe pesante (aggiunta di metodi a Containerlike layOutAsGrid(), layOutAsFlow()ecc.) Non è pratica perché non è possibile modificare il codice sorgente per il Containersemplice perché è necessario un layout non supportato. Nel modello di strategia, si crea Strategyun'interfaccia ( LayoutManager) implementata da diverse Concrete Strategyclassi ( FlowLayout, GridLayout, ecc). Quindi dici a un Contextoggetto (aContainer) come fare qualcosa facendogli passare un Strategyoggetto. (Si passa una Containera LayoutManagerche definisce una strategia di layout.)

Il modello Builder è simile a Strategy. La differenza principale è che la Builderclasse implementa una strategia per costruire qualcosa (come un JComponentflusso o XML che rappresenta lo stato di un oggetto). Buildergli oggetti in genere costruiscono i loro prodotti anche utilizzando un processo a più fasi. Cioè, le chiamate a vari metodi di Buildersono necessarie per completare il processo di costruzione e Buildertipicamente non conosce l'ordine in cui verranno effettuate le chiamate o il numero di volte che uno dei suoi metodi verrà chiamato. La caratteristica più importante del builder è che l'oggetto business (chiamato Context) non sa esattamente cosa Buildersta costruendo l' oggetto. Il modello isola l'oggetto business dalla sua rappresentazione.

Il modo migliore per vedere come funziona un semplice costruttore è guardarne uno. Per prima cosa diamo un'occhiata a Context, l'oggetto business che deve esporre un'interfaccia utente. Il listato 1 mostra una Employeeclasse semplicistica . L' Employeeha name, ide salarygli attributi. (Gli stub per queste classi sono in fondo alla lista, ma questi stub sono solo segnaposto per la cosa reale. Puoi, spero, immaginare facilmente come funzionerebbero queste classi.)

Questo particolare Contextutilizza quello che penso come un costruttore bidirezionale. Il classico Gang of Four Builder va in una direzione (output), ma ho anche aggiunto Builderun Employeeoggetto che può utilizzare per inizializzarsi. Sono Buildernecessarie due interfacce. L' Employee.Exporterinterfaccia (Listato 1, riga 8) gestisce la direzione dell'output. Definisce un'interfaccia a un Builderoggetto che costruisce la rappresentazione dell'oggetto corrente. I Employeedelegati la costruzione UI reale al Buildernel export()metodo (sulla linea 31). Non Buildervengono passati i campi effettivi, ma utilizza invece Strings per passare una rappresentazione di quei campi.

Listato 1. Employee: The Builder Context

1 import java.util.Locale; 2 3 public class Employee 4 {private Name name; 5 ID EmployeeId privato; 6 stipendio privato in denaro; 7 8 interfaccia pubblica Esportatore 9 {void addName (String name); 10 void addID (String id); 11 void addSalary (String salary); 12} 13 14 interfaccia pubblica Importatore 15 {String provideName (); 16 String provideID (); 17 String provideSalary (); 18 void open (); 19 void close (); 20} 21 22 public Employee (importatore costruttore) 23 {builder.open (); 24 this.name = new Name (builder.provideName ()); 25 this.id = new EmployeeId (builder.provideID ()); 26 this.salary = new Money (builder.provideSalary (), 27 new Locale ("en", "US")); 28 builder.close (); 29} 30 31 public void export (Exporter builder) 32 {builder.addName (name.toString ()); 33 builder.addID (id.toString ()); 34 builder.addSalary (stipendio.accordare() ); 35} 36 37// ... 38} 39 // ---------------------------------------- ------------------------------ 40 // Roba da test unitario 41 // 42 nome classe 43 {valore stringa privata; 44 nome pubblico (valore stringa) 45 {this.value = value; 46} 47 public String toString () {valore di ritorno; }; 48} 49 50 class EmployeeId 51 {private String value; 52 public EmployeeId (String value) 53 {this.value = value; 54} 55 public String toString () {valore di ritorno; } 56} 57 58 class Money 59 {private String value; 60 public Money (String value, Locale location) 61 {this.value = value; 62} 63 public String toString () {valore di ritorno; } 64}

Diamo un'occhiata a un esempio. Il codice seguente crea l'interfaccia utente della figura 1: The following code builds Figure 1's UI:

Dipendente wilma = ...; JComponentExporter uiBuilder = nuovo JComponentExporter (); // Crea il builder wilma.export (uiBuilder); // Crea l'interfaccia utente JComponent userInterface = uiBuilder.getJComponent (); // ... someContainer.add (userInterface);

Il listato 2 mostra la sorgente per il JComponentExporter. Come puoi vedere, tutto il codice relativo all'interfaccia utente è concentrato nel Concrete Builder(the JComponentExporter) e il Context(the Employee) guida il processo di compilazione senza sapere esattamente cosa sta costruendo.

Listato 2. Esportazione in un'interfaccia utente lato client

1 importa javax.swing. *; 2 importare java.awt. *; 3 importare java.awt.event. *; 4 5 classe JComponentExporter implementa Employee.Exporter 6 {private String name, id, salary; 7 8 public void addName (String name) {this.name = name; } 9 public void addID (String id) {this.id = id; } 10 public void addSalary (String salary) {this.salary = salary; } 11 12 JComponent getJComponent () 13 {JComponent panel = new JPanel (); 14 panel.setLayout (new GridLayout (3,2)); 15 panel.add (new JLabel ("Name:")); 16 panel.add (nuovo JLabel (nome)); 17 panel.add (nuovo JLabel ("ID dipendente:")); 18 panel.add (nuovo JLabel (id)); 19 panel.add (new JLabel ("Salary:")); 20 panel.add (nuovo JLabel (stipendio)); 21 pannello di ritorno; 22} 23}