2009-05-28 9 views
8

He tomado el consejo que he visto en otras preguntas respondidas sobre cuándo lanzar excepciones, pero ahora mis API tienen un ruido nuevo. En lugar de llamar a métodos envueltos en bloques try/catch (molestas excepciones), tengo parámetros de argumentos sin salida con una colección de errores que pueden haber ocurrido durante el procesamiento. Entiendo por qué envolver todo en un try/catch es una mala forma de controlar el flujo de una aplicación, pero rara vez veo código en cualquier lugar que refleje esta idea.¿Cómo se ve el código cuando no usa excepciones para controlar el flujo?

Es por eso que todo esto me parece tan extraño. Es una práctica que supuestamente es la forma correcta de codificar, pero no la veo en ningún lado. Además de eso, no entiendo muy bien cómo relacionarme con el código del cliente cuando ha ocurrido un comportamiento "malo".

Aquí hay un fragmento de código con el que estoy trabajando y que trata de guardar imágenes que cargan los usuarios de una aplicación web. No te preocupes por los detalles (es feo), solo mira cómo agregué estos parámetros de salida a todo para obtener mensajes de error.

public void Save(UserAccount account, UserSubmittedFile file, out IList<ErrorMessage> errors) 
{ 
    PictureData pictureData = _loader.GetPictureData(file, out errors); 

    if(errors.Any()) 
    { 
     return; 
    } 

    pictureData.For(account); 

    _repo.Save(pictureData); 
} 

¿Es esta la idea correcta? Puedo razonablemente esperar que un archivo enviado por el usuario sea inválido de alguna manera, así que no debería lanzar una excepción, sin embargo, me gustaría saber qué está mal con el archivo, así que produzco mensajes de error. Del mismo modo, cualquier cliente que ahora consume este método de guardado también querrá saber qué fue lo que falló con la operación general de guardado de imágenes.

Tenía otras ideas sobre devolver algún objeto de estado que contuviera un resultado y mensajes de error adicionales, pero eso se siente raro. Sé que tener parámetros en todas partes va a ser difícil de mantener/refactorizar/etc.

¡Me gustaría algo de orientación sobre esto!

EDIT: Creo que el fragmento de archivo enviado por el usuario puede llevar a las personas a pensar en excepciones generadas al cargar imágenes no válidas y otros errores "difíciles". Creo que este fragmento de código es una mejor ilustración de dónde creo que se está desalentando la idea de lanzar una excepción.

Con esto estoy guardando una nueva cuenta de usuario. Hago una validación de estado en la cuenta de usuario y luego presiono la tienda persistente para averiguar si se tomó el nombre de usuario.

public UserAccount Create(UserAccount account, out IList<ErrorMessage> errors) 
{ 
    errors = _modelValidator.Validate(account); 

    if (errors.Any()) 
    { 
     return null; 
    } 

    if (_userRepo.UsernameExists(account.Username)) 
    { 
     errors.Add(new ErrorMessage("Username has already been registered.")); 
     return null; 
    } 

    account = _userRepo.CreateUserAccount(account); 

    return account; 
} 

¿Debo arrojar algún tipo de excepción de validación? ¿O debería devolver los mensajes de error?

+0

También aprendí cosas importantes con respecto a arrojar/atrapar excepciones en SO. Pero creo que debes haber malinterpretado algunas (o la mayoría) de las cosas que leíste aquí, para llegar a una solución como esa. –

+1

¡Ah, entonces debes saber cuál es la solución! Cuidado de compartir? –

+0

Mi punto es que las respuestas que estás buscando están aquí y solo necesitas volver a leerlas con un estado de ánimo diferente. –

Respuesta

7

Por definición, "excepción" significa una circunstancia excepcional de la cual una rutina no puede recuperarse. En el ejemplo que proporcionó, parece que eso significa que la imagen era inválida/corrupta/ilegible/etc. Eso debe arrojarse y burbujear hasta la capa superior, y allí decidir qué hacer con la excepción. La excepción en sí contiene la información más completa sobre lo que salió mal, que debe estar disponible en los niveles superiores.

Cuando la gente dice que no debe usar excepciones para controlar el flujo del programa, lo que quieren decir es: (por ejemplo) si un usuario intenta crear una cuenta pero la cuenta ya existe, no debe lanzar una excepción AccountExistsException y atraparla más arriba en la aplicación para poder proporcionar esa retroalimentación al usuario, porque la cuenta que ya existe no es un caso excepcional. Debería esperar esa situación y manejarla como parte del flujo normal de su programa. Si no puede conectarse a la base de datos, que es un caso excepcional.

