На этом воркшопе будут рассматриваться поведенческие паттерны. Рассматриваемый код доступен в этом репозитории: 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:
GetNext() - подвинуть итератор впередCurrent - получить значение по итераторуReset() - сбросить итератор к начальному значениюПрименяя это к примеру файловой системы выше, с помощью методов расширения:
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