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

3
规则是控制器不应具有业务逻辑,而应将其委托给服务。但是当我们这样做时,我们无法处理所有可能的情况并返回适当的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);
}

我需要进一步解释为什么这是不好的吗?
2个回答

1

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

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

我不是很喜欢这个,因为它不够通用,而且我们有空值。 - Scarass

0

我刚刚做的是创建了一个新类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;
    }
}

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

现在我会把业务逻辑分成不同的领域,这样服务(或facades)最终就不再有 Api 前缀,以便区分,例如 CarService。最好这些服务不知道与 API 响应、状态等相关的任何内容。具体实现方式由你自己决定。


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