Parte del problema con su ejemplo de registro de usuario es que está tratando de encapsular demasiado en una sola rutina. Si su método intenta hacer más de una cosa, debe seguir el estado de varias cosas (por lo tanto, las cosas se ponen feas, como las listas de mensajes de error). En este caso, lo que podría hacer en su lugar es:

UsernameStatus result = CheckUsernameStatus(username); 
if(result == UsernameStatus.Available) 
{ 
    CreateUserAccount(username); 
} 
else 
{ 
    //update UI with appropriate message 
} 

enum UsernameStatus 
{ 
    Available=1, 
    Taken=2, 
    IllegalCharacters=3 
} 

Obviamente, esto es un ejemplo simplificado, pero espero que el punto es claro: sus rutinas sólo deben tratar de hacer una cosa, y debe tener una limitada/predecible alcance de la operación Eso hace que sea más fácil detener y redirigir el flujo del programa para hacer frente a diversas situaciones.

+0

Mira mi último fragmento. ¿Es eso algo que justificaría una excepción lanzada? –

+0

No, no es un caso excepcional. Debería manejar algo así como un nombre de usuario ya en uso como parte de su lógica comercial utilizando algo así como una colección BrokenRules. – JasonS

+0

Rex, para el método de registro de usuarios, está en el nivel de la orquestación de crear una nueva cuenta. Parte de esa orquestación es validar el estado de la cuenta y luego pasarla a la tienda persistente apropiada. Suponga que no hay verificación de nombre de usuario. Solo validamos el estado de la cuenta antes de guardarla. ¿Cómo relaciono los errores de estado con el cliente? ¿La recomendación es llevar la validación a un módulo de nivel superior (en este caso, un controlador)? –

9

A pesar de las preocupaciones sobre el rendimiento, creo que es realmente más limpio permitir que se descarten las excepciones de un método. Si hay alguna excepción que pueda manejarse dentro de su método, debe manejarlas de manera apropiada, pero de lo contrario, déjenlas brotar.

La devolución de errores en los parámetros de salida, o la devolución de los códigos de estado se siente un poco torpe. A veces, cuando me enfrento a esta situación, trato de imaginar cómo manejaría .NET Framework los errores. No creo que haya muchos métodos .NET framework que devuelvan errores en los parámetros o devuelvan códigos de estado.

+0

He estado en la misma escuela de pensamiento. Otras preguntas respondidas sobre este tema generalmente tienen respuestas como "solo lanzar una excepción si hay un holocausto nuclear". Es por eso que probé este experimento para ver cómo es la codificación sin usar excepciones para controlar el flujo. Echa un vistazo al nuevo fragmento que agregué a mi pregunta. Es un conjunto de comportamientos erróneos algo más "suave" que lidiar con la carga de archivos, etc. ¿Eso cambia la perspectiva en absoluto? Y aunque los parámetros parecen torpes, admito que tener que probar/atrapar todo es torpe por sí mismo. –

+0

La diferencia clave es que .NET Framework no tiene ninguna lógica comercial. Es un marco, por lo que cualquier cosa fuera de la norma es una excepción. En lógica de dominio, hay diversos grados de "fuera de la norma". ¿Está fuera de la norma si se toma un nombre de usuario? Yo diría que no. –

+1

@Rex, ese es un buen punto. Creo que la dificultad radica en tomar estas decisiones cuando se trata de la lógica de negocios. Supongo que me parece más intuitivo suponer que un método hizo lo que se suponía que era, a menos que se lanzara una excepción. Prefiero no tener que preocuparme por verificar un valor de retorno nulo o verificar/manejar códigos de error en un objeto de estado. Creo que un método de bajo nivel como "CreateUser" podría no saber qué hacer si el nombre de usuario ya se tomó, por lo que es mejor lanzar una excepción y dejar que el código de nivel superior decida cómo manejarlo. –

3

Creo que este es el enfoque equivocado. Sí, es muy probable que obtenga ocasionalmente imágenes no válidas. Pero ese sigue siendo el escenario excepcional. En mis opiniones, las excepciones son la elección correcta aquí.

0

Permitiría excepciones, pero en función de su hilo, busca una alternativa. ¿Por qué no incluir un estado o información de error en su objeto PictureData? A continuación, puede devolver el objeto con los errores y las demás cosas que quedaron vacías.Solo una sugerencia, pero estás haciendo exactamente lo que se hicieron excepciones para resolver :)

0

En primer lugar, nunca se deben usar excepciones como mecanismo de control de flujo. Las excepciones son un mecanismo de propagación y manejo de errores, pero nunca deben usarse para controlar el flujo del programa. El flujo de control es el dominio de sentencias condicionales y bucles. A menudo, esto es una idea errónea y crítica que muchos programadores hacen, y generalmente es lo que lleva a tales pesadillas cuando tratan de lidiar con las excepciones.

