Паттерны проектирования в C#: полное руководство с примерами

Паттерны проектирования (design patterns) — это проверенные решения типовых задач при разработке ПО.

Зачем нужны паттерны?

Паттерны помогают:

Все паттерны делятся на три группы: порождающие (создание объектов), структурные (компоновка классов и объектов) и поведенческие (взаимодействие между объектами).

Порождающие паттерны (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());

Комбинирование паттернов в реальном проекте

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

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

Паттерны в .NET / C#: где они уже встроены

Заключение

Паттерны проектирования — это не догма, а набор инструментов. Главное — понимать, какую проблему они решают, и не усложнять код там, где можно обойтись простым решением. Изучайте паттерны, применяйте их с умом, и ваш код станет чище, понятнее и легче в поддержке.