Паттерны проектирования в C# для продвинутых: от теории к коду
Паттерны проектирования — это не просто абстрактные концепции из учебников. Для опытного C#-разработчика они становятся инструментарием, позволяющим создавать гибкую, расширяемую и поддерживаемую архитектуру приложений. В этой статье мы разберём три мощных паттерна, которые часто остаются за рамками базовых курсов: Visitor, Strategy и Command. Вы увидите не только их реализацию на C#, но и поймёте, как они решают реальные задачи в .NET-экосистеме.
1. Паттерн Visitor (Посетитель) — работа с иерархией объектов
Паттерн Visitor позволяет добавлять новые операции к существующим классам, не изменяя их структуру. Это особенно полезно, когда у вас есть сложная иерархия объектов, и вы хотите выполнять различные действия в зависимости от их типа.
Проблема
Представьте, что у вас есть система документооборота с классами TextDocument, Spreadsheet и Presentation. Вам нужно добавить функциональность экспорта в PDF, HTML и XML. Без паттерна Visitor вам пришлось бы модифицировать каждый класс, что нарушает принцип открытости/закрытости (OCP).
Реализация на C#
// 1. Интерфейс элемента
public interface IElement
{
void Accept(IVisitor visitor);
}
// 2. Конкретные элементы
public class TextDocument : IElement
{
public string Content { get; set; }
public void Accept(IVisitor visitor) => visitor.Visit(this);
}
public class Spreadsheet : IElement
{
public List<List<string>> Rows { get; set; }
public void Accept(IVisitor visitor) => visitor.Visit(this);
}
// 3. Интерфейс посетителя
public interface IVisitor
{
void Visit(TextDocument doc);
void Visit(Spreadsheet sheet);
}
// 4. Конкретный посетитель — экспорт в HTML
public class HtmlExportVisitor : IVisitor
{
public void Visit(TextDocument doc)
{
Console.WriteLine($"<h1>{doc.Content}</h1>");
}
public void Visit(Spreadsheet sheet)
{
Console.WriteLine("<table>");
foreach (var row in sheet.Rows)
{
Console.WriteLine("<tr><td>" + string.Join("</td><td>", row) + "</td></tr>");
}
Console.WriteLine("</table>");
}
}
// Использование
var docs = new List<IElement> { new TextDocument { Content = "Привет" }, new Spreadsheet { Rows = new List<List<string>> { new List<string> { "A", "B" } } } };
var visitor = new HtmlExportVisitor();
foreach (var doc in docs) doc.Accept(visitor);
Плюсы: легко добавлять новые операции, не трогая существующие классы. Минусы: если иерархия элементов часто меняется, Visitor становится неудобным.
2. Паттерн Strategy (Стратегия) — динамическая смена алгоритмов
Паттерн Strategy позволяет определять семейство алгоритмов, инкапсулировать каждый из них и делать их взаимозаменяемыми. Это классический способ избежать громадных if-else или switch конструкций.
Проблема
Вам нужно реализовать систему расчёта стоимости доставки. Способы доставки разные: курьером, почтой, самовывоз. Каждый способ имеет свою логику расчёта цены.
Реализация на C#
// 1. Интерфейс стратегии
public interface IShippingStrategy
{
decimal CalculateCost(Order order);
}
// 2. Конкретные стратегии
public class CourierShipping : IShippingStrategy
{
public decimal CalculateCost(Order order) => order.Weight * 5.0m + 100;
}
public class PostShipping : IShippingStrategy
{
public decimal CalculateCost(Order order) => order.Weight * 2.0m + 50;
}
public class PickupShipping : IShippingStrategy
{
public decimal CalculateCost(Order order) => 0; // Бесплатно
}
// 3. Контекст
public class Order
{
public decimal Weight { get; set; }
private IShippingStrategy _strategy;
public void SetShippingStrategy(IShippingStrategy strategy) => _strategy = strategy;
public decimal GetShippingCost() => _strategy?.CalculateCost(this) ?? throw new InvalidOperationException("Стратегия не задана");
}
// Использование
var order = new Order { Weight = 10 };
order.SetShippingStrategy(new CourierShipping());
Console.WriteLine($"Стоимость доставки курьером: {order.GetShippingCost()} руб.");
Плюсы: легко добавлять новые алгоритмы, код становится чище. Минусы: клиент должен знать о существующих стратегиях.
3. Паттерн Command (Команда) — инкапсуляция запроса
Паттерн Command превращает запросы в объекты. Это позволяет параметризовать методы, откладывать выполнение запросов, поддерживать отмену операций (undo) и вести журнал действий.
Проблема
Создайте систему для текстового редактора, которая поддерживает операции "вставить текст", "удалить символ" и "отменить последнее действие".
Реализация на C#
// 1. Интерфейс команды
public interface ICommand
{
void Execute();
void Undo();
}
// 2. Получатель (Receiver) — текстовый редактор
public class TextEditor
{
private string _text = "";
public void Insert(string text, int position)
{
_text = _text.Insert(position, text);
}
public void Delete(int position, int length)
{
_text = _text.Remove(position, length);
}
public string GetText() => _text;
}
// 3. Конкретная команда — вставка
public class InsertCommand : ICommand
{
private readonly TextEditor _editor;
private readonly string _text;
private readonly int _position;
public InsertCommand(TextEditor editor, string text, int position)
{
_editor = editor;
_text = text;
_position = position;
}
public void Execute() => _editor.Insert(_text, _position);
public void Undo() => _editor.Delete(_position, _text.Length);
}
// 4. Invoker (вызывающий) — управляет историей
public class CommandHistory
{
private readonly Stack<ICommand> _history = new Stack<ICommand>();
public void ExecuteCommand(ICommand command)
{
command.Execute();
_history.Push(command