Gestione di riferimenti null nella logica C #

2

Supponiamo che sia possibile utilizzare un metodo API per calcolare la somma di tutti gli ordini effettuati da un cliente specifico:

Amount CalculateOrderSum(int customerId)
{
    // Perform authentication to make sure caller has access to customerId

    // Retrieve customer with id customerId

    // Retrieve all orders related to the customer

    // Retrieve details for different orders (not always, depending on state)
}

In qualsiasi punto di questa funzione, alcuni amministratori di sistema possono eliminare vecchi elementi dal sistema. Ciò significa che uno dei seguenti può accadere mentre il metodo sopra è in esecuzione:

  • L'autenticazione fallirà perché il cliente non esiste più
  • Il cliente non può essere caricato, poiché è stato eliminato
  • Il cliente può essere caricato, ma dopo un millesimo di secondo gli ordini vengono cancellati e non possono essere recuperati.
  • Il recupero dei dettagli dell'ordine funziona per alcuni ordini, ma non ha esito positivo per gli altri poiché sono stati eliminati durante l'elaborazione intermedia.

Desidero che l'applicazione restituisca un messaggio di errore amichevole quando si verifica uno di questi eventi, piuttosto che restituire una NullReferenceException o simile.

Come vedo, ci sono alcuni approcci diversi per aggiungere la gestione degli errori per questa logica:

  1. Potrei introdurre molti controlli null in tutto il codice, ad esempio: if (customer! = null) lancia OrderRetrievalFailedException ("customer is a goner."). Dal momento che tutti i dati nel database possono essere eliminati in qualsiasi momento, ciò comporterebbe un notevole dispendio di se stessi in tutto il codice (che sembra essere disordinato)
  2. È possibile modificare la funzionalità di eliminazione per contrassegnare i clienti o gli ordini come eliminati (anziché rimuovere effettivamente le righe del database). In questo modo, la funzione potrebbe ancora svolgere il proprio lavoro perché i dati saranno ancora lì. Il problema qui è che in realtà vogliamo eliminare i vecchi dati per diversi motivi (ad esempio, considerazioni meno sulla superficie di attacco e sulle prestazioni).
  3. Potrei cambiare tutti i metodi da lanciare se non è possibile caricare un oggetto (così GetOrders (customerId) potrebbe lanciare CustomerNotFoundException se il cliente non può essere caricato) che verrebbe catturato nella funzione CalculateOrderSum e un errore dato all'utente. Quindi in pratica il codice dovrebbe essere disseminato di if (qualcosa == null) per lanciare una nuova SomeException.
  4. Potrei introdurre un meccanismo di blocco globale, in modo che un cliente non possa essere cancellato mentre qualcuno sta leggendo uno qualsiasi dei suoi dati. Il problema qui è che il nostro sistema è distribuito, quindi avremmo bisogno di implementare un meccanismo di blocco centrale. Inoltre, ho una brutta esperienza con il blocco delle righe del database in casi d'uso come questo nel database ad alto traffico.

Tutti questi approcci sono abbastanza complicati e difficili da ottenere per me, e ciò significa che il "flusso principale di successo" del codice sarà pieno di scenari di eccezione. Mi sto orientando verso l'alternativa 3, ma vorrei sapere se c'è un altro modo "standard e robusto" di gestirlo.

