Un getter dovrebbe lanciare un'eccezione se il suo oggetto ha uno stato non valido?


55

Mi capita spesso di imbattermi in questo problema, soprattutto in Java, anche se penso che sia un problema OOP generale. Cioè: sollevare un'eccezione rivela un problema di progettazione.

Supponiamo che io abbia una classe che ha un String namecampo e un String surnamecampo.

Quindi utilizza quei campi per comporre il nome completo di una persona al fine di visualizzarlo su una sorta di documento, ad esempio una fattura.

public void String name;
public void String surname;

public String getCompleteName() {return name + " " + surname;}

public void displayCompleteNameOnInvoice() {
    String completeName = getCompleteName();
    //do something with it....
}

Ora voglio rafforzare il comportamento della mia classe lanciando un errore se displayCompleteNameOnInvoiceviene chiamato prima che il nome sia stato assegnato. Sembra una buona idea, vero?

Posso aggiungere un getCompleteNamemetodo per aumentare le eccezioni al metodo. Ma in questo modo sto violando un contratto "implicito" con l'utente della classe; in generale i getter non dovrebbero generare eccezioni se i loro valori non sono impostati. Ok, questo non è un getter standard poiché non restituisce un singolo campo, ma dal punto di vista dell'utente la distinzione potrebbe essere troppo sottile per pensarci.

Oppure posso generare l'eccezione dall'interno di displayCompleteNameOnInvoice. Ma per fare ciò dovrei testare direttamente nameo surnamecampi e così facendo violerei l'astrazione rappresentata da getCompleteName. È responsabilità di questo metodo verificare e creare il nome completo. Potrebbe persino decidere, basando la decisione su altri dati, che in alcuni casi è sufficiente il surname.

Quindi l'unica possibilità sembra cambiare la semantica del metodo getCompleteNamea composeCompleteName, che suggerisce un comportamento più 'attivo' e, con essa, la capacità di un'eccezione.

È questa la migliore soluzione di design? Sono sempre alla ricerca del miglior equilibrio tra semplicità e correttezza. Esiste un riferimento di progettazione per questo problema?


8
"in generale i getter non dovrebbero generare eccezioni se i loro valori non sono impostati." - Cosa dovrebbero fare allora?
Rotem,

2
@scriptin Quindi, seguendo questa logica, displayCompleteNameOnInvoicepuò semplicemente generare un'eccezione se getCompleteNameritorna null, no?
Rotem,

2
@Rotem can can. La domanda è se dovrebbe.
scriptin,

22
In materia, i programmatori di Falsehoods Believe About Names sono un'ottima lettura del perché "nome" e "cognome" sono concetti molto ristretti.
Lars Viklund,

9
Se il tuo oggetto può avere uno stato non valido, hai già rovinato tutto.
user253751

Risposte:


151

Non consentire la costruzione della tua classe senza assegnare un nome.


9
È una soluzione interessante ma piuttosto radicale. Molte volte le tue classi devono essere più fluide, tenendo conto degli stati che diventano un problema solo se e quando vengono chiamati determinati metodi, altrimenti finirai con un proliferare di piccole classi che richiedono troppe spiegazioni per essere usate.
AgostinoX,

71
Questo non è un commento. Se applica il consiglio nella risposta, non avrà più il problema descritto. È una risposta
DeadMG

14
@AgostinoX: dovresti rendere fluide le tue lezioni solo se assolutamente necessario. Altrimenti, dovrebbero essere il più invarianti possibile.
DeadMG

25
@gnat: fai un ottimo lavoro qui mettendo in discussione la qualità di ogni domanda e ogni risposta su PSE, ma a volte sei IMHO un po 'troppo nitido.
Doc Brown,

9
Se possono essere validamente opzionali, non c'è motivo per lui di lanciare in primo luogo.
DeadMG,

75

La risposta fornita da DeadMG in qualche modo lo inchioda, ma lasciatemi esprimere la frase in modo leggermente diverso: Evitare di avere oggetti con stato non valido. Un oggetto dovrebbe essere "intero" nel contesto del compito che svolge. O non dovrebbe esistere.

Quindi, se vuoi fluidità, usa qualcosa come il Builder Pattern . O qualsiasi altra cosa che sia una reificazione separata di un oggetto in costruzione in contrapposizione alla "cosa reale", in cui è garantito che quest'ultimo abbia uno stato valido ed è quello che espone effettivamente le operazioni definite su quello stato (es getCompleteName.).


7
So if you want fluidity, use something like the builder pattern- fluidità o immutabilità (a meno che non sia quello che intendi in qualche modo). Se si desidera creare oggetti immutabili, ma è necessario posticipare l'impostazione di alcuni valori, lo schema da seguire è di impostarli uno a uno su un oggetto builder e crearne uno immutabile solo dopo aver raccolto tutti i valori richiesti per il corretto stato ...
Konrad Morawski,

