itmo_conspects

Лекция 9. Поведенческие паттерны. Воркшоп 4

На этом воркшопе будут рассматриваться поведенческие паттерны. Рассматриваемый код доступен в этом репозитории: https://github.com/is-oop-y27/workshop-4

Шаблонный метод

Шаблонный метод (Template Method) - паттерн, позволяющий подклассам переопределять поведение, не меняя сигнатуры метода

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

Пример: у нас есть куча сотрудников, у которых есть количество выполненных задач и количество отработанных часов, хотим иметь возможность делать сортировку по этим параметрам. Сделаем интерфейс IEmployeeEvaluator

public interface IEmployeeEvaluator
{
    Employee FindBestEmployee(IEnumerable<RatedEmployee> employees);
}

И абстрактный класс EmployeeEvaluatorBase:

public abstract class EmployeeEvaluatorBase : IEmployeeEvaluator
{
    public Employee FindBestEmployee(IEnumerable<RatedEmployee> employees)
    {
        IEnumerable<RatedEmployee> sorted = Sort(employees);
        return sorted.First().Employee;
    }

    protected abstract IEnumerable<RatedEmployee> Sort(
        IEnumerable<RatedEmployee> employees);
}

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

public class TaskEmployeeEvaluator : EmployeeEvaluatorBase
{
    protected override IEnumerable<RatedEmployee> Sort(
        IEnumerable<RatedEmployee> employees)
    {
        return employees.OrderByDescending(x => x.Rating.TaskCompletedCount);
    }
}

public class HoursEmployeeEvaluator : EmployeeEvaluatorBase
{
    protected override IEnumerable<RatedEmployee> Sort(IEnumerable<RatedEmployee> employees)
    {
        return employees.OrderByDescending(x => x.Rating.HoursWorked);
    }
}

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

При этом фабричный метод - паттерн порождающий, а шаблонный - поведенческий. Полный код примера с шаблонным методом - https://github.com/is-oop-y27/workshop-4/tree/master/src/1_TemplateMethod

Стратегия

Стратегия (Strategy) - паттерн, в котором схожие алгоритмы помещаются в классы, которые можно взаимозаменять во время исполнения программы

Проблема та же, что и с шаблонным методом - параметризуем задачу; отличие в том, что в шаблонном методе используем наследование, а в стратегии - композицию

Возьмем тот же пример: сортировка сотрудников. Здесь вынесем метод Sort в классы EmployeeSorter, которые будем передавать в EmployeeEvaluator:

var sorter = new TaskEmployeeSorter();

var evaluator = new EmployeeEvaluator(sorter);

Employee bestEmployee = evaluator.FindBestEmployee(ratedEmployees);

Помимо этого этот sorter можно использовать в двух или более местах.

В целом, стратегией можно называть любую выделенную абстракцию. Код стратегии: https://github.com/is-oop-y27/workshop-4/tree/master/src/2_Strategy

Цепочка обязанностей

Цепочка обязанностей (Chain of responsibility) - паттерн, позволяющий передавать запрос по цепи через обработчики, которые решают, что с запросом делать

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

Пример цепочки обязанностей - парсинг аргументов. Пускаем по цепочке обработчиков слово из командной строки: если это какое-то имя аргумента, начинающееся с дефиса, то парсим следующее слово, иначе передаем другому обработчику в цепочке:

public class OutputRunner : IOutputRunner
{
    private readonly IParameterHandler _handler;

    public OutputRunner(IParameterHandler handler)
    {
        _handler = handler;
    }

    public void Run(IEnumerable<string> args)
    {
        using IEnumerator<string> request = args.GetEnumerator();
        ITextModifier? modifier = null;

        while (request.MoveNext())
        {
            ITextModifier? nextModifier = _handler.Handle(request);

            if (nextModifier is not null)
            {
                modifier = new AggregateModifier(modifier, nextModifier);
            }
        }

        var text = "Hello world!";
        text = modifier?.Modify(text) ?? text;

        Console.WriteLine(text);
    }
}

В итоге, каждый обработчик ответственен за одну какую-то штуку. Код пример цепочки: https://github.com/is-oop-y27/workshop-4/tree/master/src/3_ResponsibilityChain

Наблюдатель

Наблюдатель (Observer) - паттерн, который позволяет наблюдать одним объектам за события, которые генерирует другой объект

Чаще всего есть есть сущность, которая производит какие-то события (такой объект называют издателем), и сущности, которые хотят отслеживать эти события (такие объекты называют наблюдателями или подписчиками)

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

public interface IChatObserver
{
    void OnChatMessageReceived(ChatUserMessage message);
}

public class Chat
{
    private readonly List<IChatObserver> _observers = [];

    public Chat(long id, string name)
    {
        Id = id;
        Name = name;
    }

    public long Id { get; }

