【问题标题】:Separation of validator and service with external API calls验证器和服务与外部 API 调用的分离
【发布时间】:2015-08-06 21:17:42
【问题描述】:

我目前正在构建一个 Web 应用程序,并尝试按照良好的 MVC 和面向服务的架构来设计它。

然而,我在连接表示层(即我的控制器)和后端服务时遇到了一些困难,同时仍然保持良好的错误/验证报告给用户。

我阅读了一篇非常好的 SO 帖子 here,介绍了如何将验证逻辑与服务层分离,并且在大多数情况下,这一切都是有道理的。 然而,如果你可以这么说的话,有一个“缺陷”,在这个模型中让我很恼火:在查找验证器和服务都需要的对象时,如何避免重复工作?

我认为用一个相当简单的例子来解释会更容易:

假设我有一个允许用户共享代码 sn-ps 的应用程序。现在,我决定添加一个新功能,允许用户将他们的 GitHub 帐户附加到他们在我网站上的帐户(即建立个人资料)。 出于本示例的目的,我将简单地假设我的所有用户都是值得信赖的,并且只会尝试添加他们自己的 GitHub 帐户,而不是其他任何人的 :)

根据上述 SO 文章,我设置了一个基本的 GitHub 服务来检索 GitHub 用户信息。

interface IGitHubUserService {
    GitHubUser FindByUserName(string username);
}

GitHubUserService 的具体实现对https://api.github.com/users/{0} 进行了昂贵的调用,以提取用户信息。 同样,按照本文的模型,我实现了以下命令来将用户帐户链接到 GitHub 用户:

// Command for linking a GitHub account to an internal user account
public class GitHubLinkCommand {
    public int UserId { get; set; }
    public string GitHubUsername { get; set }
};

我的验证器需要验证用户输入的用户名是有效的 GitHub 帐户。这非常简单:在GitHubUserService 上调用FindByUserName 并确保结果不为空:

public sealed class GitHubLinkCommandValidator : Validator<GitHubLinkCommand> {
    private readonly IGitHubUserService _userService;

    public GitHubLinkCommandValidator(IGitHubUserService userService) {
        this._userService = userService;
    }

    protected override IEnumerable<ValidationResult> Validate(GitHubLinkCommand command) {
        try {
            var user = this._userService.FindByUserName(command.GitHubUsername);
            if (user == null)
                yield return new ValidationResult("Username", string.Format("No user with the name '{0}' found on GitHub's servers."));
        }
        catch(Exception e) {
            yield return new ValidationResult("Username", "There was an error contacting GitHub's API.");
        }
    }
}

好的,那太好了!验证器非常简单且有意义。现在是时候制作GitHubLinkCommandHandler

public class GitHubLinkCommandHandler : ICommandHandler<GitHubLinkCommand>
{
    private readonly IGitHubUserService _userService;

    public GitHubLinkCommandHandler(IGitHubUserService userService)
    {
        this._userService = userService;
    }

    public void Handle(GitHubLinkCommand command)
    {
        // Get the user details from GitHub:
        var user = this._userService.FindByUserName(command.GitHubUsername);

        // implementation of this entity isn't really relevant, just assume it's a persistent entity to be stored in a backing database
        var entity = new GitHubUserEntity
        {
            Name = user.Login,
            AvatarUrl = user.AvatarUrl
            // etc.
        };

        // store the entity:
        this._someRepository.Save(entity);
    }
}

同样,这看起来非常简洁明了。但是有一个明显的问题:对IGitHubUserService::FindByUserName 的重复调用,一个来自验证器,一个来自服务。 在糟糕的日子里,如果没有服务器端缓存,这样的调用可能需要 1-2 秒,这使得复制成本太高,无法使用这种架构模型。

有没有其他人在围绕外部 API 编写验证器/服务时遇到过这样的问题?除了在具体类中实现缓存之外,您是如何减少重复工作的?

【问题讨论】:

  • 我通常会将IGitHubUserService 与我的应用程序分离。我会在那里放置一个缓存,即使它经常像一个代理(带有一些装饰)它也可能成为一个适配器(如果GitHub接口更改)甚至是一个Bridge(如果你想让它足够通用,也可以与CodePlex、Google Code...一起使用)
  • @AdrianoRepetti 这几乎是我想出的唯一解决方案,但它并不是真正的“解决方案”。我可以看到它适用于一个大型项目,但对于像示例中的这样一个简单实现的功能来说,这似乎是一笔巨大的投资。
  • 带有缓存的 IGitHubService 代理并不比 GitHubLinkCommandHandler 多多少行代码(假设您只需要公开 FindUserByName)但是是的,我同意如果有 something i> 自动化这个样板代码(就像我们对 AOP 所做的那样)。
  • 据我所知,您有 2 个选项缓存选项,是一个烦人的样板。但也许更好的选择是不为验证器设置单独的类。或者更确切地说,验证器只处理琐碎的问题,发送空值等。但是对于任何需要在句柄方法中进行验证的实际时间......这很烦人,但在可读性方面可能是最好的选择代码和效率?
  • @DanielSlater 我同意。我目前这样做的方法是完全建立它;一个“输入验证器”(基本上只是验证用户请求的基本要素是否符合模型的示意图;可为空的字段、数字字段等),然后在我的服务运行操作时将业务层验证传递给我的服务.我遇到的主要问题是提出了一种将错误暴露给调用者(即控制器)的正确方法,这就是我调查我在原始问题中提到的 SO 文章的原因。越来越混乱了!

