Зачем нужны паттерны?
Паттерны помогают:
- Избегать «изобретения велосипеда» — использовать готовые архитектурные решения.
- Улучшить коммуникацию в команде (достаточно сказать «здесь мы применили Strategy», и коллега поймёт структуру).
- Снизить связанность кода и облегчить его тестирование.
Все паттерны делятся на три группы: порождающие (создание объектов), структурные (компоновка классов и объектов) и поведенческие (взаимодействие между объектами).
Порождающие паттерны (Creational)
1. Singleton (Одиночка)
Гарантирует, что у класса есть только один экземпляр, и предоставляет глобальную точку доступа к нему. Часто используется для логгеров, конфигураций, кэша.
Реализация в C# (потокобезопасная через Lazy<T>):
public sealed class Logger
{
private static readonly Lazy<Logger> _instance = new Lazy<Logger>(() => new Logger());
private Logger() { }
public static Logger Instance => _instance.Value;
public void Log(string message) => Console.WriteLine($"{DateTime.Now}: {message}");
}
Использование: Logger.Instance.Log("Старт приложения");
Важно: Singleton часто критикуют за скрытые зависимости и сложности в тестировании. В современных приложениях лучше использовать DI-контейнеры с жизненным циклом Singleton.
2. Factory Method (Фабричный метод)
Определяет интерфейс для создания объекта, но оставляет подклассам решение о том, экземпляр какого класса создавать. Позволяет делегировать создание объектов наследникам.
Пример: создание разных типов документов.
public abstract class DocumentCreator
{
public abstract IDocument CreateDocument();
}
public class PdfCreator : DocumentCreator
{
public override IDocument CreateDocument() => new PdfDocument();
}
public class WordCreator : DocumentCreator
{
public override IDocument CreateDocument() => new WordDocument();
}
public interface IDocument
{
void Open();
void Save();
}
public class PdfDocument : IDocument { /* реализация */ }
public class WordDocument : IDocument { /* реализация */ }
3. Builder (Строитель)
Отделяет конструирование сложного объекта от его представления, позволяя использовать один и тот же процесс построения для разных представлений.
Пример: построение SQL-запроса.
public class SqlQueryBuilder
{
private string _select;
private string _from;
private string _where;
public SqlQueryBuilder Select(params string[] columns)
{
_select = "SELECT " + string.Join(", ", columns);
return this;
}
public SqlQueryBuilder From(string table)
{
_from = "FROM " + table;
return this;
}
public SqlQueryBuilder Where(string condition)
{
_where = "WHERE " + condition;
return this;
}
public string Build() => $"{_select} {_from} {_where}".Trim();
}
// Использование
var query = new SqlQueryBuilder()
.Select("Id", "Name")
.From("Users")
.Where("Age > 18")
.Build();
Структурные паттерны (Structural)
4. Adapter (Адаптер)
Позволяет объектам с несовместимыми интерфейсами работать вместе. Часто применяется при интеграции со сторонними библиотеками.
Пример: адаптация стороннего логгера под ваш интерфейс.
// Ваш интерфейс логирования
public interface ILogger
{
void Log(string message);
}
// Сторонний логгер (несовместимый)
public class ThirdPartyLogger
{
public void WriteEntry(string entry) => Console.WriteLine($"ThirdParty: {entry}");
}
// Адаптер
public class ThirdPartyLoggerAdapter : ILogger
{
private readonly ThirdPartyLogger _adaptee;
public ThirdPartyLoggerAdapter(ThirdPartyLogger adaptee) => _adaptee = adaptee;
public void Log(string message) => _adaptee.WriteEntry(message);
}
5. Decorator (Декоратор)
Динамически добавляет объекту новые обязанности. Является гибкой альтернативой наследованию.
Пример: добавление сжатия и шифрования к потоку данных.
public interface IStream
{
void Write(byte[] data);
}
public class FileStream : IStream
{
public void Write(byte[] data) => /* запись в файл */;
}
public abstract class StreamDecorator : IStream
{
protected IStream _stream;
public StreamDecorator(IStream stream) => _stream = stream;
public abstract void Write(byte[] data);
}
public class CompressStream : StreamDecorator
{
public CompressStream(IStream stream) : base(stream) { }
public override void Write(byte[] data)
{
var compressed = Compress(data);
_stream.Write(compressed);
}
}
public class EncryptStream : StreamDecorator
{
public EncryptStream(IStream stream) : base(stream) { }
public override void Write(byte[] data)
{
var encrypted = Encrypt(data);
_stream.Write(encrypted);
}
}
// Использование
IStream stream = new FileStream();
stream = new CompressStream(stream);
stream = new EncryptStream(stream);
stream.Write(data); // данные будут сжаты, затем зашифрованы и записаны
Поведенческие паттерны (Behavioral)
6. Observer (Наблюдатель)
Создаёт механизм подписки, позволяющий одним объектам следить за изменениями других. В C# удобно реализовывать через события.
Пример: биржа акций уведомляет подписчиков об изменении цены.
public class Stock
{
private decimal _price;
public event EventHandler<decimal> PriceChanged;
public decimal Price
{
get => _price;
set
{
_price = value;
PriceChanged?.Invoke(this, _price);
}
}
}
public class Investor
{
public void OnPriceChanged(object sender, decimal price) =>
Console.WriteLine($"Уведомление: цена изменилась на {price}");
}
// Использование
var stock = new Stock();
var investor = new Investor();
stock.PriceChanged += investor.OnPriceChanged;
stock.Price = 150.5m; // инвестор получит уведомление
7. Strategy (Стратегия)
Определяет семейство алгоритмов, инкапсулирует каждый из них и делает их взаимозаменяемыми. Позволяет изменять алгоритмы независимо от клиентов, которые ими пользуются.
Пример: стратегии расчёта стоимости доставки.
public interface IShippingStrategy
{
decimal Calculate(Order order);
}
public class ExpressShipping : IShippingStrategy
{
public decimal Calculate(Order order) => order.Total * 0.1m + 10;
}
public class StandardShipping : IShippingStrategy
{
public decimal Calculate(Order order) => order.Total * 0.05m + 5;
}
public class Order
{
public decimal Total { get; set; }
private IShippingStrategy _shippingStrategy;
public void SetShippingStrategy(IShippingStrategy strategy) => _shippingStrategy = strategy;
public decimal GetShippingCost() => _shippingStrategy?.Calculate(this) ?? 0;
}
// Использование
var order = new Order { Total = 1000 };
order.SetShippingStrategy(new ExpressShipping());
Console.WriteLine(order.GetShippingCost());
Комбинирование паттернов в реальном проекте
Паттерны редко используются поодиночке. Рассмотрим пример обработки платежей в интернет-магазине:
- Factory Method — создание платёжного провайдера (PayPal, Stripe) в зависимости от выбора пользователя.
- Strategy — разные алгоритмы расчёта комиссии для каждого провайдера.
- Decorator — добавление логирования и кэширования результатов запросов к платёжному шлюзу.
- Observer — уведомление служб (бухгалтерия, склад) после успешной оплаты.
Такое сочетание делает систему гибкой: можно добавлять новых провайдеров, менять логику комиссий и расширять функциональность, не трогая основной код.
Паттерны в .NET / C#: где они уже встроены
- IEnumerator и yield return — реализация паттерна Iterator.
- LINQ — основан на паттернах Visitor, Iterator и др.
- HttpClient — использует Builder для построения запросов.
- ASP.NET Core Middleware — реализация цепочки обязанностей (Chain of Responsibility).
- Dependency Injection — паттерн Внедрение зависимостей (разновидность Inversion of Control).
Заключение
Паттерны проектирования — это не догма, а набор инструментов. Главное — понимать, какую проблему они решают, и не усложнять код там, где можно обойтись простым решением. Изучайте паттерны, применяйте их с умом, и ваш код станет чище, понятнее и легче в поддержке.