(Sto usando C #, ma presumo che lo stesso problema si applichi agli utenti di Java o C ++ ad esempio)

    
posta Martin 26.01.2016 - 15:51
fonte

5 risposte

2

Piuttosto che il codice C # esegue tutto questo calcolo e quindi devono gestire tutte le condizioni di errore che descrivi, perché non spostare tutto su una stored procedure che è transazionale? In questo modo, puoi bloccare il DB, ad esempio, per assicurarti che non cambi durante l'esecuzione di più passaggi.

    
risposta data 26.01.2016 - 17:02
fonte
1

Onestamente, dovrei semplicemente configurare il tuo progetto in modo che gestisca gli errori con garbo, probabilmente portando l'utente a una pagina di errore personalizzata (tramite un gestore di errori) che dice qualcosa come "Whoops, qualcosa è andato storto. Riprova più tardi."

Ammiro l'idea di provare a prevenire l'errore in primo luogo (e l'idea di @ @ DavidArno con le transazioni è azzeccata). Ma sappi solo che a un certo punto ci saranno solo degli errori che non è possibile codificare, come, ad esempio, avere il tuo server database appena morto a metà richiesta. Certo, potresti mettere migliaia di assegni dappertutto per ogni situazione immaginabile, ma poi il tuo codice diventa un incubo irraggiungibile. Vorresti avere un codice base come questo (psuedocode, anche esagerato per fare un punto):

function GetUserName(userId) {
    get connection
    check to make sure connection failure one didn't happen
    check to make sure connection failure two didn't happen
    ....
    check to make sure connection failure nine didn't happen
    check to make sure connection failure ten didn't happen
    try grabbing the user
    check that no errors were thrown because the db may have died
    repeat previous checks again
    check that user is not null
    make sure ....
    return user.Name;
}

Ciò che dovrebbe essere una semplice funzione di linea di coppia ora si è trasformata in un enorme mostro. Non voglio immaginare qualcosa di più complicato. (Questo non vuol dire che alcuni errori di controllo non siano appropriati. Ad un certo punto, tuttavia, è necessario assumere che le cose funzionino.)

A volte devi solo essere tollerante nei confronti della vita. Un messaggio generico oops è sufficiente quando non c'è nulla di più specifico da dire che vale la pena avere un codice più complicato.

    
risposta data 26.01.2016 - 17:56
fonte
0

Si chiamano "Clausole di protezione". Sarebbe simile a questo:

if (!userHasAccess) throw new UnauthorizedException();
if (customer == null) throw new NotFoundException("customer");
if (orders.Count == 0) return 0;
if (anOrderHasFailed) throw new NotFoundException("Order " + orderNumber.ToString());

Non così difficile, davvero, e il tuo codice sarà più robusto per lo sforzo.

Per coloro che sono preoccupati per le condizioni di gara, otterrete una NullReferenceException quando provate a dereferire comunque una proprietà sull'oggetto null, quindi se volete, potete semplicemente saltare i controlli null.

In pratica, tali condizioni di gara sono piuttosto rare; se il tuo cliente scompare improvvisamente, hai problemi più grandi.

    
risposta data 26.01.2016 - 16:35
fonte
0

Benvenuti nel mondo della codifica in un linguaggio orientato agli oggetti che non utilizza un framework per proteggersi da eventuali percorsi di riferimento null. Questo dolore è uno dei motivi principali per cui le persone guardano ai paradigmi di altre lingue per affrontare questa incertezza. Ad esempio, gli oggetti immutabili, una volta istanziati, possono essere tranquillamente trattati come mai null se il tuo codice di istanza garantisce un corretto ritorno non nullo e un'idonea idratazione non nulla di tutte le proprietà degli oggetti.

Ecco alcuni dei miei pensieri personali sul tuo esempio di codice sopra:

Se un determinato metodo può prendere qualsiasi ID cliente come parametro, allora non può sempre restituire lo stesso tipo di oggetto (un oggetto "Importo") per un caso in cui l'ID è valido rispetto a non valido. E lanciare un'eccezione per ogni possibile valore non valido in ogni metodo è brutta. Programmazione basata sull'eccezione, che personalmente non mi piace, perché interrompe il normale flusso di funzioni ed è simile a GOTO a tale riguardo. Suggerirei di restituire qualcosa come un oggetto Message<T> , dove la classe Amount è la T per la chiamata, e la classe Message ha campi per cose come bool Success e string[] Errors . In questo modo, il tuo metodo può solo rilevare o tentare / catturare un carico del Cliente non riuscito e restituire quell'oggetto messaggio senza nemmeno tentare di elaborare un ritorno Importo valido.

Tuttavia, ti rimane ancora un oggetto Message<Amount> che potrebbe contenere un oggetto Amount vuoto. Per far fronte a questo, hai alcune opzioni. In primo luogo, lascia semplicemente null e imposta il codice per controllare sempre il campo Success prima di operare su <T> all'interno. Oppure, puoi creare gli oggetti di restituzione come struct , che, essendo tipi di valore, non sono mai nulli. TUTTAVIA, viene fornito con un mondo di conseguenze, quindi assicuratevi di essere a posto con i tipi di valore che vengono trasmessi in caso affermativo. In terzo luogo, puoi utilizzare il pattern NullObject , in cui definisci un modo per costruire un oggetto Amount, ma di un sottotipo chiamato qualcosa come NullAmount , e il tuo codice che consuma dovrà sapere come cercare questi oggetti NullAmount e reagire di conseguenza.

Se sei dead-set contro qualsiasi tipo di controllo condizionale o nullo quando consumi i risultati di tale chiamata di metodo, probabilmente lanciare eccezioni è la soluzione migliore, ma ti incoraggio vivamente guardare al pattern Message, o il pattern NullObject sopra come soluzioni migliori. Indipendentemente da ciò, C # / Java consente che i riferimenti agli oggetti siano nulli di progettazione, e quindi un certo livello di legwork è coinvolto nel controllo e nella gestione di questo più famoso di Miliardi di dollari in errori .

    
risposta data 26.01.2016 - 17:29
fonte
0

Lo scenario descritto è in realtà un ottimo strumento per eseguire correttamente la gestione delle eccezioni - per interrompere un processo e recuperare da errori eccezionali.

Le eccezioni dovrebbero insinuarsi, in primo luogo dove è possibile continuare in modo intelligente, prendendo una nuova direzione alla luce dell'errore. Non aver paura di lasciare che sia completamente fuori dall'applicazione. Puoi recuperare solo dove puoi, provare a farlo troppo presto porta a un altro errore.

Passare tutto il tempo a fare controlli di errore, che in realtà non si può fare nulla, è controproducente. Abbraccia le eccezioni, non provare a combatterli.

    
risposta data 30.01.2016 - 07:55
fonte

Leggi altre domande sui tag