1
@KonradMorawski: sono confuso. Stai chiarendo o contraddicendo la mia affermazione? ;)
back2dos,

3
Ho provato a completarlo ...
Konrad Morawski,

40

Non hai un oggetto con uno stato non valido.

Lo scopo di un costruttore o costruttore è di impostare lo stato dell'oggetto su qualcosa di coerente e utilizzabile. Senza questa garanzia, i problemi sorgono con uno stato inizializzato in modo errato negli oggetti. Questo problema si aggrava se hai a che fare con la concorrenza e qualcosa può accedere all'oggetto prima che tu abbia finito di configurarlo.

Quindi, la domanda per questa parte è "perché permetti a nome o cognome di essere nulli?" Se questo non è qualcosa che è valido nella classe per farlo funzionare correttamente, non permetterlo. Avere un costruttore o un costruttore che convalida correttamente la creazione dell'oggetto e, se non è corretto, sollevare il problema a quel punto. Puoi anche utilizzare una delle @NotNullannotazioni esistenti per aiutare a comunicare che "questo non può essere nullo" e applicarlo nella codifica e nell'analisi.

Con questa garanzia, diventa molto più facile ragionare sul codice, su ciò che fa e non dover generare eccezioni in luoghi strani o effettuare controlli eccessivi attorno alle funzioni getter.

Getter che fanno di più.

C'è un bel po 'fuori su questo argomento. Hai quanta logica in Getter e cosa dovrebbe essere consentito all'interno di getter e setter? da qui e getter e setter eseguono ulteriore logica su Stack Overflow. Questo è un problema che si presenta ancora e ancora nel design di classe.

Il nucleo di questo deriva da:

in generale i getter non dovrebbero generare eccezioni se i loro valori non sono impostati

E hai ragione. Non dovrebbero. Dovrebbero apparire "stupidi" al resto del mondo e fare ciò che ci si aspetta. Inserire troppa logica lì dentro porta a problemi in cui viene violata la legge del minimo stupore. E ammettiamolo, non vuoi davvero avvolgere i getter con un try catchperché sappiamo quanto amiamo farlo in generale.

