将业务逻辑与控制器解耦的最佳方式

Posted

技术标签:

【中文标题】将业务逻辑与控制器解耦的最佳方式【英文标题】:The optimal way to decouple business logic from controller 【发布时间】:2019-01-22 10:04:39 【问题描述】:

规则是控制器不应该有业务逻辑,而是应该将其委托给服务。但是当我们这样做时,我们无法处理所有可能的情况并返回适当的 HTTP 响应。

让我们看一个例子。假设我们正在构建某种社交网络,我们需要创建一个端点来对帖子进行评分(喜欢或不喜欢)。

首先让我们看一个将逻辑委托给服务的示例,这是我们的控制器操作:

public IActionResult Rate(long postId, RatingType ratingType)

    var user = GetCurrentUser();
    PostRating newPostRating = _postsService.Rate(postId, ratingType, user);
    return Created(newPostRating);

你觉得这有问题吗?如果没有给定 id 的帖子,我们将如何返回未找到的响应?如果用户无权评价帖子,我们将如何返回禁止回复?

PostsService.Rate只能返回一个新的PostRating,但是其他情况呢?好吧,我们可以抛出异常,我们需要创建很多自定义异常,以便我们可以将它们映射到适当的 HTTP 响应。我不喜欢为此使用异常,我认为有更好的方法来处理这些情况而不是异常。因为我认为帖子不存在和用户没有权限的情况根本不例外,它们只是正常情况,就像成功评分一样。

我的建议是在控制器中处理该逻辑。因为在我看来,无论如何这应该是控制器的责任,在提交操作之前检查所有权限。所以我会这样做:

public IActionResult Rate(long postId, RatingType ratingType)

    var user = GetCurrentUser();
    var post = _postsRepository.GetByIdWithRatings(postId);

    if (post == null)
        return NotFound();

    if (!_permissionService.CanRate(user, post))
        return Forbidden();

    PostRating newPostRating = new PostRating 
    
        Post = post,
        Author = user,
        Type = ratingType
    ;

    _postRatingsRepository.Save(newPostRating);

    return Created(newPostRating);

这是我认为应该这样做的方式,但我敢打赌,有人会说这对控制器来说逻辑太多,或者你不应该在其中使用存储库。

如果您不喜欢在控制器中使用存储库而不是在哪里放置获取或保存帖子的方法?在服务中?所以会有PostsService.GetByIdWithRatingsPostsService.Save 只会调用PostsRepository.GetByIdWithRatingsPostsRepository.Save。这是不必要的,只会导致样板代码。

更新: 也许有人会说使用 PostsService 检查权限,然后调用 PostsService.Rate。这很糟糕,因为它涉及到更多不必要的数据库访问。例如,它可能是这样的:

public IActionResult Rate(long postId, RatingType ratingType)

    var user = GetCurrentUser();

    if(_postsService.Exists(postId))
         return NotFound();

    if(!_postsService.CanUserRate(user, postId))        
         return Forbidden();

    PostRating newPostRating = _postsService.Rate(postId, ratingType, user);
    return Created(newPostRating);

我还需要进一步解释为什么这很糟糕吗?

【问题讨论】:

【参考方案1】:

我(刚刚)所做的是创建新类ApiResult

public class ApiResult

    public int StatusCode  get; private set;  = 200;
    public string RouteName  get; private set; 
    public object RouteValues  get; private set; 
    public object Content  get; private set; 

    public void Ok(object content = null)
    
        this.StatusCode = 200;
        this.Content = content;
    

    public void Created(string routeName, object routeValues, object content)
    
        this.StatusCode = 201;
        this.RouteName = routeName;
        this.RouteValues = routeValues;
        this.Content = content;
    

    public void BadRequest(object content = null)
    
        this.StatusCode = 400;
        this.Content = content;
    

    public void NotFound(object content = null)
    
        this.StatusCode = 404;
        this.Content = content;
    

    public void InternalServerError(object content = null)
    
        this.StatusCode = 500;
        this.Content = content;
    

还有一个带有单一方法的控制器基类TranslateApiResult

public abstract class CommonControllerBase : ControllerBase

    protected IActionResult TranslateApiResult(ApiResult result)
    
        if (result.StatusCode == 201)
        
            return CreatedAtAction(result.RouteName, result.RouteValues, result.Content);
        
        else
        
            return StatusCode(result.StatusCode, result.Content);
        
    

现在我在控制器中:

[ApiController]
[Route("[controller]/[action]")]
public class MyController : CommonControllerBase

    private readonly IMyApiServcie _service;

    public MyController (
        IMyApiServcie service)
    
        _service = service;
    

    [HttpGet]
    public async Task<IActionResult> GetData()
    
        return TranslateApiResult(await _service.GetData());
    

在您的服务中注入存储库和其他依赖项:

public class MyApiServcie : IMyApiServcie 

    public async Task<ApiResult> GetData()
    
        var result = new ApiResult();
        // do something here
        result.Ok("success");
        return result;
    

现在,Api 前缀在 Service 之前的原因是,该服务并不是包含所有逻辑的最终服务。

此时,我会将业务逻辑拆分为不同的域,以便服务(或 facades)最终没有 Api 前缀,只是为了区分 CarService。这些服务最好不知道任何与 API 响应、状态等相关的信息。不过,如何实现它取决于您。

【讨论】:

【参考方案2】:

有很多方法可以处理这个问题,但最接近“最佳实践”的方法可能是使用结果类。例如,如果您的服务方法创建了一个评级,然后返回它创建的评级,那么您将返回一个封装评级以及其他相关信息(如成功状态、错误消息(如果有)等)的对象。

public class RateResult

    public bool Succeeded  get; internal set; 
    public PostRating PostRating  get; internal set; 
    public string[] Errors  get; internal set; 

然后,您的控制器代码将变为:

public IActionResult Rate(long postId, RatingType ratingType)

    var user = GetCurrentUser();
    var result = _postsService.Rate(postId, ratingType, user);
    if (result.Succeeded)
    
        return Created(result.PostRating);
    
    else
    
        // handle errors
    

【讨论】:

我不太喜欢这个,因为它不是通用的,而且我们有空值。

以上是关于将业务逻辑与控制器解耦的最佳方式的主要内容,如果未能解决你的问题,请参考以下文章

基于电商业务中台最佳实践:总体架构介绍与交易业务中台核心设计

消息队列最佳实践消息恰好被消费一次

将业务与表示逻辑分开的最佳方法?

使用存储库时,ASP.NET MVC 中业务逻辑的最佳位置是啥?

Spring学习整理

Spring学习,初识Spring