Паттерны проектирования в .NET за 5 минут — Стратегия(Strategy)

Привет. Я вновь решил затронуть тему паттернов проектирования. Почему? Недавно решил освежить свои знания, открыл свои старые статьи на эту тему, и оказалось, что они ужасны. На момент их написания у меня было гораздо меньше опыта и знаний, и в них не раскрыты многие темы, да и то что раскрыто, можно было бы сделать гораздо лучше. Начал искать на YouTube что-то подходящее, но ничего меня не устроило, так как мне хотелось найти что-то краткое, содержащее только выжимку материала, чтобы на теорию не уходило много времени, а лишь хотелось скорее приступить к практике. Поэтому я решил записать серию видео «Паттерны за 5 минут», которые бы соответствовали всему вышеперечисленному. Публиковать я их буду на моём YouTube канале и в данном блоге. Первым будет паттерн «Стратегия».

Стратегия(Strategy) — поведенческий шаблон проектирования. Стратегия инкапсулирует определённое поведение с возможностью его подмены.

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

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

Для решения подобных задач удобно использовать шаблон «Стратегия»

Для реализации стратегии в .NET есть 2 подхода – выделение интерфейса и использование делагата. В некоторых случаях оба этих подхода можно совмещать, отличным примером является стратегия сортировки, имеющая интерфейс IComparable и делегат Comparison.

Реализация через интерфейсы:

Интерфейс IReader:

interface IReader
{
    string GetValue(string datas);
}

Класс DataReader, который и будет выступать в качестве стратегии, для инкапсуляции используемого алгоритма получения данных. Метод класса принимает объект интерфейса IReader и строку, и возвращает результат метода GetValue из переданного объекта.

class DataReader
{
    public string GetValue(IReader reader, string datas)
    {
        return reader.GetValue(datas);
    }
}

Класс FileDataReader, который будет отвечать за чтение информации из файла и его обработку.

class FileDataReader: IReader
{
    public string GetValue(string datas)
    {
        if (string.IsNullOrEmpty(datas))
        {
            return String.Empty;
        }

        using (var fileReader = new StreamReader(datas))
        {
            var dataArray = fileReader.ReadToEnd().Trim().Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
            var resultValues = dataArray.Select((x, i) => $"{i + 1}: {x}");
            return string.Join("\n", resultValues);
        }
    }
}

Класс StringDataReader. Данный класс будет отвечать за получение данных из строк и их обработку.

class StringDataReader: IReader
{
    public string GetValue(string datas)
    {
        if (string.IsNullOrEmpty(datas))
        {
            return String.Empty;
        }
        var dataArray = datas.Trim().Split(new[] {'\n'}, StringSplitOptions.RemoveEmptyEntries);
        var resultValues = dataArray.Select((x, i) => $"{i+1}: {x}");
        return string.Join("\n", resultValues);

    }
}

Реализации «Стратегия», используя делегаты вместо интерфейсов.

Класс DataReader. В методе GetValue вместо передачи интерфейса, будем передавать функтор, который принимает и возвращает строку

class DataReader
{
    public string GetValue(Func<string, string> dataReader, string datas)
    {
        return dataReader.Invoke(datas);
    }
}

В классах FileDataReader и StringDataReader методы GetValue сделаем статическими, для более удобной их передачи при тестировании. Сами методы, при этом, меняться не будут.

Класс FileDataReader:

public static string GetValue(string datas)
{
    if (string.IsNullOrEmpty(datas))
    {
        return String.Empty;
    }

    using (var fileReader = new StreamReader(datas))
    {
        var dataArray = fileReader.ReadToEnd().Trim().Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
        var resultValues = dataArray.Select((x, i) => $"{i + 1}: {x}");
        return string.Join("\n", resultValues);
    }
}

Класс StringDataReader:

    class StringDataReader
    {
        public static string GetValue(string datas)
        {
            if (string.IsNullOrEmpty(datas))
            {
                return String.Empty;
            }
            var dataArray = datas.Trim().Split(new[] {'\n'}, StringSplitOptions.RemoveEmptyEntries);
            var resultValues = dataArray.Select((x, i) => $"{i+1}: {x}");
            return string.Join("\n", resultValues);

        }
    }

Работать всё будет аналогично реализации с интерфейсом.

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

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

В большинстве случаев вместо интерфейса будет удобнее использовать делегат, если стратегия содержит один метод. Но итоговая архитектура вашего приложения, в любом случае, должна зависеть от конкретной задачи.

Исходный код, показанный в примере.

Обязательно закрепите полученные знания на практике!

Приятного программирования.

Добавить комментарий