Паттерны проектирования в C# и .NET: руководство с примерами
Для кого эта статья:
- Профессиональные разработчики на C# и .NET
- Люди, интересующиеся архитектурным проектированием и паттернами
Тем, кто хочет улучшить качество своего кода и навыки разработки
Профессиональные разработчики на C# и .NET отличаются от кодеров-любителей не столько знанием синтаксиса языка, сколько умением структурировать код и применять проверенные архитектурные решения. Паттерны проектирования — это тот инструмент, который превращает хаотичный, трудноподдерживаемый код в элегантную систему взаимодействующих компонентов. В этом руководстве мы рассмотрим ключевые паттерны проектирования для C# и .NET с практическими примерами, которые вы сможете применить в своих проектах уже сегодня — будь то корпоративное приложение, веб-сервис или десктопная программа. 🚀
Что такое паттерны проектирования и их роль в C# разработке
Паттерны проектирования — это типовые решения часто возникающих проблем в архитектуре программного обеспечения. Они представляют собой формализованные лучшие практики, которые программист может использовать для решения общих задач при проектировании приложения или системы.
В контексте C# и .NET, паттерны приобретают особое значение благодаря объектно-ориентированной природе языка и обширной экосистеме фреймворков. Они позволяют:
- Ускорить процесс разработки за счет использования проверенных временем решений
- Избежать типичных проблем, с которыми сталкиваются неопытные разработчики
- Повысить читаемость и поддерживаемость кода
- Облегчить командную работу благодаря общему словарю проектных решений
- Упростить рефакторинг и тестирование кода
Классически все паттерны проектирования делятся на три основные категории:
| Категория | Назначение | Примеры |
|---|---|---|
| Порождающие | Механизмы создания объектов | Singleton, Factory Method, Builder, Abstract Factory, Prototype |
| Структурные | Компоновка объектов и классов | Adapter, Bridge, Composite, Decorator, Facade, Proxy |
| Поведенческие | Взаимодействие между объектами | Observer, Strategy, Command, Iterator, State, Template Method |
Важно понимать, что паттерны проектирования тесно связаны с SOLID принципами объектно-ориентированного программирования. Многие из них, по сути, являются инструментами для реализации этих принципов в коде.
Алексей Петров, ведущий архитектор .NET
В 2019 году я присоединился к проекту, где код представлял собой запутанный монолит с классами по 5000 строк. Когда команде потребовалось добавить новую функциональность, это часто приводило к каскадным изменениям и непредсказуемым багам. Первым шагом к исправлению ситуации было внедрение базовых паттернов проектирования.
Мы начали с выделения абстрактных фабрик для создания взаимозависимых объектов, применили паттерн Repository для работы с данными и Strategy для инкапсуляции различных алгоритмов бизнес-логики. За шесть месяцев количество производственных инцидентов сократилось на 64%, а скорость внедрения новых функций увеличилась примерно вдвое. Примечательно, что мы не переписывали систему с нуля, а постепенно улучшали архитектуру, применяя подходящие паттерны проектирования.