    public string Name { get; }

    public void SendMessage(UserMessage message)
    {
        foreach (IChatObserver observer in _observers)
        {
            observer.OnChatMessageReceived(new ChatUserMessage(
                this,
                message));
        }
    }

    public void AddObserver(IChatObserver observer)
    {
        _observers.Add(observer);
    }
}

Обычно перед началом отправки событий подписчики должны изъявить желание получать события с помощью вызова метода AddObserver

Код издателя-подписчика: https://github.com/is-oop-y27/workshop-4/tree/master/src/4_Observer

Команда

Команда (Command) - объект, хранящий поведение, что позволяет его хранить, передавать, ставить в очередь или производить отмену действия

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

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

Пример использования команд в веб-приложении: https://github.com/is-oop-y27/workshop-4/tree/master/src/5_Command

Посетитель

Посетитель (или визитор, Visitor) - паттерн, позволяющий добавлять в программу новые операции, не изменяя изначальные классы объектов

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

Например: делаем вывод дерева файловой системы. Для этого сделаем посетитель, реализующий этот интерфейс с методами посещения файла и директории:

public interface IFileSystemComponentVisitor
{
    void Visit(FileFileSystemComponent component);

    void Visit(DirectoryFileSystemComponent component);
}

В реализации ConsoleVisitor сделаем вывод имени файла/директории

А в самих объектах, представляющих файлы и директории, сделаем метод Accept(IFileSystemComponentVisitor visitor):

public void Accept(IFileSystemComponentVisitor visitor)
{
    visitor.Visit(this);
}

Этот метод дает объекту понять, что его посетили, и дает свой тип посетителю. Тем самым вот так мы можем пройтись по всем директории и файлам в них:

var factory = new FileSystemComponentFactory();
IFileSystemComponent component = factory.Create("sample_folder");

var visitor = new ConsoleVisitor();

component.Accept(visitor);

Код примера: https://github.com/is-oop-y27/workshop-4/blob/master/src/6_Visitor

Снимок

Снимок (Snapshot или Memento) - паттерн, позволяющий сохранить состояние объектов

В паттерне “Снимок” есть 2 сущности:

По сути снимок - это просто копия всех полей инициатора в конкретный момент времени. Благодаря этому, мы можем вернуть инициатор к какому-то предыдущему состоянию из прошлого. Например:

var caretaker = new TextFieldHistory(new TextField());

caretaker.UpdateValue("1");
TextFieldSnapshot snapshot = caretaker.UpdateValue("2");

Console.WriteLine(string.Join("\n", caretaker.History.Select(x => x.ToString())));

Console.WriteLine(caretaker.Value);
caretaker.Restore(snapshot);
Console.WriteLine(caretaker.Value);

Здесь смотритель хранит в себе снимки инициатора и может изменять его через свой метод, возвращающий снимок. Код из примера: https://github.com/is-oop-y27/workshop-4/tree/master/src/7_Snapshot

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

Состояние

Состояние (State) - паттерн, позволяющий объектам менять поведение в зависимости от своего состояния

Паттерн “Состояние” просто описывает конечная машина состояний (finite state machine) - объекты представляются как состояния, а переходы между ними как методы, возвращающие тип результат, показывающий, есть такой переход или нет

Машина состояний на примере состояний лабораторной работы:

var submission = new Submission(new ActiveSubmissionStateHandler());

submission.Complete();
submission.Ban();

SubmissionActionResult result = submission.Complete();
Console.WriteLine(result);

Код примера: https://github.com/is-oop-y27/workshop-4/blob/master/src/8_State

Итератор

Итератор (Iterator) - паттерн, позволяющий обходить элементы составных объектов, не раскрывая их внутреннего представления

В C# итерируемые объекты реализованы через интерфейс IEnumerable и метод GetEnumerator, который возвращает итератор - реализацию интерфейса IEnumerator:

Применяя это к примеру файловой системы выше, с помощью методов расширения:

public static class FileSystemComponentExtensions
{
    public static IEnumerator<IFileSystemComponent> EnumerateBreadth(this IFileSystemComponent component)
        => new FileSystemBreadthIterator(component);

    public static IEnumerator<IFileSystemComponent> EnumerateDepth(this IFileSystemComponent component)
        => new FileSystemDepthIterator(component);
}

А реализации итераторов (в данном случае сделаем итераторы обходов в глубину и в ширину) мы можем сделать так:

var factory = new FileSystemComponentFactory();
IFileSystemComponent component = factory.Create("sample_folder");

using IEnumerator<IFileSystemComponent> breadthIterator = component.EnumerateBreadth();

while (breadthIterator.MoveNext())
{
    Console.WriteLine(breadthIterator.Current.Name);
}

Код примера итератора: https://github.com/is-oop-y27/workshop-4/tree/master/src/9_Iterator