c# - ¿Cómo se comunican los mensajes/errores de la capa de servicio a capas superiores usando MVP?
asp.net exception (3)
Actualmente estoy escribiendo una aplicación ASP.Net desde la interfaz de usuario hacia abajo. Estoy implementando una arquitectura MVP porque estoy harto de Winforms y quería algo que tuviera una mejor separación de preocupaciones.
Entonces, con MVP, el presentador maneja los eventos planteados por la vista. Aquí hay un código que tengo listo para tratar con la creación de usuarios:
public class CreateMemberPresenter
{
private ICreateMemberView view;
private IMemberTasks tasks;
public CreateMemberPresenter(ICreateMemberView view)
: this(view, new StubMemberTasks())
{
}
public CreateMemberPresenter(ICreateMemberView view, IMemberTasks tasks)
{
this.view = view;
this.tasks = tasks;
HookupEventHandlersTo(view);
}
private void HookupEventHandlersTo(ICreateMemberView view)
{
view.CreateMember += delegate { CreateMember(); };
}
private void CreateMember()
{
if (!view.IsValid)
return;
try
{
int newUserId;
tasks.CreateMember(view.NewMember, out newUserId);
view.NewUserCode = newUserId;
view.Notify(new NotificationDTO() { Type = NotificationType.Success });
}
catch(Exception e)
{
this.LogA().Message(string.Format("Error Creating User: {0}", e.Message));
view.Notify(new NotificationDTO() { Type = NotificationType.Failure, Message = "There was an error creating a new member" });
}
}
}
Realizo la validación de mi formulario principal utilizando los controles de validación de .NET incorporados, pero ahora debo verificar que los datos satisfagan suficientemente los criterios para la capa de servicio.
Digamos que los siguientes mensajes de Capa de servicio pueden aparecer:
- La cuenta de correo electrónico ya existe (falla)
- El usuario de referencia ingresado no existe (falla)
- La longitud de la contraseña excede la longitud permitida del almacén de datos (error)
- Miembro creado con éxito (éxito)
Digamos también que habrá más reglas en la capa de servicio que la interfaz de usuario no puede anticipar.
Actualmente estoy haciendo que la capa de servicio arroje una excepción si las cosas no salen según lo planeado. ¿Es eso una estrategia suficiente? ¿Te huele este código? Si escribiera una capa de servicio como esta, ¿le molestaría tener que escribir Presenters que la usen de esta manera? Los códigos de devolución parecen demasiado antiguos y un bool no es lo suficientemente informativo.
Editar no por OP: fusionarse en los comentarios de seguimiento que se publicaron como respuestas por el OP
Cheekysoft, me gusta el concepto de una excepción ServiceLayerException. Ya tengo un módulo de excepción global para las excepciones que no anticipo. ¿Le parece tedioso hacer todas estas excepciones personalizadas? Estaba pensando que atrapar la clase base de Exception era un poco mal, pero no estaba exactamente seguro de cómo progresaría desde allí.
tgmdbm, me gusta el uso inteligente de la expresión lambda allí!
Gracias Cheekysoft por el seguimiento. Así que supongo que esa sería la estrategia si no le importa que el usuario se muestre en una página separada (soy principalmente un desarrollador web) si no se maneja la excepción.
Sin embargo, si deseo devolver el mensaje de error en la misma vista donde el usuario envió los datos que causaron el error, ¿tendría que atrapar la excepción en el presentador?
Así es como se ve CreateUserView cuando el presentador ha manejado la excepción ServiceLayerException:
Para este tipo de error, es bueno informarlo a la misma vista.
De todos modos, creo que vamos más allá del alcance de mi pregunta original ahora. Jugaré con lo que has publicado y, si necesito más detalles, publicaré una nueva pregunta.
Como sugiere Cheekysoft, yo tendería a mover todas las excepciones principales a un ExceptionHandler y dejar que esas excepciones aparezcan. El ExceptionHandler representaría la vista adecuada para el tipo de excepción.
Sin embargo, todas las excepciones de validación deben manejarse en la vista, pero normalmente esta lógica es común a muchas partes de su aplicación. Entonces me gusta tener un ayudante como este
public static class Try {
public static List<string> This( Action action ) {
var errors = new List<string>();
try {
action();
}
catch ( SpecificException e ) {
errors.Add( "Something went ''orribly wrong" );
}
catch ( ... )
// ...
return errors;
}
}
Luego, cuando llame a su servicio solo haga lo siguiente
var errors = Try.This( () => {
// call your service here
tasks.CreateMember( ... );
} );
Luego, en los errores está vacío, estás listo para ir.
Puede llevar esto más allá y ampliarlo con los manejadores de excepciones custome que manejan excepciones poco comunes .
En respuesta a la pregunta de seguimiento:
En cuanto a crear excepciones que se vuelven tediosas, te acostumbras un poco a eso. El uso de un buen generador de código o plantilla puede crear la clase de excepción con una edición manual mínima en aproximadamente 5 o 10 segundos.
Sin embargo, en muchas aplicaciones del mundo real, el manejo de errores puede ser el 70% del trabajo, por lo que todo es parte del juego realmente.
Como tgmdbm sugiere, en las aplicaciones MVC / MVP dejo todas las excepciones no manejables hasta la cima y me atrapó el despachador que delega en un ExceptionHandler. Lo configuro para que use un ExceptionResolver que busque en el archivo de configuración para elegir una vista apropiada para mostrar al usuario. La biblioteca Spring MVC de Java lo hace muy bien. Aquí hay un fragmento de un archivo de configuración para el resolvedor de excepciones de Spring MVC: es para Java / Spring pero obtendrá la idea.
Esto requiere una gran cantidad de manejo de excepciones de sus presentadores / controladores por completo.
<bean id="exceptionResolver"
class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<props>
<prop key="UserNotFoundException">
rescues/UserNotFound
</prop>
<prop key="HibernateJdbcException">
rescues/databaseProblem
</prop>
<prop key="java.net.ConnectException">
rescues/networkTimeout
</prop>
<prop key="ValidationException">
rescues/validationError
</prop>
<prop key="EnvironmentNotConfiguredException">
rescues/environmentNotConfigured
</prop>
<prop key="MessageRejectedPleaseRetryException">
rescues/messageRejected
</prop>
</props>
</property>
<property name="defaultErrorView" value="rescues/general" />
</bean>
Eso suena perfecto para mí. Las excepciones son preferibles, ya que pueden lanzarse a la parte superior de la capa de servicio desde cualquier lugar dentro de la capa de servicio, sin importar qué tan anidadas estén dentro de la implementación del método de servicio. Esto mantiene el código de servicio limpio ya que usted sabe que el presentador llamante siempre recibirá una notificación del problema.
No atrape la excepción
Sin embargo, no atrape Exception en el presentador, sé que es tentador porque mantiene el código más corto, pero debe detectar excepciones específicas para evitar detectar las excepciones a nivel del sistema.
Planea una Jerarquía de excepciones simples
Si va a usar excepciones de esta manera, debe diseñar una jerarquía de excepciones para sus propias clases de excepciones. En un mínimo crea una clase ServiceLayerException y lanza una de éstas en tus métodos de servicio cuando ocurre un problema. Entonces, si necesita lanzar una excepción que el presentador deba o pueda manejar de manera diferente, puede lanzar una subclase específica de ServiceLayerException: say, AccountAlreadyExistsException.
Su presentador tiene la opción de hacer
try {
// call service etc.
// handle success to view
}
catch (AccountAlreadyExistsException) {
// set the message and some other unique data in the view
}
catch (ServiceLayerException) {
// set the message in the view
}
// system exceptions, and unrecoverable exceptions are allowed to bubble
// up the call stack so a general error can be shown to the user, rather
// than showing the form again.
Usar la herencia en sus propias clases de excepción significa que no es necesario capturar excepciones múltiples en su presentador, puede hacerlo si es necesario, y no termina capturando accidentalmente excepciones que no puede manejar. Si su presentador ya está en la parte superior de la pila de llamadas, agregue un bloque de captura (Excepción) para manejar los errores del sistema con una vista diferente.
Siempre trato de pensar en mi capa de servicio como una biblioteca distribuible separada, y arrojo una excepción tan específica como tenga sentido. Depende de la implementación del presentador / controlador / servicio remoto decidir si necesita preocuparse por los detalles específicos o simplemente tratar los problemas como un error genérico.