Ci sono anche situazioni in cui è necessario utilizzare un getFoometodo come il framework JavaBeans e quando si ha qualcosa dalla chiamata EL che si aspetta di ottenere un bean (in modo che <% bar.foo %>effettivamente chiama getFoo()nella classe - mettendo da parte '' il bean dovrebbe fare la composizione o dovrebbe che deve essere lasciato alla vista? 'perché si possono facilmente trovare situazioni in cui l'una o l'altra può essere chiaramente la risposta giusta)

Comprendi anche che è possibile specificare un determinato getter in un'interfaccia o far parte dell'API pubblica precedentemente esposta per la classe che viene refactored (una versione precedente aveva appena "completeName" che veniva restituito e un refactoring si era rotto in due campi).

Alla fine del giorno...

Fai la cosa più facile da ragionare. Trascorrerai più tempo a mantenere questo codice di quanto impiegherai a progettarlo (anche se meno tempo impieghi a progettare, più tempo impiegherai a mantenere). La scelta ideale è progettarla in modo che non ci vorrà molto tempo per mantenerla, ma non stare lì a pensarci nemmeno per giorni - a meno che questa non sia davvero una scelta progettuale che avrà giorni di implicazioni in seguito .

Cercare di rimanere con la purezza semantica dell'essere getter private T foo; T getFoo() { return foo; }ti metterà nei guai ad un certo punto. A volte il codice non si adatta a quel modello e le contorsioni che si attraversano per cercare di mantenerlo in questo modo non hanno senso ... e alla fine rende più difficile la progettazione.

Accetta a volte che gli ideali del design non possano essere realizzati nel modo desiderato nel codice. Le linee guida sono linee guida, non catene.


2
Ovviamente il mio esempio era solo una scusa per migliorare lo stile di codifica ragionandoci sopra, non un problema di blocco. Ma sono abbastanza perplesso dall'importanza che tu e altri sembrate dare ai costruttori, in teoria mantengono la loro promessa. In pratica, diventano ingombranti da mantenere con l'eredità, non utilizzabili quando si estrae un'interfaccia da una classe, non adottata dai javabeani e da altri framework come la primavera se non sbaglio. Sotto l'ombrello di un'idea di 'classe' vivono molte diverse categorie di oggetti. Un oggetto valore può avere un costruttore di convalida, ma questo è solo un tipo.
AgostinoX,

2
Essere in grado di ragionare sul codice è la chiave per poter scrivere codice usando un'API o essere in grado di mantenerlo. Se una classe ha un costruttore che le consente di trovarsi in uno stato non valido, diventa molto più difficile pensare "bene, che cosa fa?" perché ora devi considerare più situazioni rispetto al progettista della classe considerata o prevista. Questo carico extra concettuale comporta la penalità di più bug e un tempo più lungo per scrivere il codice. D'altra parte, se puoi guardare il codice e dire "nome e cognome non saranno mai nulli", diventa molto più facile scrivere codice usando loro.

2
Incompleto non significa necessariamente non valido. Una fattura senza ricevuta può essere in corso e l'API pubblica che presenta rifletterà che tale stato è valido. Se surnameo nameessendo null è uno stato valido per l'oggetto, gestirlo di conseguenza. Ma se non è uno stato valido - fa sì che i metodi all'interno della classe generino eccezioni, dovrebbe essere impedito di trovarsi in quello stato dal punto in cui è stato inizializzato. È possibile passare intorno a oggetti incompleti, ma passare attorno a oggetti non validi è problematico. Se un cognome null è eccezionale, prova a prevenirlo prima o poi.

2
Un post di blog correlato da leggere: Progettazione di inizializzazione di oggetti e notare i quattro approcci per l'inizializzazione di una variabile di istanza. Uno di questi è consentire che si trovi in ​​uno stato non valido, sebbene si noti che genera un'eccezione. Se stai cercando di evitarlo, quell'approccio non è valido e dovrai lavorare con gli altri approcci, che prevedono la garanzia di un oggetto valido in ogni momento. Dal blog: "Gli oggetti che possono avere stati non validi sono più difficili da usare e più difficili da comprendere rispetto a quelli che sono sempre validi". e questa è la chiave.

5
"Fai la cosa più facile da ragionare." Quello
Ben

5

Non sono un ragazzo Java, ma questo sembra aderire a entrambi i vincoli che hai presentato.

getCompleteNamenon genera un'eccezione se i nomi non sono inizializzati e lo displayCompleteNameOnInvoicefa.

public String getCompleteName()
{
    if (name == null || surname == null)
    {
        return null;
    }
    return name + " " + surname;
}

public void displayCompleteNameOnInvoice() 
{
    String completeName = getCompleteName();
    if (completeName == null)
    {
        //throw an exception.
    }
    //do something with it....
}

Per espandere il motivo per cui questa è una soluzione corretta: in questo modo il chiamante getCompleteName()non deve preoccuparsi se la proprietà è effettivamente memorizzata o se viene calcolata al volo.
Bart van Ingen Schenau,

18
Per espandere il motivo per cui questa soluzione è pazza, devi controllare la nullità su ogni singola chiamata a getCompleteName, che è orribile.
DeadMG

No, @DeadMG, devi solo verificarlo nei casi in cui dovrebbe essere generata un'eccezione se getCompleteName restituisce null. Qual è il modo in cui dovrebbe essere. Questa soluzione è corretta
Dawood dice di ripristinare Monica il

1
@DeadMG Sì, ma non sappiamo quali altri usi ci siano getCompleteName()nel resto della classe, o anche in altre parti del programma. In alcuni casi, il ritorno nullpuò essere esattamente ciò che è richiesto. Ma mentre c'è UN metodo la cui specifica è "getta un'eccezione in questo caso", questo è ESATTAMENTE ciò che dovremmo codificare.
Dawood dice di ripristinare Monica il

4
Ci possono essere situazioni in cui questa è la soluzione migliore, ma non riesco a immaginarmene. Nella maggior parte dei casi, questo sembra eccessivamente complicato: un metodo genera dati incompleti, un altro restituisce null. Temo che questo possa essere usato in modo errato dai chiamanti.
sleske,

4

Sembra che nessuno stia rispondendo alla tua domanda.
A differenza di ciò a cui piace credere, "evitare oggetti non validi" non è spesso una soluzione pratica.

La risposta è:

Sì dovrebbe.

Esempio semplice: FileStream.Lengthin C # /. NET , che genera NotSupportedExceptionse il file non è ricercabile.


I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat .
maple_shaft

1

Hai l'intero nome del controllo a testa in giù.

getCompleteName()non deve sapere quali funzioni lo utilizzeranno. Pertanto, non avere una namechiamata quando displayCompleteNameOnInvoice()non è getCompleteName()una responsabilità.

Ma nameè un chiaro prerequisito per displayCompleteNameOnInvoice(). Pertanto, dovrebbe assumersi la responsabilità di controllare lo stato (o dovrebbe delegare la responsabilità del controllo a un altro metodo, se si preferisce).

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.