如何使用MVP将服务层消息/错误传达给更高层?
我目前正在从UI向下编写ASP.Net应用程序。我之所以要实现MVP架构,是因为我对Winforms感到厌倦,并且想要一种可以更好地分离关注点的东西。
因此,借助MVP,Presenter可以处理View引发的事件。这是我用来处理用户创建的一些代码:
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" }); } } }
我使用内置的.Net验证控件完成了主窗体验证,但是现在我需要验证数据是否足以满足服务层的条件。
假设可以显示以下服务层消息:
- 电子邮件帐户已经存在(失败)
- 输入的推荐用户不存在(失败)
- 密码长度超过数据存储区允许的长度(失败)
- 成员创建成功(成功)
我们还要说,UI无法预期的更多规则将出现在服务层中。
目前,如果事情没有按计划进行,我将使服务层引发异常。这是足够的策略吗?这段代码对你们有影响吗?如果我编写了这样的服务层,我们是否会因为不得不编写以这种方式使用它的Presenters而感到烦恼?返回码似乎太老了,布尔值还不足以提供信息。
Edit not by OP: merging in follow-up comments that were posted as answers by the OP
厚脸皮,我喜欢ServiceLayerException的概念。我已经有了一个意外的全局异常模块。我们是否发现所有这些自定义例外都很乏味?我当时在想捕获基础Exception类有点难闻,但不能完全确定从那里取得的进展。
tgmdbm,我喜欢在那里聪明地使用lambda表达式!
感谢Cheekysoft的跟进。因此,我想如果我们不介意在未处理异常的情况下不向用户显示单独的页面(我主要是网络开发人员),那将是一种策略。
但是,如果我想在用户提交导致错误的数据的同一视图中返回错误消息,那么我是否必须在Presenter中捕获异常?
这是演示者处理ServiceLayerException时的CreateUserView外观:
对于这种错误,很高兴将其报告给同一视图。
无论如何,我认为我们现在已经超出了我原始问题的范围。我将处理我们发布的内容,如果需要更多详细信息,我将发布一个新问题。
解决方案
回答
听起来对我来说是正确的。异常是可取的,因为无论嵌套在服务方法实现内部有多深,它们都可以从服务层内部的任何地方抛出到服务层的顶部。这样可以使服务代码保持整洁,因为我们知道主叫演示者将始终收到问题的通知。
不要捕获异常
但是,不要在演示者中捕获Exception,我知道它很诱人,因为它可使代码保持较短,但是我们需要捕获特定的异常,以避免捕获系统级异常。
计划简单的异常层次结构
如果要以这种方式使用异常,则应为自己的异常类设计一个异常层次结构。
最少创建一个ServiceLayerException类,并在出现问题时将这些方法中的一种扔到服务方法中。然后,如果我们需要引发一个异常,演示者应该/可以不同地处理该异常,则可以引发ServiceLayerException的特定子类:比如AccountAlreadyExistsException。
然后,演示者可以选择执行
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.
在我们自己的异常类中使用继承意味着我们不需要在演示者中捕获多重异常-如果需要,则可以-并且不会最终捕获无法处理的异常。如果演示者已经在调用堆栈的顶部,请添加catch(Exception)块以其他视图处理系统错误。
我总是尝试将我的服务层视为一个单独的可分发库,并抛出一个有意义的特定异常。然后由演示者/控制器/远程服务实现来决定是否需要担心特定细节或者仅将问题视为一般错误。
回答
正如Cheekysoft所建议的那样,我倾向于将所有主要的异常都移到ExceptionHandler中,然后让这些异常冒出来。 ExceptionHandler将为异常类型呈现适当的视图。
但是,任何验证异常都应在视图中处理,但是通常,此逻辑对于应用程序的许多部分都是通用的。所以我喜欢有这样的帮手
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; } }
然后,在致电服务时,只需执行以下操作
var errors = Try.This( () => { // call your service here tasks.CreateMember( ... ); } );
然后在错误中是空的,我们很好。
我们可以更进一步,并使用可处理罕见异常的定制异常处理程序对其进行扩展。
回答
回答后续问题:
至于创建变得乏味的异常,我们已经习惯了。使用好的代码生成器或者模板可以在大约5或者10秒内以最少的手工编辑来创建异常类。
但是,在许多实际的应用程序中,错误处理可以完成70%的工作,因此,这实际上只是游戏的一部分。
正如tgmdbm所建议的那样,在MVC / MVP应用程序中,我让所有无法处理的异常冒泡到顶部,并被委托给ExceptionHandler的调度程序捕获。我对其进行了设置,使其使用在配置文件中查找的ExceptionResolver来选择合适的视图来显示用户。 Java的Spring MVC库可以很好地做到这一点。这是Spring MVC的Java / Spring异常解析器的配置文件的片段,但是我们会明白的。
这完全消除了演示者/控制器的大量异常处理。
<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>