Паттерны проектирования в C#: полное руководство с примерами
Паттерны проектирования (design patterns) — это проверенные решения типовых задач при разработке программного обеспечения. В экосистеме C# и .NET они помогают создавать гибкую, масштабируемую и поддерживаемую архитектуру. В этой статье мы разберем четыре ключевых паттерна: Singleton, Factory Method, Observer и Strategy, с реальными примерами на C#.
Зачем нужны паттерны проектирования в C#?
Паттерны не являются готовыми библиотеками или фреймворками. Это концептуальные шаблоны, которые:
- Упрощают коммуникацию — разработчики используют единую терминологию
- Повышают качество кода — уменьшают связанность и увеличивают гибкость
- Экономят время — не нужно изобретать велосипед
- Соответствуют принципам SOLID — большинство паттернов реализуют эти принципы
1. Singleton (Одиночка)
Назначение: Гарантирует, что у класса есть только один экземпляр, и предоставляет глобальную точку доступа к нему.
Когда использовать:
- Логгеры
- Менеджеры конфигураций
- Пул соединений с базой данных
- Кэши
Пример на C# (потокобезопасная реализация):
public sealed class ConfigurationManager
{
private static readonly Lazy<ConfigurationManager> _instance =
new Lazy<ConfigurationManager>(() => new ConfigurationManager());
private readonly Dictionary<string, string> _settings;
private ConfigurationManager()
{
_settings = new Dictionary<string, string>
{
{ "ConnectionString", "Server=localhost;Database=myDb" },
{ "ApiKey", "12345-ABCDE" }
};
}
public static ConfigurationManager Instance => _instance.Value;
public string GetSetting(string key)
{
return _settings.TryGetValue(key, out var value) ? value : null;
}
}
// Использование
var config = ConfigurationManager.Instance;
Console.WriteLine(config.GetSetting("ConnectionString"));
Плюсы:
- Контролируемый доступ к единственному экземпляру
- Ленивая инициализация (Lazy<T>)
- Потокобезопасность без блокировок
2. Factory Method (Фабричный метод)
Назначение: Определяет интерфейс для создания объекта, но позволяет подклассам изменять тип создаваемого объекта.
Когда использовать:
- Когда заранее неизвестен конкретный тип объекта
- Когда создание объекта требует сложной логики
- Для разделения кода создания и использования
Пример на C#:
// Абстрактный продукт
public interface ILogger
{
void Log(string message);
}
// Конкретные продукты
public class FileLogger : ILogger
{
public void Log(string message) =>
File.AppendAllText("log.txt", $"{DateTime.Now}: {message}\n");
}
public class ConsoleLogger : ILogger
{
public void Log(string message) =>
Console.WriteLine($"{DateTime.Now}: {message}");
}
// Создатель с фабричным методом
public abstract class LoggerFactory
{
public abstract ILogger CreateLogger();
public void LogMessage(string message)
{
var logger = CreateLogger();
logger.Log(message);
}
}
public class FileLoggerFactory : LoggerFactory
{
public override ILogger CreateLogger() => new FileLogger();
}
public class ConsoleLoggerFactory : LoggerFactory
{
public override ILogger CreateLogger() => new ConsoleLogger();
}
// Использование
LoggerFactory factory = new ConsoleLoggerFactory();
factory.LogMessage("Привет, мир!");
3. Observer (Наблюдатель)
Назначение: Определяет зависимость "один ко многим" между объектами так, что при изменении состояния одного объекта все зависимые оповещаются и обновляются автоматически.
Когда использовать:
- Системы событий и уведомлений
- UI-компоненты (MVVM, WinForms)
- Мониторинг изменений данных
Пример на C# с использованием событий:
public class StockMarket
{
private decimal _price;
public event EventHandler<decimal> PriceChanged;
public decimal Price
{
get => _price;
set
{
if (_price != value)
{
_price = value;
OnPriceChanged(_price);
}
}
}
protected virtual void OnPriceChanged(decimal newPrice)
{
PriceChanged?.Invoke(this, newPrice);
}
}
public class Investor
{
public string Name { get; }
public Investor(string name, StockMarket market)
{
Name = name;
market.PriceChanged += OnPriceChanged;
}
private void OnPriceChanged(object sender, decimal newPrice)
{
Console.WriteLine($"{Name} получил уведомление: новая цена {newPrice:C}");
}
}
// Использование
var market = new StockMarket();
var investor1 = new Investor("Иван", market);
var investor2 = new Investor("Мария", market);
market.Price = 150.50m;
market.Price = 152.30m;
4. Strategy (Стратегия)
Назначение: Определяет семейство алгоритмов, инкапсулирует каждый из них и делает их взаимозаменяемыми. Паттерн позволяет изменять алгоритм независимо от клиентов, которые его используют.
Когда использовать:
- Разные способы сортировки, фильтрации или валидации
- Расчет стоимости с разными скидками
- Алгоритмы сжатия или шифрования
Пример на C#:
// Интерфейс стратегии
public interface ISortStrategy
{
void Sort(int[] array);
}
// Конкретные стратегии
public class BubbleSortStrategy : ISortStrategy
{
public void Sort(int[] array)
{
for (int i = 0; i < array.Length - 1; i++)
for (int j = 0; j < array.Length - i - 1; j++)
if (array[j] > array[j + 1])
(array[j], array[j + 1]) = (array[j + 1], array[j]);
Console.WriteLine("Отсортировано пузырьком");
}
}
public class QuickSortStrategy : ISortStrategy
{
public void Sort(int[] array)
{
QuickSort(array, 0, array.Length - 1);
Console.WriteLine("Отсортировано быстрой сортировкой");