Паттерн "Фабрика" в C# — когда хватает DI-контейнера
Модель и масштаб — Основы БД, опорные темы, проектирование БД, пакетная работа. Карта — о разделе.
В порождающих паттернах разобраны три уровня "фабрики" — простая фабрика (приём, не паттерн GoF), фабричный метод и абстрактная фабрика. Здесь фокус на современном .NET: где классическая абстрактная фабрика даёт структуру, а где ту же задачу проще решает контейнер зависимостей.
Play ITЗагрузка интерактивного демо…
Что делает Abstract Factory
Абстрактная фабрика задаёт интерфейс для создания семейства связанных объектов без привязки клиента к конкретным классам.
Упрощённый пример для двух СУБД:
Код ITЗагрузка примера кода…
Клиент получает IDbFactory и работает с контрактом:
public class OrderRepository
{
private readonly IDbFactory _factory;
public OrderRepository(IDbFactory factory) => _factory = factory;
public void Save(Order order)
{
using var connection = _factory.CreateConnection();
using var command = _factory.CreateCommand();
// ...
}
}
Переключение PostgreSQL/SQL Server выполняется заменой фабрики.
Play ITЗагрузка интерактивного демо…
Цена классической схемы
Вместе с паттерном появляются дополнительные сущности:
- интерфейс фабрики;
- реализация под каждое семейство;
- новые методы при добавлении каждого типа продукта (
IDbParameter,IDbTransactionи т.д.).
Эта структура оправдана в больших доменах, где семейства объектов живут долго и реально расширяются. Для прикладного кода ASP.NET Core такая схема часто тяжеловесна по сравнению с возможностями DI-контейнера.
DI-контейнер решает ту же задачу
Контейнер зависимостей тоже выступает фабрикой: он создаёт и возвращает реализации по контракту.
Регистрация по окружению:
// Program.cs
if (builder.Environment.IsDevelopment())
{
builder.Services.AddScoped<IDbConnection, NpgsqlConnection>();
}
else
{
builder.Services.AddScoped<IDbConnection, SqlConnection>();
}
OrderRepository получает IDbConnection через конструктор. Переключение окружений сосредоточено в конфигурации приложения.
Для большинства сервисов это даёт тот же уровень развязки при меньшем количестве кода.
Выбор реализации в рантайме
Если решение зависит от входных данных (например, платёжный провайдер, выбранный пользователем), удобно использовать фабричный делегат:
builder.Services.AddScoped<Func<string, IPaymentProvider>>(sp => key => key switch
{
"stripe" => sp.GetRequiredService<StripeProvider>(),
"paypal" => sp.GetRequiredService<PayPalProvider>(),
_ => throw new ArgumentException($"Unknown provider: {key}")
});
В .NET 8 можно зарегистрировать keyed services:
builder.Services.AddKeyedScoped<IPaymentProvider, StripeProvider>("stripe");
builder.Services.AddKeyedScoped<IPaymentProvider, PayPalProvider>("paypal");
Получение keyed-сервиса:
Код ITЗагрузка примера кода…
Где Abstract Factory по-прежнему полезен
Паттерн остаётся практичным, когда:
| Ситуация | Почему фабрика полезна |
|---|---|
| Строгие семейства совместимых объектов | Один контракт создаёт набор, который согласован между собой |
| UI-темы и платформенные наборы | Кнопка, диалог и поля ввода берутся из одного style-пакета |
| Код вне DI-контейнера | Библиотеки, утилиты, изолированные модули и unit-тесты без host-пайплайна |
| Явный публичный API фреймворка | Контракт фабрики задаёт точку расширения для внешних плагинов |
Здесь фабрика даёт читаемую архитектуру и явную модель расширения.
Чек-лист выбора
| Вопрос | Выбор |
|---|---|
| Одна зависимость, выбор при старте приложения | Регистрация в DI |
| Несколько реализаций по ключу в рантайме | Делегат-фабрика или keyed services |
| Нужны согласованные семейства продуктов | Abstract Factory |
| Модуль работает автономно без контейнера | Abstract Factory или Factory Method |
| Паттерн увеличил слой абстракций без новых сценариев | Упростить до DI-конфигурации |
Для прикладных сервисов ASP.NET Core чаще хватает DI-конфигурации.
Abstract Factory хорошо работает как инструмент архитектуры библиотек и платформенных слоёв с жёсткими семействами объектов.
Итог
Abstract Factory сохраняет ценность как паттерн проектирования. В современном C# многие прикладные сценарии покрываются DI-контейнером, фабричным делегатом и keyed services. Выигрыш приходит от выбора минимальной формы абстракции под конкретную задачу.