Продвинутые паттерны проектирования в C#: от теории к практике
Паттерны проектирования (Design Patterns) — это проверенные решения типовых задач архитектуры программного обеспечения. На начальном уровне разработчики знакомятся с Singleton, Factory или Adapter. Однако для создания гибких, поддерживаемых и масштабируемых систем на платформе .NET необходимо владение более сложными поведенческими и структурными паттернами. В этой статье мы разберём три продвинутых паттерна: Chain of Responsibility, State и Visitor. Каждый паттерн будет проиллюстрирован практическим примером на C# с использованием современных возможностей языка (records, pattern matching, generics).
1. Chain of Responsibility (Цепочка обязанностей)
Паттерн Chain of Responsibility позволяет передавать запрос последовательно по цепочке обработчиков, пока один из них не обработает запрос. Это избавляет от жёсткой привязки отправителя к получателю. В C# часто применяется для middleware в ASP.NET Core или для валидации данных.
Пример: система проверки прав доступа
Допустим, у нас есть запрос на выполнение операции. Каждый обработчик проверяет одно условие (аутентификация, авторизация, лимиты).
// Базовый класс обработчика
public abstract class Handler
{
protected Handler? NextHandler;
public void SetNext(Handler handler) => NextHandler = handler;
public abstract void Handle(RequestContext context);
}
public class AuthenticationHandler : Handler
{
public override void Handle(RequestContext context)
{
if (context.IsAuthenticated)
{
Console.WriteLine("Аутентификация пройдена.");
NextHandler?.Handle(context);
}
else
{
Console.WriteLine("Ошибка: пользователь не аутентифицирован.");
}
}
}
public class AuthorizationHandler : Handler
{
public override void Handle(RequestContext context)
{
if (context.Role == "Admin")
{
Console.WriteLine("Авторизация пройдена.");
NextHandler?.Handle(context);
}
else
{
Console.WriteLine("Ошибка: недостаточно прав.");
}
}
}
// Контекст запроса
public record RequestContext(bool IsAuthenticated, string Role);
// Использование
var authHandler = new AuthenticationHandler();
var authzHandler = new AuthorizationHandler();
authHandler.SetNext(authzHandler);
var context = new RequestContext(true, "Admin");
authHandler.Handle(context);
// Вывод:
// Аутентификация пройдена.
// Авторизация пройдена.
Этот паттерн идеален для создания конвейеров обработки, где каждый шаг может прервать выполнение или передать управление дальше.
2. State (Состояние)
Паттерн State позволяет объекту изменять своё поведение при изменении внутреннего состояния. Внешне это выглядит так, будто объект меняет свой класс. В C# удобно реализовывать через интерфейсы и композицию.
Пример: управление заказом в интернет-магазине
Заказ может находиться в состояниях: Новый, Оплачен, Отправлен, Доставлен. Каждое состояние определяет, какие действия допустимы.
// Интерфейс состояния
public interface IOrderState
{
void Pay(Order order);
void Ship(Order order);
void Deliver(Order order);
}
// Конкретные состояния
public class NewOrderState : IOrderState
{
public void Pay(Order order)
{
Console.WriteLine("Заказ оплачен.");
order.State = new PaidOrderState();
}
public void Ship(Order order) => Console.WriteLine("Ошибка: заказ ещё не оплачен.");
public void Deliver(Order order) => Console.WriteLine("Ошибка: заказ ещё не отправлен.");
}
public class PaidOrderState : IOrderState
{
public void Pay(Order order) => Console.WriteLine("Ошибка: заказ уже оплачен.");
public void Ship(Order order)
{
Console.WriteLine("Заказ отправлен.");
order.State = new ShippedOrderState();
}
public void Deliver(Order order) => Console.WriteLine("Ошибка: заказ ещё не отправлен.");
}
// Класс контекста
public class Order
{
public IOrderState State { get; set; } = new NewOrderState();
public void Pay() => State.Pay(this);
public void Ship() => State.Ship(this);
public void Deliver() => State.Deliver(this);
}
// Использование
var order = new Order();
order.Pay(); // Заказ оплачен.
order.Ship(); // Заказ отправлен.
order.Deliver();// Заказ доставлен.
Паттерн State избавляет от огромных switch-блоков и делает добавление новых состояний тривиальным.
3. Visitor (Посетитель)
Паттерн Visitor позволяет добавлять новые операции к иерархии классов, не изменяя сами классы. Это классический пример принципа Open/Closed. В C# часто применяется для обхода деревьев (например, AST) или сериализации.
Пример: экспорт фигур в разные форматы
Пусть у нас есть иерархия геометрических фигур. Мы хотим добавить операции экспорта в XML и JSON, не модифицируя классы фигур.
// Интерфейс элемента
public interface IShape
{
void Accept(IShapeVisitor visitor);
}
// Конкретные элементы
public record Circle(double Radius) : IShape
{
public void Accept(IShapeVisitor visitor) => visitor.Visit(this);
}
public record Rectangle(double Width, double Height) : IShape
{
public void Accept(IShapeVisitor visitor) => visitor.Visit(this);
}
// Интерфейс посетителя
public interface IShapeVisitor
{
void Visit(Circle circle);
void Visit(Rectangle rectangle);
}
// Конкретный посетитель для экспорта в XML
public class XmlExportVisitor : IShapeVisitor
{
public void Visit(Circle circle) =>
Console.WriteLine($"<Circle Radius=\"{circle.Radius}\" />");
public void Visit(Rectangle rect) =>
Console.WriteLine($"<Rectangle Width=\"{rect.Width}\" Height=\"{rect.Height}\" />");
}
// Использование
var shapes = new IShape[] { new Circle(5), new Rectangle(10, 20) };
var xmlVisitor = new XmlExportVisitor();
foreach (var shape in shapes)
{
shape.Accept(xmlVisitor);
}
// Вывод:
// <Circle Radius="5" />
// <Rectangle Width="10" Height="20" />
Добавление нового формата (например, JSON) требует лишь создания нового класса-посетителя, без изменения Circle или Rectangle.
Заключение
Продвинутые паттерны проектирования — это не роскошь, а необходимость для профессиональной разработки на C#. Chain of Responsibility помогает строить гибкие конвейеры, State — управлять поведением объектов с изменяющимся состоянием, а Visitor — расширять функциональность без изменения существующего кода. Освоив эти паттерны, вы сможете