En un lenguaje como C# que ofrece manejo estructurado de excepciones, la idea es permitir que los casos 'excepcionales' en su código sean identificados, propagados y eventualmente manejados. El manejo generalmente se deja al nivel más alto de su aplicación (es decir, un cliente de Windows con una interfaz de usuario y diálogos de error, un sitio web con páginas de error, un recurso de registro en el bucle de mensajes de un servicio en segundo plano, etc.) A diferencia de Java, que usa control de excepciones comprobado, C# no requiere que maneje específicamente cada excepción que pueda pasar por sus métodos. Por el contrario, tratar de hacerlo indudablemente conduciría a algunos cuellos de botella de rendimiento severos, ya que capturar, manejar y posiblemente volver a lanzar excepciones es un negocio costoso.

La idea general con excepciones en C# es que si, por casualidad ... y subrayo si, porque se llaman excepciones debido al hecho de que durante el funcionamiento normal, usted no debe encontrar ningún condiciones excepcionales, ... si suceden, entonces usted tiene las herramientas a su disposición para recuperar de forma segura y limpia y presentar al usuario (si lo hay) con una notificación de la falla de la aplicación y posibles opciones de resolución.

La mayoría de las veces, una aplicación C# bien escrita no tendrá tantos bloques try/catch en la lógica empresarial central, y tendrá mucho más try/finally, o mejor aún, utilizando bloques. Para la mayoría del código, la preocupación en respuesta a una excepción es recuperarse muy bien liberando recursos, bloqueos, etc.y permitiendo que la excepción continúe. En su código de nivel superior, normalmente en el bucle externo de procesamiento de mensajes de una aplicación o en el controlador de eventos estándar para sistemas como ASP.NET, con el tiempo realizará su estructurado manejo con try/catch, posiblemente con varias cláusulas catch para tratar con errores específicos que requieren un manejo único.

Si maneja adecuadamente las excepciones y el código de construcción que usa excepciones de forma adecuada, no debe preocuparse por muchos bloques try/catch/finally, códigos de retorno o firmas de métodos intrincados con muchos ref y fuera de los parámetros. Debería ver el código de la misma familia:

public void ClientAppMessageLoop() 
{ 
    bool running = true; 
    while (running) 
    { 
     object inputData = GetInputFromUser(); 
     try 
     { 
      ServiceLevelMethod(inputData); 
     } 
     catch (Exception ex) 
     { 
      // Error occurred, notify user and let them recover 
     } 
    } 
} 

// ... 

public void ServiceLevelMethod(object someinput) 
{ 
    using (SomeComponentThatsDisposable blah = new SomeComponentThatsDisposable()) 
    { 
     blah.PerformSomeActionThatMayFail(someinput); 
    } // Dispose() method on SomeComponentThatsDisposable is called here, critical resource freed regardless of exception 
} 

// ... 

public class SomeComponentThatsDisposable: IDosposable 
{ 
    public void PErformSomeActionThatMayFail(object someinput) 
    { 
     // Get some critical resource here... 

     // OOPS: We forgot to check if someinput is null below, NullReferenceException! 
     int hash = someinput.GetHashCode(); 
     Debug.WriteLine(hash); 
    } 

    public void Dispose() 
    { 
     GC.SuppressFinalize(this); 

     // Clean up critical resource if its not null here! 
    } 
} 

Siguiendo el paradigma anterior, usted no tiene una gran cantidad de código try/catch desordenado por todas partes, pero todavía su "protegido" de excepciones que de otro modo interrumpir su normal de Flujo de programa y burbuja hasta su código de manejo de excepciones de nivel superior.

EDIT:

Un buen artículo que cubre el uso previsto de excepciones, y por qué excepciones no se comprueban en C#, es la siguiente entrevista con Anders Heijlsberg, el principal arquitecto del lenguaje C#:

http://www.artima.com/intv/handcuffsP.html

EDIT 2:

Para proporcionar un mejor ejemplo que funciona con el código que envió, tal vez lo siguiente será más útil. Supongo que a algunos de los nombres, y hacer las cosas una de las maneras que he encontrado ... servicios implementados por lo perdonará ninguna licencia Tomo:

