Паттерны проектирования в C# с примерами: от А до Я
Паттерны проектирования (design patterns) — это проверенные решения типовых задач, возникающих при разработке программного обеспечения. Они не являются готовым кодом, а представляют собой шаблоны, которые можно адаптировать под конкретную ситуацию. Для разработчика на C# знание паттернов — это не просто плюс в резюме, а необходимость для создания гибкой, поддерживаемой и масштабируемой архитектуры.
В этой статье мы разберем три основные группы паттернов по классификации GoF (Gang of Four): порождающие, структурные и поведенческие. Каждый паттерн будет рассмотрен на практическом примере на C# (.NET).
Порождающие паттерны (Creational Patterns)
Эти паттерны управляют процессом создания объектов, делая систему независимой от способа их порождения. Рассмотрим два самых популярных.
Одиночка (Singleton)
Гарантирует, что у класса есть только один экземпляр, и предоставляет глобальную точку доступа к нему. В C# для безопасной работы в многопоточной среде используется Lazy<T>.
public sealed class Logger
{
private static readonly Lazy<Logger> _instance =
new Lazy<Logger>(() => new Logger());
public static Logger Instance => _instance.Value;
private Logger() { }
public void Log(string message)
{
Console.WriteLine($"[{DateTime.Now}] {message}");
}
}
// Использование
Logger.Instance.Log("Приложение запущено");
Фабричный метод (Factory Method)
Определяет интерфейс для создания объекта, но позволяет подклассам изменять тип создаваемого объекта. Отлично подходит для ситуаций, когда тип объекта заранее неизвестен.
public interface ITransport
{
void Deliver();
}
public class Truck : ITransport
{
public void Deliver() => Console.WriteLine("Доставка по земле");
}
public class Ship : ITransport
{
public void Deliver() => Console.WriteLine("Доставка по морю");
}
public abstract class Logistics
{
public abstract ITransport CreateTransport();
public void PlanDelivery()
{
var transport = CreateTransport();
transport.Deliver();
}
}
public class RoadLogistics : Logistics
{
public override ITransport CreateTransport() => new Truck();
}
// Использование
Logistics logistics = new RoadLogistics();
logistics.PlanDelivery(); // Вывод: Доставка по земле
Структурные паттерны (Structural Patterns)
Они определяют способы построения связей между объектами, обеспечивая гибкость и эффективность архитектуры.
Адаптер (Adapter)
Позволяет объектам с несовместимыми интерфейсами работать вместе. Часто используется при интеграции со старым кодом или сторонними библиотеками.
// Целевой интерфейс, ожидаемый клиентом
public interface IModernPrinter
{
void Print(string text);
}
// Старый класс с несовместимым интерфейсом
public class OldPrinter
{
public void PrintOldStyle(string message)
{
Console.WriteLine($"Старый принтер: {message}");
}
}
// Адаптер
public class PrinterAdapter : IModernPrinter
{
private readonly OldPrinter _oldPrinter;
public PrinterAdapter(OldPrinter oldPrinter)
{
_oldPrinter = oldPrinter;
}
public void Print(string text)
{
_oldPrinter.PrintOldStyle(text);
}
}
// Использование
IModernPrinter printer = new PrinterAdapter(new OldPrinter());
printer.Print("Привет, мир!"); // Вывод: Старый принтер: Привет, мир!
Декоратор (Decorator)
Динамически добавляет объекту новые обязанности, не изменяя его код. Является гибкой альтернативой наследованию.
public interface ICoffee
{
double GetCost();
string GetDescription();
}
public class SimpleCoffee : ICoffee
{
public double GetCost() => 10.0;
public string GetDescription() => "Обычный кофе";
}
public abstract class CoffeeDecorator : ICoffee
{
protected ICoffee _coffee;
public CoffeeDecorator(ICoffee coffee) => _coffee = coffee;
public virtual double GetCost() => _coffee.GetCost();
public virtual string GetDescription() => _coffee.GetDescription();
}
public class MilkDecorator : CoffeeDecorator
{
public MilkDecorator(ICoffee coffee) : base(coffee) { }
public override double GetCost() => base.GetCost() + 2.5;
public override string GetDescription() => base.GetDescription() + ", с молоком";
}
// Использование
ICoffee coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
Console.WriteLine($"{coffee.GetDescription()}: {coffee.GetCost()} руб.");
// Вывод: Обычный кофе, с молоком: 12.5 руб.
Поведенческие паттерны (Behavioral Patterns)
Эти паттерны определяют алгоритмы и распределение ответственности между объектами.
Наблюдатель (Observer)
Создает механизм подписки, позволяющий одним объектам (наблюдателям) следить за изменениями состояния других объектов (субъектов). В C# часто реализуется через события.
public class Stock
{
private decimal _price;
private readonly List<IInvestor> _investors = new();
public void Attach(IInvestor investor) => _investors.Add(investor);
public void Detach(IInvestor investor) => _investors.Remove(investor);
public decimal Price
{
get => _price;
set
{
if (_price != value)
{
_price = value;
Notify();
}
}
}
private void Notify()
{
foreach (var investor in _investors)
investor.Update(this);
}
}
public interface IInvestor
{
void Update(Stock stock);
}
public class Investor : IInvestor
{
public string Name { get; set; }
public void Update(Stock stock)
{
Console.WriteLine($"Уведомление для {Name}: цена изменилась на {stock.Price}");
}
}
// Использование
var stock = new Stock();
var investor1 = new Investor { Name = "Иван" };
stock.Attach(investor1);
stock.Price = 150.5m; // Уведомление для Иван: цена изменилась на 150,5
Стратегия (Strategy)
Определяет семейство алгоритмов, инкапсулирует каждый из них и делает их взаимозаменяемыми. Позволяет изменять поведение объекта во время выполнения.
public interface ISortStrategy
{
void Sort(List<int> list);
}
public class QuickSort : ISortStrategy
{
public void Sort(List<int> list)
{
list.Sort(); // Упрощенная реализация
Console.WriteLine("Быстрая сортировка выполнена