如何使用MVP模式将服务层消息/错误传递给更高层?

18

我目前正在从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无法预见。
目前,如果事情没有按计划进行,我让服务层抛出异常。这是一个足够的策略吗?这段代码对你们来说有异味吗?如果我像这样编写服务层,您会因不得不编写以这种方式使用它的Presenter而感到烦恼吗?返回代码似乎太老套了,而布尔值则不够信息化。

非原作者编辑:将原作者的后续评论合并到答案中


Cheekysoft,我喜欢ServiceLayerException的概念。我已经为我不预料到的异常设置了全局异常模块。你觉得创建所有这些自定义异常很繁琐吗?我想捕获基本的Exception类有点不好,但不确定如何进一步处理。
tgmdbm,我喜欢那里聪明地使用lambda表达式!
感谢Cheekysoft的跟进。所以我猜如果你不介意用户显示一个单独的页面(我主要是Web开发人员),那么这将是策略,如果异常未被处理。

但是,如果我想在提交导致错误的数据的同一视图中返回错误消息,则必须在Presenter中捕获异常?

当Presenter处理ServiceLayerException时,CreateUserView的外观如下:

Create a user

对于这种错误,最好将其报告给同一视图。
无论如何,我认为我们现在已经超出了我的原始问题的范围。我会尝试使用你发布的内容,如果我需要进一步的细节,我会发布一个新问题。
3个回答

16

这对我来说听起来很合适。异常是首选的,因为无论服务方法实现嵌套多深,都可以从服务层内的任何位置抛出到服务层的顶部。这使得服务代码保持整洁,因为您知道调用者将始终收到问题的通知。

不要捕获Exception

然而,在presenter中不要捕获Exception,虽然它很诱人,因为它可以使代码更短,但你需要捕获特定的异常,以避免捕获系统级别的异常。

计划一个简单的异常层次结构

如果要以这种方式使用异常,应该为自己的异常类设计一个异常层次结构。至少创建一个ServiceLayerException类,并在服务方法中抛出其中之一当发生问题时。然后,如果需要抛出应该/可以由presenter以不同方式处理的异常,可以抛出ServiceLayerException的特定子类:例如AccountAlreadyExistsException。

然后您的presenter就有选择的余地了。

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.

在你自己的异常类中使用继承可以避免在 presenter 中捕获多个异常(如果需要,也可以这样做),并且不会意外地捕获无法处理的异常。如果你的 presenter 已经处于调用栈的顶部,则添加一个 catch(Exception) 块来处理系统错误并使用不同的视图。

我总是尝试将我的服务层视为一个独立的可分发库,并抛出尽可能具体的异常。然后由 presenter/controller/remote-service 实现决定是否需要关注特定的细节,或者只将问题视为一般性错误。


3
如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( ... );
} );

如果错误为空,则说明一切正常。

您还可以进一步扩展它,并使用自定义异常处理程序来处理不常见的异常。


1

回复跟进问题:

关于创建异常变得繁琐,你会逐渐习惯。使用好的代码生成器或模板可以在大约5到10秒内创建异常类,最小化手动编辑。

然而,在许多实际应用中,错误处理可能占据70%的工作量,所以这只是游戏的一部分。

正如tgmdbm所建议的,在MVC/MVP应用程序中,我让所有无法处理的异常上升到顶部,并由调度程序捕获,然后委托给ExceptionHandler。我设置它使用一个ExceptionResolver,该解析器查找配置文件以选择适当的视图来向用户显示。Java的Spring MVC库做得非常好。以下是Spring MVC异常解析器的配置文件片段-它适用于Java/Spring,但您会明白的。

这将完全从您的Presenter/Controller中删除大量的异常处理。

<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>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接