Паттерны проектирования в C# (.NET): подробное руководство
Что это такое и зачем? (аналогия из реальной жизни)
Представьте, что вы строите дом. Если вы каждый раз будете начинать строительство с нуля — рыть котлован, замешивать бетон, пилить доски — на один дом уйдут годы. Но если у вас есть проверенные чертежи (типовые проекты домов), вы просто берёте готовое решение: «здесь будет несущая стена, здесь — окно, а крыша — двускатная». Вы не изобретаете велосипед, а используете опыт тысяч строителей до вас.
В программировании то же самое. Паттерны проектирования (или просто «паттерны») — это готовые, проверенные временем решения типовых задач. Это не готовый код, который можно скопировать, а скорее шаблон мысли, способ организации классов и объектов, чтобы код был:
- Понятным (другие разработчики сразу догадаются, что вы хотели сделать)
- Гибким (легко добавлять новые функции, не ломая старые)
- Переиспользуемым (один раз написали — используете в сотне проектов)
Важно: Паттерны — это не магия и не обязательное требование. Это инструмент. Как молоток: если вы забиваете гвозди — он незаменим, но если вы хотите закрутить шуруп — лучше взять отвёртку. Используйте паттерны только тогда, когда они действительно решают проблему.
Аналогия из жизни: Представьте, что вы — шеф-повар. У вас есть набор базовых рецептов: «как сварить бульон», «как сделать соус бешамель», «как замесить тесто». Каждый раз, когда приходит заказ, вы не придумываете новый способ варки бульона — вы берёте готовый рецепт и немного адаптируете его под конкретное блюдо. Паттерны — это такие же «рецепты» для программиста.
Всего существует 23 классических паттерна, описанных в книге «Банды четырёх» (Gang of Four). Они делятся на три группы:
- Порождающие — как удобно создавать объекты (например, не через new, а через специальный метод)
- Структурные — как собирать классы в большие конструкции (как из кирпичиков собрать стену)
- Поведенческие — как объекты общаются друг с другом (как люди в офисе передают друг другу документы)
Теория: как это работает изнутри
Давайте заглянем «под капот» самого популярного паттерна — Одиночка (Singleton). Он нужен, когда у вас в программе должен быть ровно один экземпляр какого-то класса. Например, подключение к базе данных, логгер (запись ошибок) или настройки приложения.
Как это работает изнутри:
- Конструктор класса делается закрытым (private). Это значит, что никто снаружи не может написать
new MyClass(). - Создаётся статическое поле, которое хранит единственный экземпляр класса.
- Создаётся статический метод (обычно называется
GetInstance()или свойствоInstance), который проверяет: если экземпляр уже есть — вернуть его, если нет — создать новый и вернуть.
Вот как это выглядит на схеме:
┌─────────────────────────┐
│ Singleton │
├─────────────────────────┤
│ - instance: Singleton │ ← статическое поле (единственный экземпляр)
├─────────────────────────┤
│ - Singleton() │ ← закрытый конструктор
│ + GetInstance(): Singleton│ ← статический метод доступа
└─────────────────────────┘
Когда вы вызываете Singleton.GetInstance() в первый раз, создаётся объект. Во второй, третий, сотый раз — возвращается тот же самый объект. Это как если бы у вас был один ключ от сейфа: вы можете сделать сколько угодно копий ключа (вызовов метода), но сейф (объект) остаётся один.
Важно: В многопоточных программах (когда несколько частей кода работают одновременно) обычный Singleton может сломаться — два потока могут одновременно проверить, что экземпляра нет, и создать два объекта. Для этого используют «блокировку» (lock) — мы рассмотрим это в примерах.
Синтаксис в C# (.NET): все формы и варианты
В C# есть несколько способов реализовать паттерны. Давайте разберём основные формы на примере всё того же Singleton, а потом перейдём к другим паттернам.
1. Простой Singleton (не потокобезопасный)
public class SimpleSingleton
{
// Статическое поле для хранения единственного экземпляра
private static SimpleSingleton _instance;
// Закрытый конструктор — никто не сможет создать объект через new
private SimpleSingleton()
{
Console.WriteLine("Создан новый экземпляр SimpleSingleton");
}
// Статическое свойство для доступа к экземпляру
public static SimpleSingleton Instance
{
get
{
// Если экземпляр ещё не создан — создаём
if (_instance == null)
{
_instance = new SimpleSingleton();
}
// Возвращаем существующий экземпляр
return _instance;
}
}
public void DoSomething()
{
Console.WriteLine("Работает единственный экземпляр");
}
}
2. Потокобезопасный Singleton с блокировкой (double-check locking)
public class ThreadSafeSingleton
{
// Объект для блокировки (замок)
private static readonly object _lock = new object();
private static ThreadSafeSingleton _instance;
private ThreadSafeSingleton() { }
public static ThreadSafeSingleton Instance
{
get
{
// Первая проверка — без блокировки (быстрая)
if (_instance == null)
{
// Блокируем доступ для других потоков
lock (_lock)
{
// Вторая проверка — уже внутри блокировки
if (_instance == null)
{
_instance = new ThreadSafeSingleton();
}
}
}
return _instance;
}
}
}
3. Singleton через Lazy (самый простой и безопасный)
public class LazySingleton
{
// Lazy сам заботится о потокобезопасности
private static readonly Lazy<LazySingleton> _lazyInstance =
new Lazy<LazySingleton>(() => new LazySingleton());
private LazySingleton() { }
public static LazySingleton Instance => _lazyInstance.Value;
}
Теперь давайте посмотрим на другие популярные паттерны и их синтаксис в C#.
4. Фабричный метод (Factory Method)
// Абстрактный класс (шаблон) для всех видов транспорта
public abstract class Transport
{
public abstract void Move();
}
// Конкретные виды транспорта
public class Car : Transport
{
public override void Move() => Console.WriteLine("Машина едет по дороге");
}
public class Ship : Transport
{
public override void Move() => Console.WriteLine("Корабль плывёт по морю");
}
// Фабрика — создаёт нужный транспорт
public class TransportFactory
{
public Transport CreateTransport(string type)
{
switch (type.ToLower())
{
case "car":
return new Car();
case "ship":
return new Ship();
default:
throw new ArgumentException("Неизвестный тип транспорта");
}
}
}
5. Строитель (Builder)
// Класс, который мы будем собирать по частям
public class Pizza
{
public string Dough { get; set; }
public string Sauce { get; set; }
public string Topping { get; set; }
public void Display()
{
Console.WriteLine($"Пицца: тесто - {Dough}, соус - {Sauce}, топпинг - {Topping}");
}
}
// Строитель — пошагово собирает пиццу
public class PizzaBuilder
{
private Pizza _pizza = new Pizza();
public PizzaBuilder SetDough(string dough)
{
_pizza.Dough = dough;
return this; // Возвращаем себя для цепочки вызовов
}
public PizzaBuilder SetSauce(string sauce)
{
_pizza.Sauce = sauce;
return this;
}
public PizzaBuilder SetTopping(string topping)
{
_pizza.Topping = topping;
return this;
}
public Pizza Build()
{
return _pizza;
}
}
// Использование:
// Pizza myPizza = new PizzaBuilder()
// .SetDough("тонкое")
// .SetSauce("томатный")
// .SetTopping("сыр")
// .Build();
6. Адаптер (Adapter)
// Старый интерфейс (например, для зарядки micro-USB)
public interface IMicroUsb
{
void Charge();
}
// Старое устройство
public class OldPhone : IMicroUsb
{
public void Charge() => Console.WriteLine("Заряжается через micro-USB");
}
// Новый интерфейс (USB-C)
public interface IUsbC
{
void FastCharge();
}
// Адаптер — превращает micro-USB в USB-C
public class UsbCAdapter : IUsbC
{
private IMicroUsb _microUsbDevice;
public UsbCAdapter(IMicroUsb device)
{
_microUsbDevice = device;
}
public void FastCharge()
{
Console.WriteLine("Адаптер преобразует сигнал...");
_microUsbDevice.Charge();
}
}
7. Наблюдатель (Observer)
// Интерфейс наблюдателя (подписчика)
public interface IObserver
{
void Update(string message);
}
// Конкретный наблюдатель
public class User : IObserver
{
private string _name;
public User(string name) => _name = name;
public void Update(string message)
{
Console.WriteLine($"{_name} получил уведомление: {message}");
}
}
// Издатель (наблюдаемый объект)
public class NewsPublisher
{
private List<IObserver> _observers = new List<IObserver>();
public void Subscribe(IObserver observer)
{
_observers.Add(observer);
Console.WriteLine("Новый подписчик добавлен");
}
public void Unsubscribe(IObserver observer)
{
_observers.Remove(observer);
}
public void PublishNews(string news)
{
Console.WriteLine($"
Опубликована новость: {news}");
foreach (var observer in _observers)
{
observer.Update(news);
}
}
}
Примеры кода — от простого к сложному
Пример 1: Singleton для логирования (простой)
using System;
using System.IO;
public class Logger
{
private static Logger _instance;
private string _logFilePath;
// Закрытый конструктор — создаём файл лога
private Logger()
{
_logFilePath = "app.log";
// Очищаем файл при старте
File.WriteAllText(_logFilePath, $"Лог запущен: {DateTime.Now}
");
}
public static Logger Instance
{
get
{
if (_instance == null)
{
_instance = new Logger();
}
return _instance;
}
}
// Метод для записи сообщения в лог
public void Log(string message)
{
string logEntry = $"[{DateTime.Now:HH:mm:ss}] {message}";
Console.WriteLine(logEntry); // Выводим в консоль
File.AppendAllText(_logFilePath, logEntry + "
"); // Пишем в файл
}
}
// Использование:
// Logger.Instance.Log("Программа запущена");
// Logger.Instance.Log("Пользователь нажал кнопку");
// // Второй раз создавать не нужно — используем тот же экземпляр
Пример 2: Фабрика для создания персонажей игры
// Базовый класс персонажа
public abstract class GameCharacter
{
public string Name { get; set; }
public int Health { get; set; }
public abstract void Attack();
}
// Конкретные персонажи
public class Warrior : GameCharacter
{
public Warrior()
{
Name = "Воин";
Health = 100;
}
public override void Attack()
{
Console.WriteLine($"{Name} атакует мечом! (Здоровье: {Health})");
}
}
public class Mage : GameCharacter
{
public Mage()
{
Name = "Маг";
Health = 60;
}
public override void Attack()
{
Console.WriteLine($"{Name} кидает огненный шар! (Здоровье: {Health})");
}
}
public class Archer : GameCharacter
{
public Archer()
{
Name = "Лучник";
Health = 80;
}
public override void Attack()
{
Console.WriteLine($"{Name} стреляет из лука! (Здоровье: {Health})");
}
}
// Фабрика персонажей
public class CharacterFactory
{
public GameCharacter CreateCharacter(string type)
{
// Используем switch expression (C# 8.0+)
return type.ToLower() switch
{
"warrior" => new Warrior(),
"mage" => new Mage(),
"archer" => new Archer(),
_ => throw new ArgumentException($"Неизвестный тип: {type}")
};
}
}
// Демонстрация работы
class Program
{
static void Main()
{
var factory = new CharacterFactory();
// Создаём персонажей через фабрику
var warrior = factory.CreateCharacter("warrior");
var mage = factory.CreateCharacter("mage");
var archer = factory.CreateCharacter("archer");
// Все персонажи атакуют по-своему
warrior.Attack();
mage.Attack();
archer.Attack();
}
}
Пример 3: Строитель для сложного заказа (компьютер)
// Класс компьютера — много параметров
public class Computer
{
public string CPU { get; set; }
public string GPU { get; set; }
public int RAM { get; set; } // в ГБ
public int Storage { get; set; } // в ГБ
public bool HasWiFi { get; set; }
public string OS { get; set; }
public void ShowSpecs()
{
Console.WriteLine($"
=== Спецификация компьютера ===");
Console.WriteLine($"Процессор: {CPU}");
Console.WriteLine($"Видеокарта: {GPU}");
Console.WriteLine($"ОЗУ: {RAM} ГБ");
Console.WriteLine($"Накопитель: {Storage} ГБ");
Console.WriteLine($"Wi-Fi: {(HasWiFi ? "Да" : "Нет")}");
Console.WriteLine($"ОС: {OS}");
}
}
// Строитель — пошагово собирает компьютер
public class ComputerBuilder
{
private Computer _computer = new Computer();
public ComputerBuilder SetCPU(string cpu)
{
_computer.CPU = cpu;
return this;
}
public ComputerBuilder SetGPU(string gpu)
{
_computer.GPU = gpu;
return this;
}
public ComputerBuilder SetRAM(int ram)
{
if (ram < 4 || ram > 128)
throw new ArgumentException("ОЗУ должно быть от 4 до 128 ГБ");
_computer.RAM = ram;
return this;
}
public ComputerBuilder SetStorage(int storage)
{
_computer.Storage = storage;
return this;
}
public ComputerBuilder EnableWiFi()
{
_computer.HasWiFi = true;
return this;
}
public ComputerBuilder SetOS(string os)
{
_computer.OS = os;
return this;
}
public Computer Build()
{
// Проверяем, что все обязательные поля заполнены
if (string.IsNullOrEmpty(_computer.CPU))
throw new InvalidOperationException("Не указан процессор!");
if (_computer.RAM == 0)
throw new InvalidOperationException("Не указана ОЗУ!");
return _computer;
}
}
// Использование:
// var gamingPC = new ComputerBuilder()
// .SetCPU("Intel Core i7")
// .SetGPU("NVIDIA RTX 3080")
// .SetRAM(32)
// .SetStorage(1000)
// .EnableWiFi()
// .SetOS("Windows 11")
// .Build();
// gamingPC.ShowSpecs();
Пример 4: Адаптер для работы со старыми и новыми датчиками
// Старый датчик — выдаёт температуру в Фаренгейтах
public class OldSensor
{
public double GetTemperatureFahrenheit()
{
// Имитация показаний датчика
return 98.6; // Температура тела в F
}
}
// Новый интерфейс — все датчики должны выдавать Цельсии
public interface INewSensor
{
double GetTemperatureCelsius();
}
// Адаптер — преобразует Фаренгейты в Цельсии
public class SensorAdapter : INewSensor
{
private OldSensor _oldSensor;
public SensorAdapter(OldSensor oldSensor)
{
_oldSensor = oldSensor;
}
public double GetTemperatureCelsius()
{
// Получаем температуру в Фаренгейтах
double fahrenheit = _oldSensor.GetTemperatureFahrenheit();
// Преобразуем в Цельсии
double celsius = (fahrenheit - 32) * 5.0 / 9.0;
return Math.Round(celsius, 1);
}
}
// Новая система, которая работает только с Цельсиями
public class TemperatureMonitor
{
private INewSensor _sensor;
public TemperatureMonitor(INewSensor sensor)
{
_sensor = sensor;
}
public void ShowTemperature()
{
double temp = _sensor.GetTemperatureCelsius();
Console.WriteLine($"Текущая температура: {temp}°C");
if (temp > 37.5)
Console.WriteLine("⚠️ Внимание! Высокая температура!");
}
}
// Демонстрация
class Program
{
static void Main()
{
// У нас есть старый датчик
var oldSensor = new OldSensor();
// Создаём адаптер, который делает его совместимым с новой системой
var adapter = new SensorAdapter(oldSensor);
// Новая система работает через адаптер
var monitor = new TemperatureMonitor(adapter);
monitor.ShowTemperature();
}
}
Пример 5: Наблюдатель для системы уведомлений
// Интерфейс наблюдателя
public interface INotificationObserver
{
void OnNotification(string title, string message);
}
// Конкретные наблюдатели
public class EmailNotifier : INotificationObserver
{
private string _email;
public EmailNotifier(string email) => _email = email;
public void OnNotification(string title, string message)
{
Console.WriteLine($"[EMAIL] Отправлено на {_email}: {title} - {message}");
}
}
public class SmsNotifier : INotificationObserver
{
private string _phoneNumber;
public SmsNotifier(string phone) => _phoneNumber = phone;
public void OnNotification(string title, string message)
{
Console.WriteLine($"[SMS] Отправлено на {_phoneNumber}: {title} - {message}");
}
}
public class ConsoleNotifier : INotificationObserver
{
public void OnNotification(string title, string message)
{
Console.WriteLine($"[CONSOLE] {title}: {message}");
}
}
// Издатель — управляет подписками и рассылкой
public class NotificationService
{
private List<INotificationObserver> _observers = new List<INotificationObserver>();
// Подписаться на уведомления
public void Subscribe(INotificationObserver observer)
{
_observers.Add(observer);
Console.WriteLine($"Подписчик добавлен. Всего подписчиков: {_observers.Count}");
}
// Отписаться от уведомлений
public void Unsubscribe(INotificationObserver observer)
{
_observers.Remove(observer);
Console.WriteLine($"Подписчик удалён. Осталось: {_observers.Count}");
}
// Отправить уведомление всем подписчикам
public void SendNotification(string title, string message)
{
Console.WriteLine($"
📢 Отправка уведомления: "{title}"");
foreach (var observer in _observers)
{
observer.OnNotification(title, message);
}
}
}
// Демонстрация
class Program
{
static void Main()
{
var notificationService = new NotificationService();
// Создаём подписчиков
var emailNotifier = new EmailNotifier("user@example.com");
var smsNotifier = new SmsNotifier("+7-999-123-45-67");
var consoleNotifier = new ConsoleNotifier();
// Подписываем их
notificationService.Subscribe(emailNotifier);
notificationService.Subscribe(smsNotifier);
notificationService.Subscribe(consoleNotifier);
// Отправляем уведомление
notificationService.SendNotification("Важное сообщение", "Обновление системы в 23:00");
// Отписываем одного
notificationService.Unsubscribe(smsNotifier);
// Отправляем ещё одно
notificationService.SendNotification("Напоминание", "Не забудьте сохранить файлы!");
}
}
Пример 6: Стратегия (Strategy) — разные способы сортировки
// Интерфейс стратегии сортировки
public interface ISortStrategy
{
void Sort(int[] array);
}
// Конкретные стратегии
public class BubbleSort : ISortStrategy
{
public void Sort(int[] array)
{
Console.WriteLine("Сортировка пузырьком:");
int n = array.Length;
for (int i = 0; i < n - 1; i++)
{
for (int j = 0; j < n - i - 1; j++)
{
if (array[j] > array[j + 1])
{
// Меняем местами
(array[j], array[j + 1]) = (array[j + 1], array[j]);
}
}
}
}
}
public class QuickSort : ISortStrategy
{
public void Sort(int[] array)
{
Console.WriteLine("Быстрая сортировка:");
QuickSortRecursive(array, 0, array.Length - 1);
}
private void QuickSortRecursive(int[] array, int low, int high)
{
if (low < high)
{
int pi = Partition(array, low, high);
QuickSortRecursive(array, low, pi - 1);
QuickSortRecursive(array, pi + 1, high);
}
}
private int Partition(int[] array, int low, int high)
{
int pivot = array[high];
int i = low - 1;
for (int j = low; j < high; j++)
{
if (array[j] < pivot)
{
i++;
(array[i], array[j]) = (array[j], array[i]);
}
}
(array[i + 1], array[high]) = (array[high], array[i + 1]);
return i + 1;
}
}
// Контекст — использует стратегию
public class Sorter
{
private ISortStrategy _strategy;
// Можно изменить стратегию в любой момент
public void SetStrategy(ISortStrategy strategy)
{
_strategy = strategy;
}
public void SortArray(int[] array)
{
if (_strategy == null)
throw new InvalidOperationException("Стратегия не установлена!");
// Делаем копию массива, чтобы не портить оригинал
int[] copy = new int[array.Length];
Array.Copy(array, copy, array.Length);
_strategy.Sort(copy);
Console.WriteLine($"Результат: [{string.Join(", ", copy)}]
");
}
}
// Демонстрация
class Program
{
static void Main()
{
int[] numbers = { 64, 34, 25, 12, 22, 11, 90 };
var sorter = new Sorter();
// Используем пузырьковую сортировку
sorter.SetStrategy(new BubbleSort());
sorter.SortArray(numbers);
// Меняем стратегию на быструю сортировку
sorter.SetStrategy(new QuickSort());
sorter.SortArray(numbers);
}
}
Пример 7: Декоратор (Decorator) — добавление функционала кофе
// Базовый интерфейс для напитка
public interface ICoffee
{
string GetDescription();
double GetCost();
}
// Конкретный напиток — просто кофе
public class SimpleCoffee : ICoffee
{
public string GetDescription() => "Обычный кофе";
public double GetCost() => 50.0; // 50 рублей
}
// Базовый декоратор — содержит ссылку на оборачиваемый объект
public abstract class CoffeeDecorator : ICoffee
{
protected ICoffee _coffee;
public CoffeeDecorator(ICoffee coffee)
{
_coffee = coffee;
}
public virtual string GetDescription() => _coffee.GetDescription();
public virtual double GetCost() => _coffee.GetCost();
}
// Конкретные декораторы — добавляют свои функции
public class MilkDecorator : CoffeeDecorator
{
public MilkDecorator(ICoffee coffee) : base(coffee) { }
public override string GetDescription() => _coffee.GetDescription() + ", с молоком";
public override double GetCost() => _coffee.GetCost() + 20.0;
}
public class SugarDecorator : CoffeeDecorator
{
private int _sugarAmount; // количество сахара (кубиков)
public SugarDecorator(ICoffee coffee, int sugarAmount = 1) : base(coffee)
{
_sugarAmount = sugarAmount;
}
public override string GetDescription() =>
$"{_coffee.GetDescription()}, с {_sugarAmount} кубик(а/ов) сахара";
public override double GetCost() => _coffee.GetCost() + 5.0 * _sugarAmount;
}
public class WhippedCreamDecorator : CoffeeDecorator
{
public WhippedCreamDecorator(ICoffee coffee) : base(coffee) { }
public override string GetDescription() => _coffee.GetDescription() + ", со взбитыми сливками";
public override double GetCost() => _coffee.GetCost() + 30.0;
}
// Демонстрация
class Program
{
static void Main()
{
// Начинаем с простого кофе
ICoffee coffee = new SimpleCoffee();
Console.WriteLine($"{coffee.GetDescription()}: {coffee.GetCost()} руб.");
// Добавляем молоко
coffee = new MilkDecorator(coffee);
Console.WriteLine($"{coffee.GetDescription()}: {coffee.GetCost()} руб.");
// Добавляем сахар
coffee = new SugarDecorator(coffee, 2);
Console.WriteLine($"{coffee.GetDescription()}: {coffee.GetCost()} руб.");
// Добавляем взбитые сливки
coffee = new WhippedCreamDecorator(coffee);
Console.WriteLine($"{coffee.GetDescription()}: {coffee.GetCost()} руб.");
// Можно создать сложный заказ сразу:
Console.WriteLine("
--- Сложный заказ сразу ---");
ICoffee specialCoffee = new WhippedCreamDecorator(
new SugarDecorator(
new MilkDecorator(
new SimpleCoffee()
), 3
)
);
Console.WriteLine($"{specialCoffee.GetDescription()}: {specialCoffee.GetCost()} руб.");
}
}
Сравнение с Python: чем похоже и чем отличается
Паттерны проектирования — это концепция, которая не зависит от языка программирования. Вы можете использовать их и в C#, и в Python, и в Java, и в JavaScript. Но реализация будет отличаться из-за особенностей языка.
| Аспект | C# (.NET) | Python |
|---|---|---|
| Типизация | Строгая статическая — все типы известны на этапе компиляции | Динамическая — тип определяется во время выполнения |
| Интерфейсы | Явные интерфейсы (ключевое слово interface) |
Неявные (duck typing) — «если это крякает как утка, значит это утка» |
| Singleton | Нужно писать вручную или использовать Lazy<T> |
Можно использовать модуль (модуль в Python — это Singleton по умолчанию) |
| Декоратор | Реализуется через классы и наследование | Есть встроенный синтаксис @decorator для функций |
| Наблюдатель | Нужно писать свою реализацию | Есть встроенные события (observers) через библиотеки |
| Производительность | Выше (компилируемый язык) | Ниже (интерпретируемый язык) |
| Сложность кода | Больше boilerplate-кода (шаблонного) | Меньше кода, более лаконичный |
Пример Singleton на Python для сравнения:
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
print("Создан новый экземпляр")
return cls._instance
# Использование:
# s1 = Singleton()
# s2 = Singleton()
# print(s1 is s2) # True — это один и тот же объект
Важно: В Python многие паттерны реализуются проще и короче, но это не значит, что они не нужны. Просто Python берёт на себя часть работы «под капотом». В C# вы контролируете всё вручную, что даёт больше гибкости и производительности.
Типичные ошибки и подводные камни
1. «Золотой молоток» — использование паттернов везде
Са