Стратегия

Стратегия

Дмитрий Бахтенков

Паттерн "Стратегия", несомненно, является самым главным поведенческим паттерном. Многие его используют, даже не подозревая об этом, а ещё он лежит в основе некоторых других паттернов проектирования. Например можно сказать, что фабричный метод - это стратегия порождения объектов.


Когда и зачем применять?

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

Случай 0:

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

Интересная ситуация 1:

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

Реальный кейс 2:

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

В чём суть?

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

interface IStrategy
{
    void Execute();
}

Затем нужно написать столько реализаций интерфейса, сколько у вас есть вариантов использования стратегии.

class FirstStrategy : IStrategy
{
    public void Execute()
    {
     // логика первого алгоритма 
    }
}

class SecondStrategy : IStrategy
{
    public void Execute()
    {
     // логика второго алгоритма 
    }
}

Затем, с помощью отношения агрегации мы можем использовать нашу стратегию. Класс, который это делает, назовём контекстом стратегии, или "StrategyContext":

class StrategyContext
{
    private IStrategy _strategy;

    public StrategyContext(IStrategy strategy)
    {
       _strategy = strategy;
    }

    public void ExecuteContext
    {
     // какая-то логика
     // вызываем метод нашей стратегии
     _strategy.Execute();
     // снова какая-то логика
    }
}

На диаграмме классов UML это будет выглядеть следующим образом:

Стратегия на UML

А может лучше реальный пример?

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

Задача

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

Выделение сущностей

Основная сущность, которая будет выполнять математические операции - Calculator. У калькулятора будет 4 метода: сложить, вычесть, разделить, умножить.

Ещё у нас есть два логгера, одним из которых будет пользоваться наш калькулятор.

Ну и метод Main в классе Program - будет обрабатывать пользовательский ввод и вызывать действия калькулятора.

Проектирование

Проектирование калькулятора с логгером

Стратегия будет заключаться в том, что в конструктор класса Calculator мы будем подставлять одну из реализаций интерфейса ILogger, а методы калькулятора будут вызывать методы логгера (процесс, когда один объект вызывает методы у другого объекта, называется посылкой сообщений).

Код

  • Определим интерфейс ILogger:
public interface ILogger
{
    public void LogError(string message);
    public void LogInfo(string message);
}
  • Определим реализацию консольного логгера:
public void LogError(string message)
{
    // балуемся с цветом текста в консоли, так как
    // логгирование должно быть красивым
    Console.ForegroundColor = ConsoleColor.Red;
    // выводим сообщение с типом события и текущей датой
    Console.Write($"{DateTime.Now} | ERROR: ");
    Console.ForegroundColor = ConsoleColor.White;
    Console.WriteLine(message);
}

public void LogInfo(string message)
{
    Console.ForegroundColor = ConsoleColor.Green;
    Console.Write($"{DateTime.Now} | INFO: ");
    Console.ForegroundColor = ConsoleColor.White;
    Console.WriteLine(message);
}
  • Определим реализацию файлового логгера:
public class FileLogger : ILogger
{
    private const string FileName = "log.txt";
    public void LogError(string message)
    {
        // открываем поток к файлу, чтобы добавить в него
        // сообщение о событии. Директива using сама его потом закроет.
        using (var stream = new StreamWriter(FileName, true))
        {
            var text = $"{DateTime.Now} | ERROR: {message}";
            stream.WriteLine(text);
        }
    }

    public void LogInfo(string message)
    {
        using (var stream = new StreamWriter(FileName, true))
        {
            var text = $"{DateTime.Now} | INFO: {message}";
            stream.WriteLine(text);
        }
    }
}
  • Теперь можно сделать калькулятор:
public class Calculator
{
    private readonly ILogger _logger;
    
    public Calculator(ILogger logger)
    {
        _logger = logger;
    }

    public int CalculateSum(int a, int b)
    {
        _logger.LogInfo($"{a} + {b} = {a+b}");
        return a + b;
    }

    public int CalculateSubtract(int a, int b)
    {
        _logger.LogInfo($"{a} - {b} = {a-b}");
        return a - b;
    }

    public int CalculateMultiple(int a, int b)
    {
        _logger.LogInfo($"{a} * {b} = {a*b}");
        return a * b;
    }
    
    public int CalculateDivide(int a, int b)
    {
        if (b == 0)
        {
            _logger.LogError($"Divide {a} by zero!!!");
            return 0;
        }
        
        _logger.LogInfo($"{a} / {b} = {a/b}");
        return a / b;
    }
}

Тут стоит сказать пару слов об интерфейсах. Мы можем спокойно приводить реализации интерфейса к самому типу интерфейса, это называется апкаст (upcast). Например:

ILogger logger = new ConsoleLogger().

  • Ну и сам метод Main:
static void Main(string[] args)
{
    Calculator calculator;
    Console.WriteLine("Введите 1 для консольного логгирования или нажмите Enter для использования файлового логгера");
    
    var input = Console.ReadLine();
    
    // инициализируем наш калькулятор необходимой реализацией логгера
    if (input == "1")
        calculator = new Calculator(new ConsoleLogger());
    else
        calculator = new Calculator(new FileLogger());
    
    Console.WriteLine("Введите первое число: ");
    var first = int.Parse(Console.ReadLine());
    
    Console.WriteLine("Введите второе число: ");
    var second = int.Parse(Console.ReadLine());

    Console.WriteLine("Введите действие: ");
    var action = Console.ReadLine();
    
    // проверяем введённое действие
    switch (action)
    {
        case "+":
            Console.WriteLine("Result: " + calculator.CalculateSum(first, second));
            break;
        case "-":
            Console.WriteLine("Result: " + calculator.CalculateSubtract(first, second));
            break;
        case "*":
            Console.WriteLine("Result: " + calculator.CalculateMultiple(first, second));
            break;
        case "/":
            Console.WriteLine("Result: " + calculator.CalculateDivide(first, second));
            break;
        default:
            Console.WriteLine("Некорректное действие!");
            break;
    }
}
  • Тестируем:
Консольный логгер без ошибок
Консольный логгер с ошибками
Логгирование в файл (который находится в папке bin/debug вашего проекта)
Непосредственно сам лог

Делаем выводы:

  1. Мы научились использовать паттерн "Стратегия". Это самый крутой поведенческий паттерн, так как в том или ином виде он используется очень часто, а также является основой для других паттернов, например - например, паттерн "Фабричный метод" можно считать просто стратегией порождения объектов
  2. Он используется, когда необходимо в процессе выполнения программы необходимо просто подменять различные реализации одного алгоритма.
  3. В его основе лежат такие отношения между классами и объектами, как агрегация и реализация.
  4. Апкаст (upcast) - процесс приведения конкретного типа к более абстрактному, такому как интерфейс.

__________________

Исходники: ГитХаб

С вами был FlexCode


Report Page