Разделение валидатора и услуги с внешними вызовами API

В настоящее время я создаю веб-приложение и пытаюсь его спроектировать с помощью хорошей MVC и сервис-ориентированной архитектуры.

Однако я ударил немного стены при подключении уровня представления (например, моих контроллеров) и внутренних служб, сохраняя при этом хорошую отчетность об ошибках/валидации обратно пользователю.

Я прочитал действительно хорошую публикацию SO здесь о том, как отделить логику проверки от уровня сервиса и большая часть все это имело смысл. Однако был один "недостаток", если можно так выразиться, в этой модели, которая на меня набросилась: как избежать дублирования усилий при поиске объектов, требуемых как валидатором, так и службой?

Я думаю, что было бы проще объяснить с помощью достаточно простого примера:

Скажем, у меня есть приложение, которое позволяет пользователям обмениваться фрагментами кода. Теперь я решил добавить новую функцию, которая позволяет пользователю присоединить свою учетную запись 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. Это очень просто: вызовите FindByUserName в GitHubUserService и убедитесь, что результат не равен null:

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 servers."));
        }
        catch(Exception e) {
            yield return new ValidationResult("Username", "There was an error contacting GitHub 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 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-интерфейсов и как вы уменьшили дублирование усилий за пределами реализации кеша в своем конкретном классе?

Ответ 1

С моей точки зрения, проблема в том, что ни LinkCommandHandler, ни LinkCommandValidator не должны извлекать пользователя GitHub в первую очередь. Если вы думаете о принципах единой ответственности, валидатор имеет одно задание, чтобы проверить существование пользователя, а LinkCommandHanlder имеет одно задание для загрузки объекта в репозиторий. Ни у кого из них не должно быть работы по вытаскиванию Entity/User из GitHub.

Мне нравится структурировать свой код в следующем шаблоне, каждый из которых представляет собой атрибутивный слой. Каждый слой может говорить со слоем выше и ниже, но он не может пропустить слой.

  • Уровень данных - это источник данных, такой как база данных или служба, обычно вы не пишете код для этого, вы просто потребляете его.
  • Access Layer - представляет собой код для взаимодействия с datalayer
  • Peristence Layer - это код, позволяющий получать элементы, готовые для вызова уровня доступа, такие как перенос данных, создание объектов из данных или группирование нескольких вызовов на уровень доступа в один запрос для извлечения данных или хранения данные. Кроме того, решение о кешировании и механизмы кэширования и очистки кэша будут находиться на этом уровне.
  • Processor Layer - это код, который выполняет бизнес-логику. Также вы можете использовать валидаторы, другие процессоры, парсеры и т.д.

И затем я сохраняю все вышеописанное отдельно от своего уровня представления. Концепция заключается в том, что основной код и функциональность не должны знать, используется ли он с веб-сайта или настольного приложения или службы WCF.

Итак, в вашем примере у меня будет объект GitHubLinkProcessor метод LinkUser (имя пользователя строки). Внутри этого класса я бы создавал экземпляр класса GitHubPeristenceLayer и вызывал его метод FindUserByName (string username). Затем мы переходим к созданию экземпляра класса GitHubUserValidator для проверки того, что пользователь не является нулевым, и все необходимые данные присутствуют. Проводится одна проверка, создается объект LinkRepositoryPersistence и передается GitHubUser для сохранения в AccessLayer.

Но я хочу особо отметить, что это именно так, как я бы это сделал, и ни в коем случае я не хочу подразумевать, что другие методологии менее справедливы.

EDIT:

Я собирался сделать простой ответ, потому что боялся, что мой ответ уже слишком длинный и скучный. =) Я собираюсь разбить волосы здесь на мгновение, поэтому, пожалуйста, несите меня. Для меня вы не проверяете пользователя, вызывая Git. Вы проверяете наличие удаленного ресурса, который может или не может потерпеть неудачу. Аналогом может быть то, что вы можете подтвердить, что (800) 555-1212 является допустимым форматом для американского номера телефона, но не того, что номер телефона существует и принадлежит правильному человеку. Это отдельный процесс. Как я уже сказал, это расщепление волос, но тем самым он позволяет описать общий код кода.

Итак, пусть ваш локальный пользовательский объект имеет свойство для UserName и Email, которое не может быть null. Вы бы подтвердили это и только перейдете к проверке ресурса, если эта проверка была правильной.

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