Как избежать случая переключения при реализации функциональности, подобной сортировке постов reddits - PullRequest
1 голос
/ 06 апреля 2019

У меня есть интерфейс ISortPostsStrategy для всех стратегий сортировки (SortPostsByTop, SortPostsByBest, SortPostsByNew). Мне нужна сортировка с возможностью выбора таймфрейма для всех типов сортировки без одного, и я попытался использовать шаблон стратегии, но проблема в том, что в ISortPostsStrategy есть параметр timeframe, который SortPostByнужно, и это в конечном итоге не используется.

public interface ISortPostsStrategy
{
    Task<IEnumerable<Post>> SortAsync(string userId, DateTime startDate);
}


public class SortPostsByNew : ISortPostsStrategy
{
    private readonly UnitOfWork unitOfWork;

    public SortPostsByNew(UnitOfWork unitOfWork)
    {
        this.unitOfWork = unitOfWork;
    }

    public async Task<IEnumerable<Post>> SortAsync(string userId, DateTime startDate)
    {
        var dbPosts = await this.unitOfWork.Posts.GetBySubcribedUserOrderedByNewAsync(userId);
        return dbPosts;
    }
}

public class SortPostsByBest : ISortPostsStrategy
{
    private readonly UnitOfWork unitOfWork;

    public SortPostsByBest(UnitOfWork unitOfWork)
    {
        this.unitOfWork = unitOfWork;
    }

    public async Task<IEnumerable<Post>> SortAsync(string userId, DateTime startDate)
    {
        var dbPosts = await this.unitOfWork.Posts.GetBySubscribedUserOrderedByBestAsync(userId, startDate);
        return dbPosts;
    }
}

Это случай переключения, которого я пытаюсь избежать, прежде чем стратегия паттерна

IEnumerable<Post> dbPosts = null;

        if (sortType == PostSortType.New)
        {
            dbPosts = await this.redditCloneUnitOfWork.Posts
                .GetBySubcribedUserOrderedByNewAsync(dbUser.Id);
        }
        else if (sortType == PostSortType.Top)
        {
            dbPosts = await this.redditCloneUnitOfWork.Posts
                .GetBySubcribedUserOrderedByTopAsync(dbUser.Id, startDate);
        }
        else if (sortType == PostSortType.Controversial)
        {
            dbPosts = await this.redditCloneUnitOfWork.Posts
                .GetBySubscribedUserOrderedByControversialAsync(dbUser.Id, startDate);
        }
        else if (sortType == PostSortType.Best)
        {
            dbPosts = await this.redditCloneUnitOfWork.Posts
                   .GetBySubscribedUserOrderedByBestAsync(dbUser.Id, startDate);
        }

        var models = this.mapper.Map<IEnumerable<PostConciseViewModel>>(dbPosts);

Ответы [ 2 ]

0 голосов
/ 08 апреля 2019

Фундаментальная проблема здесь заключается в том, что пользовательский интерфейс должен переводить потребности пользователя в удобный для машин формат - в данном случае, одну из ваших стратегий сортировки. Следовательно, некоторый код где-то должен посмотреть на вводимые пользователем данные и решить, возможно, с помощью предложения if, какой логический путь выбрать и какой тип стратегии создать. Этот аспект программирования пользовательского интерфейса неизбежен.

Самое безопасное и разумное место для размещения такого кода - это как можно ближе к элементам управления пользовательского интерфейса. Для этого есть несколько причин:

  1. Размещая код перевода в самом модуле пользовательского интерфейса, вы избегаете других зависимостей, формирующих код, от него.

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

  3. Уродство кода имеет смысл только в контексте дизайна пользовательского интерфейса, поэтому их объединение облегчает будущим разработчикам понимание происходящего.

Что касается шаблонов проектирования, то это одна из немногих областей, где я бы позволил разработчикам писать код, который выглядит немного спагетти-иш. Реальность требований заключается в том, что дизайн должен быть оптимизирован с учетом человеческого фактора, а не факторов машины и архитектурной элегантности. Другими словами, использование switch/case - это нормально.

При этом размещение switch/case вокруг различных вызовов для извлечения данных проблематично, поскольку обычно в этих вызовах есть логика, которую вы не хотели бы дублировать. Таким образом, шаблон стратегии не плохой. Я бы предложил фабричный шаблон, который может возвращать стратегию, учитывая серию пользовательских входных данных, возможно передаваемых в DTO.

На высоком уровне это может выглядеть так:

var dto = new GetSortedPostsRequestDto
{
    StartDate = inputForm.StartDatePicker.SelectedDate,
    SortMode = inputForm.GetSelectedSortMode()
};
var strategy = GetSortStrategy(dto);
var results = unitOfWork.GetSortedPosts(strategy);

Этот подход особенно эффективен, когда DTO приходит извне вашего кода на C #, например. если он был собран Javascript и отправлен через вызов REST.

Метод GetSortStrategy() - это место, где будет существовать ваш case/switch. Хотя фабричный класс не обязательно исключен, его обычно можно записать как не универсальный фабричный метод.

ISortStrategy GetSortStrategy(GetSortedPostsRequestDto dto)
{
    switch case dto.SortMode
    {
        case SortMode.New: return new SortByNewStrategy();
        case SortMode.Top: return new SortByTopStrategy();
        case SortMode.Recent: return new SortByRecentStrategy(dto.StartDate);
        //etc.
    }
}

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

0 голосов
/ 06 апреля 2019

Насколько я понимаю, проблема в том, что DateTime - это деталь реализации одной (или некоторых) конкретной стратегии (ий). Поэтому интерфейс высокого уровня не должен иметь эту информацию. Однако, если вам все еще нужен этот DateTime для нескольких стратегий, вы можете создать BaseTimeDependentPostSortingStrategy (или любое другое имя по вашему выбору).

Вы используете внедрение зависимостей для предоставления DateTime в методе. Вероятно, вы можете внедрить в Конструктор типы, которым вместо этого требуется временной интервал.

public interface ISortPostsStrategy
{
    Task<IEnumerable<Post>> SortAsync(string userId);
}

public abstract class BaseTimeDependentPostSortingStrategy : ISortPostsStrategy
{
    private readonly DateTime _startDate;

    protected BaseTimeDependentPostSortingStrategy(DateTime startDate)
    {
        _startDate = startDate;
    }

    public abstract Task<IEnumerable<Post>> SortAsync(string userId);
}

public class SortPostsByNew : ISortPostsStrategy
{
    private readonly UnitOfWork unitOfWork;

    public SortPostsByNew(UnitOfWork unitOfWork)
    {
        this.unitOfWork = unitOfWork;
    }

    public async Task<IEnumerable<Post>> SortAsync(string userId)
    {
        var dbPosts = await this.unitOfWork.Posts.GetBySubcribedUserOrderedByNewAsync(userId);
        return dbPosts;
    }
}

public class SortPostsByBest : BaseTimeDependentPostSortingStrategy 
{
    private readonly UnitOfWork unitOfWork;

    public SortPostsByBest(UnitOfWork unitOfWork, DateTime startDate) : base(startDate)
    {
        this.unitOfWork = unitOfWork;
    }

    public async Task<IEnumerable<Post>> SortAsync(string userId)
    {
        var dbPosts = await this.unitOfWork.Posts.GetBySubscribedUserOrderedByBestAsync(userId, _startDate);
        return dbPosts;
    }
}

Отказ от ответственности: Это может не скомпилироваться, я не проверял это.

Если это не отвечает на ваш вопрос, предоставьте дополнительную информацию, чтобы я мог помочь.

...