Как избежать случая переключения при реализации функциональности, аналогичной сортировке сообщений reddits

#c# #design-patterns #switch-statement

#c# #шаблоны проектирования #switch-statement

Вопрос:

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

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

Комментарии:

1. Вопрос не совсем ясен. Не могли бы вы отредактировать свой пост и добавить пример кода, показывающий, case/switch чего вы пытаетесь избежать? Тогда нам было бы просто рассказать вам, как его заменить.

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

3. Пожалуйста, добавьте пример. Фрагмент кода стоит тысячи слов. Мне нужно посмотреть, как вы намеревались предоставить аргументы конструкторам, если бы вы использовали switch / case. Поступает ли значение для startDate , например, из пользовательского элемента управления или откуда-то еще?

4. Добавленный пример и начальные данные будут от пользователя, и я, вероятно, буду использовать какое-нибудь перечисление или что-то еще

Ответ №1:

Основная проблема здесь заключается в том, что пользовательский интерфейс должен переводить потребности пользователя в машинный формат — в данном случае, в одну из ваших стратегий сортировки. Следовательно, где-то какой-то код должен просматривать входные данные пользователя и решать, возможно, с 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 тогда и только тогда, когда он применим к выбору пользователя.

Ответ №2:

Насколько я понимаю, проблема в том, что 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;
    }
}
  

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

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

Комментарии:

1. Я думал использовать какую-нибудь фабрику, чтобы получить правильную стратегию, а затем передать ее в контекст шаблона стратегии примерно так: var strategy = StrategyFactory. GetStrategy(sortType); а затем передайте его контексту стратегии и вызовите Order() Я добавил пример выше для проблемы с переключением, которую я пытаюсь избежать

2. Интересно, это плохая практика — использовать ваш код с фабрикой, которая проверяет, реализует ли стратегия BaseTimeDependentPostSortingStrategy, и если это так, вводит datetime?