标签: c# asp.net-mvc service-layer


【解决方案1】:

在我看来,问题在于 LinkCommandHandler 和 LinkCommandValidator 都不应该首先检索 GitHub 用户。如果从单一职责原则的角度考虑,Validator 有一个工作来验证用户的存在,而 LinkCommandHanlder 有一个工作来将实体加载到存储库中。他们都不应该负责从 GitHub 中提取实体/用户。

我喜欢按照以下模式构建我的代码,每个模式代表一个归属层。每一层都可以与上层和下层对话,但不能跳过层。

  1. 数据层——它代表一个数据源,例如数据库或服务,通常您不需要为此编写代码,您只需使用它。
  2. 访问层 -- 这表示与数据层交互的代码
  3. Peristence Layer -- 这表示为访问层调用做好准备的代码,例如数据转换、从数据构建实体,或将访问层的多个调用分组到单个请求中以检索数据或存储数据。此外,缓存的决定以及缓存和清除缓存的机制将驻留在这一层。
  4. 处理器层 - 这表示执行业务逻辑的代码。这也是您可以使用验证器、其他处理器、解析器等的地方。

然后我将上述所有内容与我的表示层分开。这个概念是核心代码和功能不应该知道它是从网站、桌面应用程序还是 WCF 服务中使用的。

因此,在您的示例中,我将有一个 GitHubLinkProcessor 对象,一个名为 LinkUser(字符串用户名)的方法。在该类中,我将实例化我的 GitHubPeristenceLayer 类并调用它的 FindUserByName(string username) 方法。接下来,我们继续实例化一个 GitHubUserValidator 类来验证用户不为空并且所有必要的数据都存在。通过一个验证,实例化一个 LinkRepositoryPersistence 对象并将 GitHubUser 传递给 AccessLayer 以进行持久性。

但我想强烈指出,这正是我要做的方式,我绝不想暗示其他方法不太有效

编辑:

我想要一个简单的答案,因为我担心我的回答已经太长太无聊了。 =)我要在这里分裂一会儿,所以请多多包涵。对我来说,您不是通过调用 Git 来验证用户。您正在检查是否存在远程资源,该资源可能会失败,也可能不会失败。一个类比可能是您可以验证 (800) 555-1212 是美国电话号码的有效格式,但不能验证电话号码存在并且属于正确的人。那是一个单独的过程。就像我说的那样,这很麻烦,但这样做可以实现我所描述的整体代码模式。

所以让我们假设您的本地用户对象有一个不能为空的 UserName 和 Email 属性。您将对这些进行验证,并且仅在验证正确时才继续检查资源。

public class User 
{
    public string UserName { get; set; }
    public string Email { get; set; }

    //git related properties
    public string Login { get; set; }
    public string AvataUrl { get; set; }
}

//A processor class to model the process of linking a local system user
//to a remote GitHub User
public class GitHubLinkProcessor()
{
    public int LinkUser(string userName, string email, string gitLogin) 
    {
            //first create our local user instance
            var myUser = new LocalNamespace.User { UserName = userName, Email = email };

        var validator = new UserValidator(myUser);
        if (!validator.Validate())
            throw new Exception("Invalid or missing user data!");

        var GitPersistence = new GitHubPersistence();

        var myGitUser = GitPersistence.FindByUserName(gitLogin);
        if (myGitUser == null)
            throw new Exception("User doesnt exist in Git!");

        myUser.Login = myGitUser.Login;
        myUser.AvatorUrl = myGitUser.AvatarUrl;

        //assuming your persistence layer is returning the Identity
        //for this user added to the database
        var userPersistence = new UserPersistence();
        return userPersistence.SaveLocalUser(myUser);

        }
}

public class UserValidator
{
    private LocalNamespace.User _user;

    public UserValidator(User user)
    {
        this._user = user;
    }

    public bool Validate()
    {
        if (String.IsNullOrEmpty(this._user.UserName) ||
            String.IsNullOrEmpty(this._user.Email))
        {
            return false;
        }
    }
}

【讨论】:

  • 理论是合理的,但我之前尝试过这样的解决方案,但它有一个严重的缺点:您总是检索 GitHub 用户,甚至在执行最基本的操作之前验证(即所有表单字段都存在吗?)。忽略缓存层,这是很多多余的工作。在需要向 GitHub 调用 API 之前,有很多“便宜”的验证可能会失败,而首先运行它会更有效率。
  • 我能看到的唯一方法(使用您的架构)是将验证过程分解为多个方法,以便GitHubLinkProcessor 仍然可以负责流程,同时仍然是能够在验证过程的早期失败。
  • 嗨,Jason,您的假设是正确的。这就是为什么我在答案中添加了更多内容以帮助澄清它。
【解决方案2】:

我将得到比 Peter Lange 更短的答案,但我认为这只是您的 UserCommandValidator 应该验证 UserCommand 是否有效,而不是 User。

将命令验证器视为权宜之计,以确保您永远不会花费 1-2 秒在 github api 上查找空白用户名或包含无效字符的用户名。一旦你确定命令本身是有效的,你就可以提交它。从那里您可以让用户自己或 UserValidator 决定用户是否有效。

您 100% 正确地认为重复是错误的,但是您在该部分中验证了错误的内容是一种代码味道。

【讨论】:

    猜你喜欢
    • 2021-01-19
    • 2019-01-08
    • 2013-05-23
    • 1970-01-01
    • 2019-01-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多