Абстрактная фабрика даёт интерфейс для создания семейств связанных объектов без привязки к конкретным классам. В книге GoF это один из ключевых порождающих паттернов. В современном .NET он почти не нужен в классическом виде.
Допустим, приложение работает с разными базами данных. Для каждой нужны свои реализации подключения и команды:
public interface IDbFactory
{
IDbConnection CreateConnection();
IDbCommand CreateCommand();
}
public class PostgresFactory : IDbFactory
{
public IDbConnection CreateConnection() => new NpgsqlConnection();
public IDbCommand CreateCommand() => new NpgsqlCommand();
}
public class SqlServerFactory : IDbFactory
{
public IDbConnection CreateConnection() => new SqlConnection();
public IDbCommand CreateCommand() => new SqlCommand();
}
Клиентский код получает
IDbFactory и не знает, с какой базой работает. Переключение между PostgreSQL и SQL Server сводится к замене фабрики: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();
// ...
}
}
Выглядит аккуратно. Но посмотрите, сколько кода нужно написать ещё до первой строки бизнес-логики. Интерфейс фабрики, две реализации, и это только для двух продуктов. Добавьте третий, например,
IDbParameter и количество классов растёт линейно.Почему DI-контейнер делает то же самое
DI-контейнер по своей сути и есть абстрактная фабрика. Вы регистрируете конкретные реализации, а контейнер разрешает их по интерфейсу.
Тот же пример без паттерна:
// Program.cs
if (builder.Environment.IsDevelopment())
{
builder.Services.AddScoped<IDbConnection, NpgsqlConnection>();
}
else
{
builder.Services.AddScoped<IDbConnection, SqlConnection>();
}
Никаких фабричных иерархий. Класс
OrderRepository получает IDbConnection через конструктор и не знает, что за ним стоит. Переключение между окружениями (Development, Staging, Production) происходит в одном месте.Когда нужен выбор в рантайме
Иногда реализацию нельзя определить при старте приложения. Например, пользователь выбирает способ оплаты, и от этого зависит, какой платёжный провайдер использовать. Для этого не нужна фабричная иерархия.
Достаточно фабричного делегата:
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");Получение в конструкторе:
public class CheckoutService
{
private readonly IPaymentProvider _provider;
public CheckoutService(
[FromKeyedServices("stripe")] IPaymentProvider provider)
{
_provider = provider;
}
}
Или через
IServiceProvider, если ключ известен только в рантайме:var provider = serviceProvider
.GetRequiredKeyedService<IPaymentProvider>(userChoice);
Где абстрактная фабрика всё ещё уместна
Паттерн имеет смысл, когда нужно гарантировать совместимость объектов внутри семейства. Например, UI-фреймворк с темами, где кнопка, поле ввода и диалог должны принадлежать одному стилю. Фабрика не даст смешать Material-кнопку с Fluent-диалогом на уровне типов.
Ещё один случай. Код работает вне DI-контейнера. Библиотека, консольная утилита, unit-тесты с ручной подстановкой зависимостей. Там фабрика по-прежнему решает задачу.
Писать отдельную иерархию фабрик стоит только тогда, когда контейнер недоступен или когда важна строгая совместимость объектов внутри семейства.
➡️ Наша новостная подписка никогда не постареет
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
#il_люминатор