Порождающие паттерны в C#: Singleton, Factory Method, Builder
Порождающие паттерны отвечают за эффективные механизмы создания объектов и являются фундаментом для построения гибкой архитектуры. Рассмотрим три наиболее часто используемых порождающих паттерна в экосистеме .NET.
Singleton (Одиночка)
Паттерн Singleton гарантирует, что класс имеет только один экземпляр и предоставляет глобальную точку доступа к нему. В C# существует несколько способов реализации этого паттерна, но thread-safe реализация с использованием статического конструктора является предпочтительной:
public sealed class Logger
{
private static readonly Logger instance = new Logger();
// Приватный конструктор предотвращает создание экземпляра извне
private Logger() { }
public static Logger Instance
{
get { return instance; }
}
public void Log(string message)
{
Console.WriteLine($"[LOG]: {message}");
}
}
// Использование
Logger.Instance.Log("Пример использования Singleton паттерна");
Когда использовать Singleton в .NET проектах:
- Для классов конфигурации приложения
- Для сервисов логирования
- Для пулов соединений с базой данных
- Для кэш-менеджеров
Однако помните, что чрезмерное использование Singleton может усложнить тестирование и привести к тесным связям между компонентами.
Factory Method (Фабричный метод)
Фабричный метод предоставляет интерфейс для создания объектов, но позволяет подклассам менять тип создаваемых объектов.
public abstract class DocumentCreator
{
// Фабричный метод
public abstract IDocument CreateDocument();
// Другие методы
public void OpenDocument()
{
IDocument document = CreateDocument();
document.Open();
}
}
public class PdfDocumentCreator : DocumentCreator
{
public override IDocument CreateDocument()
{
return new PdfDocument();
}
}
public class WordDocumentCreator : DocumentCreator
{
public override IDocument CreateDocument()
{
return new WordDocument();
}
}
public interface IDocument
{
void Open();
void Save();
}
public class PdfDocument : IDocument
{
public void Open() { /* Логика открытия PDF */ }
public void Save() { /* Логика сохранения PDF */ }
}
public class WordDocument : IDocument
{
public void Open() { /* Логика открытия Word */ }
public void Save() { /* Логика сохранения Word */ }
}
Builder (Строитель)
Builder отделяет процесс конструирования сложного объекта от его представления, что позволяет использовать один и тот же процесс для создания разных представлений.
public class Email
{
public string From { get; set; }
public string To { get; set; }
public string Subject { get; set; }
public string Body { get; set; }
public bool IsHtml { get; set; }
public List<string> Attachments { get; set; }
}
public class EmailBuilder
{
private Email _email = new Email();
public EmailBuilder From(string address)
{
_email.From = address;
return this;
}
public EmailBuilder To(string address)
{
_email.To = address;
return this;
}
public EmailBuilder Subject(string subject)
{
_email.Subject = subject;
return this;
}
public EmailBuilder Body(string body, bool isHtml = false)
{
_email.Body = body;
_email.IsHtml = isHtml;
return this;
}
public EmailBuilder AddAttachment(string path)
{
if (_email.Attachments == null)
_email.Attachments = new List<string>();
_email.Attachments.Add(path);
return this;
}
public Email Build()
{
return _email;
}
}
// Использование
Email email = new EmailBuilder()
.From("sender@example.com")
.To("recipient@example.com")
.Subject("Тема письма")
.Body("<p>Содержание письма</p>", true)
.AddAttachment("path/to/file.pdf")
.Build();
В современных версиях C# вы также можете использовать инициализаторы объектов и параметры по умолчанию для создания более простых Builder-подобных шаблонов.
Структурные паттерны для .NET приложений
Структурные паттерны определяют способы организации классов и объектов для формирования более сложных структур, обеспечивая гибкость и эффективность программного дизайна. В экосистеме .NET эти паттерны особенно полезны при работе с различными компонентами фреймворков.
Adapter (Адаптер)
Адаптер позволяет классам с несовместимыми интерфейсами работать вместе, преобразуя интерфейс одного класса в интерфейс, ожидаемый клиентом.
// Существующий интерфейс, который нужно адаптировать
public class ThirdPartyJsonConverter
{
public string ConvertToJson(object data)
{
// Преобразование в JSON
return "JSON representation";
}
}
// Целевой интерфейс, который ожидает клиентский код
public interface IDataSerializer
{
string Serialize(object data);
object Deserialize(string serialized, Type type);
}
// Адаптер, приводящий ThirdPartyJsonConverter к IDataSerializer
public class JsonConverterAdapter : IDataSerializer
{
private readonly ThirdPartyJsonConverter _jsonConverter;
public JsonConverterAdapter(ThirdPartyJsonConverter jsonConverter)
{
_jsonConverter = jsonConverter;
}
public string Serialize(object data)
{
return _jsonConverter.ConvertToJson(data);
}
public object Deserialize(string serialized, Type type)
{
// Здесь может быть вызов другого метода ThirdPartyJsonConverter
// или собственная реализация, если такого метода нет
throw new NotImplementedException();
}
}
Максим Соколов, разработчик корпоративных приложений
Когда наша команда получила задачу интегрировать унаследованную ERP-систему с новым веб-порталом на ASP.NET Core, мы столкнулись с проблемой: ERP имела собственный формат данных и API, совершенно несовместимый с нашими моделями.
Вместо написания "специального кода" для каждого случая взаимодействия, мы разработали систему адаптеров, используя соответствующий паттерн. Каждый адаптер инкапсулировал логику преобразования данных между системами. Это решение оказалось настолько удачным, что когда через год компания решила заменить ERP, нам потребовалось переписать только адаптеры, не затрагивая основную бизнес-логику портала.
Самым ценным уроком было то, что правильно примененный структурный паттерн может создать эффективный буферный слой между несовместимыми системами, значительно уменьшая стоимость будущих изменений.
Decorator (Декоратор)
Декоратор позволяет динамически добавлять объектам новую функциональность, оборачивая их в полезные "обертки". Этот паттерн идеально подходит для расширения функциональности без изменения оригинального кода.
// Базовый интерфейс
public interface INotificationService
{
void Send(string message);
}
// Конкретная реализация
public class EmailNotificationService : INotificationService
{
public void Send(string message)
{
Console.WriteLine($"Sending email: {message}");
}
}
// Базовый декоратор
public abstract class NotificationDecorator : INotificationService
{
protected readonly INotificationService _wrappedService;
public NotificationDecorator(INotificationService service)
{
_wrappedService = service;
}
public virtual void Send(string message)
{
_wrappedService.Send(message);
}
}
// Конкретный декоратор для логирования
public class LoggingNotificationDecorator : NotificationDecorator
{
public LoggingNotificationDecorator(INotificationService service)
: base(service) { }
public override void Send(string message)
{
Console.WriteLine($"[LOG] Notification being sent: {message}");
base.Send(message);
Console.WriteLine("[LOG] Notification sent successfully");
}
}
// Конкретный декоратор для шифрования
public class EncryptionNotificationDecorator : NotificationDecorator
{
public EncryptionNotificationDecorator(INotificationService service)
: base(service) { }
public override void Send(string message)
{
string encrypted = Encrypt(message);
base.Send(encrypted);
}
private string Encrypt(string input)
{
// Логика шифрования
return $"ENCRYPTED[{input}]";
}
}
Facade (Фасад)
Фасад предоставляет унифицированный интерфейс к набору интерфейсов в подсистеме, определяя высокоуровневый интерфейс, который упрощает использование этой подсистемы.
// Подсистема для проверки кредита
public class CreditCheck
{
public bool HasGoodCredit(string customerId)
{
// Сложная логика проверки кредита
return true;
}
}
// Подсистема для работы с банковским счетом
public class BankAccount
{
public bool HasSufficientFunds(string accountId, decimal amount)
{
// Проверка баланса и т.д.
return true;
}
public void Withdraw(string accountId, decimal amount)
{
Console.WriteLine($"Withdrawn {amount} from account {accountId}");
}
}
// Подсистема для отправки уведомлений
public class NotificationSystem
{
public void SendTransactionConfirmation(string customerId, decimal amount)
{
Console.WriteLine($"Transaction confirmation sent to customer {customerId}");
}
}
// Фасад, упрощающий использование всех этих подсистем
public class BankingFacade
{
private CreditCheck _creditCheck = new CreditCheck();
private BankAccount _bankAccount = new BankAccount();
private NotificationSystem _notificationSystem = new NotificationSystem();
public bool ProcessPayment(string customerId, string accountId, decimal amount)
{
if (!_creditCheck.HasGoodCredit(customerId))
{
Console.WriteLine("Payment failed: bad credit rating");
return false;
}
if (!_bankAccount.HasSufficientFunds(accountId, amount))
{
Console.WriteLine("Payment failed: insufficient funds");
return false;
}
_bankAccount.Withdraw(accountId, amount);
_notificationSystem.SendTransactionConfirmation(customerId, amount);
Console.WriteLine("Payment processed successfully");
return true;
}
}
| Паттерн | Когда применять в .NET | Примеры в фреймворке .NET |
|---|---|---|
| Adapter | При интеграции сторонних библиотек, работе с унаследованным кодом | System.IO.TextReader и его адаптеры для разных источников |
| Decorator | Для добавления функциональности существующим классам без их изменения | StreamReader/StreamWriter как декораторы Stream |
| Facade | Для упрощения сложных API или интеграции нескольких подсистем | HttpClient как фасад над низкоуровневым HTTP API |
| Proxy | Контроль доступа, ленивая загрузка, кэширование | System.Runtime.Remoting.Proxies.RealProxy |
| Composite | Представление иерархии объектов как единого объекта | Windows Forms и WPF элементы управления |
Поведенческие паттерны и их применение в C# проектах
Поведенческие паттерны определяют способы взаимодействия между объектами и распределения ответственности между ними. Они особенно важны для создания гибкого, расширяемого кода, способного адаптироваться к изменяющимся требованиям. 🧩
Observer (Наблюдатель)
Паттерн Observer позволяет объекту (наблюдателю) следить и реагировать на изменения в другом объекте (субъекте). В .NET есть встроенная поддержка этого паттерна через события, но иногда полезно реализовать его явно:
public interface IObserver<T>
{
void Update(T subject);
}
public interface ISubject<T>
{
void Attach(IObserver<T> observer);
void Detach(IObserver<T> observer);
void Notify();
}
public class StockMarket : ISubject<StockMarket>
{
private List<IObserver<StockMarket>> _observers = new List<IObserver<StockMarket>>();
private decimal _stockPrice;
public decimal StockPrice
{
get => _stockPrice;
set
{
if (_stockPrice != value)
{
_stockPrice = value;
Notify();
}
}
}
public void Attach(IObserver<StockMarket> observer)
{
_observers.Add(observer);
}
public void Detach(IObserver<StockMarket> observer)
{
_observers.Remove(observer);
}
public void Notify()
{
foreach (var observer in _observers)
{
observer.Update(this);
}
}
}
public class StockTrader : IObserver<StockMarket>
{
private string _name;
public StockTrader(string name)
{
_name = name;
}
public void Update(StockMarket subject)
{
Console.WriteLine($"Trader {_name} noticed stock price changed to {subject.StockPrice}");
}
}
Strategy (Стратегия)
Strategy определяет семейство алгоритмов, инкапсулирует каждый из них и делает их взаимозаменяемыми. Это позволяет изменять алгоритм независимо от клиентов, которые его используют.
public interface IPaymentStrategy
{
void ProcessPayment(decimal amount);
}
public class CreditCardPayment : IPaymentStrategy
{
private string _cardNumber;
private string _expiryDate;
public CreditCardPayment(string cardNumber, string expiryDate)
{
_cardNumber = cardNumber;
_expiryDate = expiryDate;
}
public void ProcessPayment(decimal amount)
{
Console.WriteLine($"Processed credit card payment of ${amount}");
// Реальная логика обработки платежа по кредитной карте
}
}
public class PayPalPayment : IPaymentStrategy
{
private string _email;
public PayPalPayment(string email)
{
_email = email;
}
public void ProcessPayment(decimal amount)
{
Console.WriteLine($"Processed PayPal payment of ${amount}");
// Реальная логика обработки платежа через PayPal
}
}
public class ShoppingCart
{
private IPaymentStrategy _paymentStrategy;
private List<Item> _items = new List<Item>();
public void SetPaymentStrategy(IPaymentStrategy strategy)
{
_paymentStrategy = strategy;
}
public void AddItem(Item item)
{
_items.Add(item);
}
public void Checkout()
{
decimal total = _items.Sum(item => item.Price);
_paymentStrategy.ProcessPayment(total);
}
}
Command (Команда)
Command превращает запрос в самостоятельный объект, содержащий всю информацию о запросе. Это позволяет откладывать выполнение команд, ставить их в очередь, сохранять в истории и поддерживать отмену операций.
public interface ICommand
{
void Execute();
void Undo();
}
public class Document
{
public string Text { get; set; } = "";
public void AddText(string text)
{
Text += text;
}
public void RemoveText(int length)
{
if (length <= Text.Length)
Text = Text.Substring(0, Text.Length – length);
}
}
public class AddTextCommand : ICommand
{
private Document _document;
private string _textToAdd;
public AddTextCommand(Document document, string text)
{
_document = document;
_textToAdd = text;
}
public void Execute()
{
_document.AddText(_textToAdd);
}
public void Undo()
{
_document.RemoveText(_textToAdd.Length);
}
}
public class TextEditor
{
private Document _document = new Document();
private Stack<ICommand> _undoStack = new Stack<ICommand>();
public void AddText(string text)
{
var command = new AddTextCommand(_document, text);
command.Execute();
_undoStack.Push(command);
}
public void Undo()
{
if (_undoStack.Count > 0)
{
var command = _undoStack.Pop();
command.Undo();
}
}
public string GetText()
{
return _document.Text;
}
}
Template Method (Шаблонный метод)
Шаблонный метод определяет скелет алгоритма в базовом классе, позволяя подклассам переопределять определенные шаги алгоритма без изменения его структуры.
public abstract class ReportGenerator
{
// Шаблонный метод, определяющий алгоритм
public void GenerateReport()
{
CollectData();
ProcessData();
FormatReport();
DisplayReport();
}
// Обязательные шаги, которые должны быть реализованы подклассами
protected abstract void CollectData();
protected abstract void FormatReport();
// Общие шаги с реализацией по умолчанию
protected virtual void ProcessData()
{
Console.WriteLine("Processing data...");
}
// Общий шаг, который нельзя переопределить
private void DisplayReport()
{
Console.WriteLine("Displaying the report...");
}
}
public class PdfReportGenerator : ReportGenerator
{
protected override void CollectData()
{
Console.WriteLine("Collecting data for PDF report...");
}
protected override void FormatReport()
{
Console.WriteLine("Formatting PDF report...");
}
}
public class ExcelReportGenerator : ReportGenerator
{
protected override void CollectData()
{
Console.WriteLine("Collecting data for Excel report...");
}
protected override void FormatReport()
{
Console.WriteLine("Formatting Excel report...");
}
// Переопределение метода с реализацией по умолчанию
protected override void ProcessData()
{
base.ProcessData();
Console.WriteLine("Adding Excel-specific data processing...");
}
}
Поведенческие паттерны особенно ценны в сложных бизнес-приложениях, где требуется гибкая обработка событий, адаптация к меняющимся бизнес-правилам и эффективное управление взаимодействием между компонентами.
Паттерны проектирования для ASP.NET и WPF приложений
Веб-приложения на ASP.NET и десктопные приложения на WPF имеют свои особенности архитектуры, которые влияют на выбор и применение паттернов проектирования. Рассмотрим специфичные для этих платформ паттерны и адаптации классических паттернов. 💻
MVC и MVVM – фундаментальные архитектурные паттерны
Model-View-Controller (MVC) – базовый паттерн для ASP.NET MVC/Core, в то время как Model-View-ViewModel (MVVM) стал стандартом для WPF приложений. Оба отделяют пользовательский интерфейс от бизнес-логики, но имеют различные подходы к связыванию данных.
Пример MVVM в WPF:
// ViewModel
public class CustomerViewModel : INotifyPropertyChanged
{
private string _name;
public string Name
{
get => _name;
set
{
if (_name != value)
{
_name = value;
OnPropertyChanged(nameof(Name));
}
}
}
private ICommand _saveCommand;
public ICommand SaveCommand
{
get
{
return _saveCommand ?? (_saveCommand = new RelayCommand(
param => SaveCustomer(),
param => !string.IsNullOrEmpty(Name)
));
}
}
private void SaveCustomer()
{
// Логика сохранения клиента
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
// Реализация ICommand для WPF
public class RelayCommand : ICommand
{
private readonly Action<object> _execute;
private readonly Predicate<object> _canExecute;
public RelayCommand(Action<object> execute, Predicate<object> canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return _canExecute == null || _canExecute(parameter);
}
public void Execute(object parameter)
{
_execute(parameter);
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
}
Dependency Injection в ASP.NET Core
ASP.NET Core имеет встроенный контейнер для внедрения зависимостей, что позволяет эффективно применять паттерны IoC (Inversion of Control) и DI (Dependency Injection):
// Регистрация сервисов в Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// Singleton: один экземпляр на всё приложение
services.AddSingleton<IGlobalConfig, GlobalConfig>();
// Scoped: один экземпляр на HTTP запрос
services.AddScoped<IUserRepository, UserRepository>();
// Transient: новый экземпляр при каждом запросе
services.AddTransient<IEmailService, EmailService>();
}
// Использование в контроллере
public class UsersController : Controller
{
private readonly IUserRepository _userRepository;
private readonly IEmailService _emailService;
public UsersController(IUserRepository userRepository, IEmailService emailService)
{
_userRepository = userRepository;
_emailService = emailService;
}
[HttpPost]
public async Task<IActionResult> CreateUser(UserViewModel model)
{
var user = await _userRepository.CreateAsync(model.ToEntity());
await _emailService.SendWelcomeEmailAsync(user);
return RedirectToAction("Index");
}
}
Repository и Unit of Work для доступа к данным
Эти паттерны особенно полезны в ASP.NET приложениях для абстрагирования логики доступа к данным:
public interface IRepository<T> where T : class
{
IQueryable<T> GetAll();
T GetById(int id);
void Add(T entity);
void Update(T entity);
void Delete(T entity);
}
public interface IUnitOfWork
{
IRepository<User> Users { get; }
IRepository<Order> Orders { get; }
Task<int> SaveChangesAsync();
}
public class EfRepository<T> : IRepository<T> where T : class
{
protected readonly DbContext _dbContext;
public EfRepository(DbContext dbContext)
{
_dbContext = dbContext;
}
public IQueryable<T> GetAll()
{
return _dbContext.Set<T>();
}
public T GetById(int id)
{
return _dbContext.Set<T>().Find(id);
}
public void Add(T entity)
{
_dbContext.Set<T>().Add(entity);
}
public void Update(T entity)
{
_dbContext.Entry(entity).State = EntityState.Modified;
}
public void Delete(T entity)
{
_dbContext.Set<T>().Remove(entity);
}
}