Sto sviluppando un'app Ruby on Rails. L'app contiene un servizio che racchiude un'API REST esterna, chiamata da un controller, con diversi possibili stati di errore. L'implementazione corrente restituisce il corpo della risposta in caso di successo e solleva eccezionalmente un'eccezione catch-all specifica del servizio.
Voglio refactoring per essere in grado di distinguere tra errori di rete e altri errori di autorizzazione causati dal client che fornisce parametri non validi. Qual è il modo più idiomatico per farlo? Che cosa ha prodotto il codice più gestibile nella tua esperienza?
Di seguito sono riportate alcune alternative che ho considerato.
Eccezioni in tutto
It is recommended that a library should have one subclass of StandardError or RuntimeError and have specific exception types inherit from it. This allows the user to rescue a generic exception type to catch all exceptions the library may raise even if future versions of the library add new exception subclasses.
Dalla documentazione ufficiale di Ruby .
Il codice completo per il mio servizio è attualmente 39 linee e non può essere considerato una libreria, ma questa strategia potrebbe essere ancora applicabile. Una possibile implementazione è elencata di seguito.
class MyService
def self.call(input)
res = do_http_call input
if res and res.code == 200
res.body
elsif res and res.code == 401
fail MyServiceAuthenticationError
else
fail MyServiceError
end
end
end
class MyServiceError < StandardError
end
class MyServiceAuthenticationError < MyServiceError
end
Venendo da altre lingue questo approccio non sembra giusto. Ho spesso sentito il mantra "riservare eccezioni per casi eccezionali", ad esempio in Code Complete (Steve McConnell, 2a edizione, pagina 199):
Throw an exception on for conditions that are truly exceptional
Exceptions should be reserved for conditions that are truly exceptional – in other words, for conditions that cannot be addressed by other coding practices. Exceptions are used in similar circumstances to assertions – for events that are not just infrequent but for events that should never occur.
Le eccezioni sono davvero per errori eccezionali? è una discussione di questo argomento. Le risposte forniscono vari consigli e la risposta di S.Lott afferma esplicitamente "Non utilizzare le eccezioni per convalidare l'input dell'utente", che Penso che sia approssimativamente ciò che la strategia delineata sopra equivale a.
Simboli per errori "non eccezionali"
La mia prima intuizione è usare le eccezioni per gli errori che voglio far esplodere lo stack e simboli per i risultati che il chiamante può aspettarsi e vuole gestire.
class MyService
def self.call(input)
res = do_http_call input
if res.code == 200
res.body
elsif res.code == 401
:invalid_authentication
else
fail MyServiceError
end
end
end
class MyServiceError < StandardError
end
Proprio come le eccezioni in tutto, questo è facile da estendere con errori aggiuntivi.
Potrebbe tuttavia portare a problemi di manutenibilità. Se viene aggiunto un nuovo valore di ritorno di un simbolo e un chiamante non viene modificato, il simbolo di errore può essere interpretato silenziosamente come risultato positivo poiché il valore di ritorno positivo è una stringa. Non so quanto sia realistico in pratica però.
Inoltre, questo approccio può essere considerato più strong insieme al suo chiamante. Se un errore debba insinuarsi nello stack delle chiamate o essere gestito dal chiamante immediato, probabilmente non si tratta di qualcosa con cui il destinatario dovrebbe preoccuparsi.
Falso in caso di errore
Un esempio di questo approccio è ActiveRecord::Base#save
.
- Se l'operazione ha esito positivo, restituisce il risultato o true nel caso di
#save
. - Se le convalide falliscono, restituisce false.
- Se si verificano alcuni tipi di errori imprevisti, come i campi di codifica con UTF-8 in
#save
, viene generata un'eccezione.
class MyService
def self.call(input)
res = do_http_call input
if res.code == 200
res.body
elsif res.code == 401
false
else
fail MyServiceError
end
end
end
class MyServiceError < StandardError
end
In genere non mi piace questa strategia poiché false
non ha alcun significato semantico ed è impossibile distinguere tra errori.
Un altro modo
C'è un altro modo migliore?