public PictureDataService: IPictureDataService 
{ 
    public PictureDataService(RepositoryFactory repositoryFactory, LoaderFactory loaderFactory) 
    { 
    _repositoryFactory = repositoryFactory; 
    _loaderFactory = loaderFactory; 
    } 

    private readonly RepositoryFactory _repositoryFactory; 
    private readonly LoaderFactory _loaderFactory; 
    private PictureDataRepository _repo; 
    private PictureDataLoader _loader; 

    public void Save(UserAccount account, UserSubmittedFile file) 
    { 
    #region Validation 
    if (account == null) throw new ArgumentNullException("account"); 
    if (file == null) throw new ArgumentNullException("file"); 
    #endregion 

    using (PictureDataRepository repo = getRepository()) 
    using (PictureDataLoader loader = getLoader()) 
    { 
     PictureData pictureData = loader.GetPictureData(file); 
     pictureData.For(account); 
     repo.Save(pictureData); 
    } // Any exceptions cause repo and loader .Dispose() methods 
     // to be called, cleaning up their resources...the exception 
     // bubbles up to the client 
    } 

    private PictureDataRepository getRepository() 
    { 
    if (_repo == null) 
    { 
     _repo = _repositoryFactory.GetPictureDataRepository(); 
    } 

    return _repo; 
    } 

    private PictureDataLoader getLoader() 
    { 
    if (_loader == null) 
    { 
     _loader = _loaderFactory.GetPictureDataLoader(); 
    } 

    return _loader; 
    } 
} 

public class PictureDataRepository: IDisposable 
{ 
    public PictureDataRepository(ConnectionFactory connectionFactory) 
    { 
    } 

    private readonly ConnectionFactory _connectionFactory; 
    private Connection _connection; 

    // ... repository implementation ... 

    public void Dispose() 
    { 
    GC.SuppressFinalize(this); 

    _connection.Close(); 
    _connection = null; // 'detatch' from this object so GC can clean it up faster 
    } 
} 

public class PictureDataLoader: IDisposable 
{ 
    // ... Similar implementation as PictureDataRepository ... 
} 
+0

Esto es lo que leo mucho y acepto. Empleo las mismas prácticas para manejo de excepciones y recursos de liberación. Entiendo que estaba dando un ejemplo rápido, pero para su ejemplo, supongamos que esta aplicación esperará un nulo ocasional porque eso es lo que hacen los usuarios. Desea manejar eso y devolver algo significativo porque hay pocas cosas tan horribles como la NullReferenceException. ¿Cómo reescribirías eso para no ser ingenuo de entradas nulas y al mismo tiempo reconocer que una entrada nula no es nada realmente excepcional? –

+0

Bueno, mi ejemplo obviamente fue inventado. Recuerda que dije que nunca deberías encontrar la mayoría de las excepciones ... una aplicación correctamente escrita habría verificado nulo, y devuelto un código de error, o ... arrojado una de las pocas excepciones aceptables y recuperables que deberían usarse: ArgumentNullException. Cada aplicación C# que escribo realiza una amplia validación de argumentos y arroja una de ArgumentException, ArgumentNullException o ArgumentOutOfRangeException si la entrada no es válida. Mi capa de interfaz de usuario maneja todo lo que se deriva de ArgumentException en base a un subsistema por subsistema caso por caso. – jrista

3

En situaciones como que tiene por lo general lanzo una costumbre excepción a la persona que llama. Tengo un punto de vista diferente sobre excepciones tal vez que otros: si el método no pudo hacer lo que se pretende (es decir, lo que dice el nombre del método: Crear una cuenta de usuario), entonces debería lanzar una excepción: yo: no hacer lo que se supone que debes hacer es excepcional.

Para el ejemplo informados, tendría algo así como:

public UserAccount Create(UserAccount account) 
{ 
    if (_userRepo.UsernameExists(account.Username)) 
     throw new UserNameAlreadyExistsException("username is already in use."); 
    else 
     return _userRepo.CreateUserAccount(account); 
} 

El beneficio, al menos para mí, es que mi interfaz de usuario es tonta. Sólo trato/captura de cualquier función y MESSAGEBOX el mensaje de excepción como:

try 
{ 
    UserAccount newAccount = accountThingy.Create(account); 
} 
catch (UserNameAlreadyExistsException unaex) 
{ 
    MessageBox.Show(unaex.Message); 
    return; // or do whatever here to cancel proceeding 
} 
catch (SomeOtherCustomException socex) 
{ 
    MessageBox.Show(socex.Message); 
    return; // or do whatever here to cancel proceeding 
} 
// If this is as high up as an exception in the app should bubble up to, 
// I'll catch Exception here too 

Esto es similar en estilo a una gran cantidad de métodos System.IO (http://msdn.microsoft.com/en-us/library/d62kzs03.aspx) para un ejemplo.

Si se convierte en un problema de rendimiento, me refactorizaré a otra cosa más adelante, pero nunca he tenido que exprimir el rendimiento de una aplicación empresarial debido a las excepciones.

Cuestiones relacionadas