c# - usuario - tipos específicos de excepciones más comunes en programacion
Lanzar excepciones en declaraciones de interruptor cuando no se puede manejar ningún caso específico (9)
Digamos que tenemos una función que cambia una contraseña para un usuario en un sistema en una aplicación MVC .:
public JsonResult ChangePassword
(string username, string currentPassword, string newPassword)
{
switch (this.membershipService.ValidateLogin(username, currentPassword))
{
case UserValidationResult.BasUsername:
case UserValidationResult.BadPassword:
// abort: return JsonResult with localized error message
// for invalid username/pass combo.
case UserValidationResult.TrialExpired
// abort: return JsonResult with localized error message
// that user cannot login because their trial period has expired
case UserValidationResult.Success:
break;
}
// NOW change password now that user is validated
}
membershipService.ValidateLogin()
devuelve una enumeración de UserValidationResult
que se define como:
enum UserValidationResult
{
BadUsername,
BadPassword,
TrialExpired,
Success
}
Siendo un programador defensivo, cambiaría el método ChangePassword()
para arrojar una excepción si ValidateLogin()
UserValidationResult
valor de UserValidationResult
no reconocido:
public JsonResult ChangePassword
(string username, string currentPassword, string newPassword)
{
switch (this.membershipService.ValidateLogin(username, currentPassword))
{
case UserValidationResult.BasUsername:
case UserValidationResult.BadPassword:
// abort: return JsonResult with localized error message
// for invalid username/pass combo.
case UserValidationResult.TrialExpired
// abort: return JsonResult with localized error message
// that user cannot login because their trial period has expired
case UserValidationResult.Success:
break;
default:
throw new NotImplementedException
("Unrecognized UserValidationResult value.");
// or NotSupportedException()
break;
}
// Change password now that user is validated
}
Siempre consideré un patrón como el último fragmento sobre una mejor práctica. Por ejemplo, ¿qué ocurre si un desarrollador obtiene un requisito de que ahora, cuando los usuarios intentan iniciar sesión, si por este o aquel motivo empresarial, se supone que deben contactar primero a la empresa? Entonces la definición de UserValidationResult
se actualiza para que sea:
enum UserValidationResult
{
BadUsername,
BadPassword,
TrialExpired,
ContactUs,
Success
}
El desarrollador cambia el cuerpo del método ValidateLogin()
para devolver el nuevo valor enum ( UserValidationResult.ContactUs
) cuando corresponda, pero se olvida de actualizar ChangePassword()
. ¡Sin la excepción en el cambio, el usuario todavía puede cambiar su contraseña cuando su intento de inicio de sesión no debería validarse en primer lugar!
¿Soy solo yo, o es esto default: throw new Exception()
una buena idea? Lo vi hace algunos años y siempre (después de hacerlo) suponer que es una mejor práctica.
Con lo que tienes, está bien, aunque la sentencia break después nunca será golpeada porque la ejecución de ese hilo cesará cuando se lanza y no se maneja una excepción.
Creo que enfoques como este pueden ser muy útiles (por ejemplo, ver rutas de código erróneamente vacías ). Es incluso mejor si puede hacer que ese tiro predeterminado se compile en el código liberado, como una afirmación. De esta forma, tendrá una actitud defensiva adicional durante el desarrollo y la prueba, pero no incurrirá en la sobrecarga del código adicional cuando libere.
He usado esta práctica antes para instancias específicas cuando la enumeración vive "lejos" de su uso, pero en los casos en que la enumeración es realmente cercana y está dedicada a una característica específica, parece un poco demasiado.
Con toda probabilidad, sospecho que un error de aserción de depuración es probablemente más apropiado.
http://msdn.microsoft.com/en-us/library/6z60kt1f.aspx
Tenga en cuenta la segunda muestra de código ...
La validación de las restricciones de tiempo de ejecución suele ser buena, así que sí, las mejores prácticas ayudan con el principio de "fallo rápido" y se está deteniendo (o iniciando sesión) al detectar una condición de error en lugar de continuar silenciosamente. Hay casos en que eso no es cierto, pero dadas las declaraciones de cambio sospecho que no las vemos muy a menudo.
Para elaborar, las instrucciones de cambio siempre pueden ser reemplazadas por bloques if / else. Mirándolo desde esta perspectiva, vemos que el tiro vs no tirar en una caja de interruptor por defecto es equivalente a este ejemplo:
if( i == 0 ) {
} else { // i > 0
}
vs
if( i == 0 ) {
} else if ( i > 0 ) {
} else {
// i < 0 is now an enforced error
throw new Illegal(...)
}
El segundo ejemplo generalmente se considera mejor ya que se produce un error cuando se infringe una restricción de error en lugar de continuar bajo una suposición potencialmente incorrecta.
Si, en cambio, queremos:
if( i == 0 ) {
} else { // i != 0
// we can handle both positve or negative, just not zero
}
Entonces, sospecho que en la práctica ese último caso probablemente aparecerá como una declaración if / else. Debido a que la instrucción switch se asemeja con frecuencia al segundo bloque if / else, que los throws son generalmente una mejor práctica.
Vale la pena considerar algunas consideraciones más: - considere un enfoque polimórfico o un método enum para reemplazar completamente la declaración de cambio, p. Ej .: Métodos dentro de la enumeración en C # - si arrojar es el mejor, como se señala en otras respuestas prefiere usar un tipo de excepción específico evitar el código de la placa de la caldera, por ejemplo: InvalidEnumArgumentException
Nunca agrego un descanso después de una instrucción de lanzamiento contenida en una declaración de cambio. Una de las preocupaciones más importantes es la molesta advertencia de "código inalcanzable detectado" . Entonces sí, es una buena idea.
Para una aplicación web, preferiría un valor predeterminado que genere un resultado con un mensaje de error que solicite al usuario que se ponga en contacto con un administrador y registre un error en lugar de lanzar una excepción que pueda dar como resultado algo menos significativo para el usuario. Como puedo anticipar, en este caso, que el valor de retorno podría ser diferente de lo que esperaba, no consideraría este comportamiento realmente excepcional.
Además, su caso de uso que da como resultado el valor enum adicional no debe introducir un error en su método particular. Mi expectativa sería que el caso de uso solo se aplica al inicio de sesión, lo que sucedería antes de que el método ChangePassword pudiera ser llamado legalmente, presumiblemente, dado que querría asegurarse de que la persona haya iniciado sesión antes de cambiar su contraseña, debería hacerlo nunca ver el valor de ContactUs como un retorno de la validación de la contraseña. El caso de uso especificaría que si un resultado de ContactUs es devuelto, esa autenticación falla hasta que se borre la condición que da como resultado el resultado. Si fuera de otro modo, esperaría que tuvieras requisitos sobre cómo otras partes del sistema deberían reaccionar bajo esa condición y escribirías pruebas que te obligarían a cambiar el código en este método para cumplir con esas pruebas y abordar el nuevo valor de retorno.
Siempre lanzo una excepción en este caso. Considere usar InvalidEnumArgumentException
, que brinda información más completa en esta situación.
Siempre me gusta hacer exactamente lo que tienes, aunque usualmente arrojo una ArgumentException
si se trata de un argumento pasado, pero me gusta más la NotImplementedException
ya que es probable que se agregue una nueva declaración de caso en lugar de que la persona que llama deba cambiar el argumento a uno compatible.
Usted declara que está siendo muy defensivo y casi podría decir que esto está por la borda. ¿El otro desarrollador no probará su código? Seguramente, cuando realicen las pruebas más simples, verán que el usuario aún puede iniciar sesión, por lo que se darán cuenta de lo que deben corregir. Lo que estás haciendo no es horrible ni está mal, pero si pasas mucho tiempo haciéndolo, puede ser demasiado.