Передовые технологии производства
Использование шаблона завода для абстрактных уровней доступа к данным в ядре .net
Table of Contents
Введение: Проблема абстракции уровня доступа к данным в .NET Core
Современные приложения .NET Core часто взаимодействуют с данными через несколько бэкэндов хранения — реляционные базы данных, хранилища NoSQL, кэши в памяти или сторонние API. По мере роста приложения тесная связь между бизнес-логикой и конкретными реализациями доступа к данным становится бременем для поддержания работоспособности. Изменение базового поставщика баз данных или добавление нового механизма хранения может пульсировать по всей кодовой базе, вызывая рекомпиляцию и обширное регрессионное тестирование. Фабричный шаблон предлагает проверенное решение, инкапсулируя создание объектов, позволяя переключать реализации доступа к данным без изменения потребляющего кода. Эта статья предоставляет авторитетное, готовое к производству руководство по реализации фабричного шаблона для абстракции уровня доступа к данным в .NET Core, с примерами кода, лучшими практиками и интеграцией со встроенным контейнером впрыска зависимостей (DI).
Оригинальное название: The Factory Pattern: Beyond Simple Object Creation
По своей сути, модель завода является шаблоном креационного дизайна, который делегирует ответственность за создание объектов в специализированный фабричный класс. Эта модель подпадает под три распространенных варианта: простая фабрика, метод производства и абстрактная фабрика. Для абстрагирования уровней доступа к данным простая фабрика (или статическая фабрика) часто является наиболее прагматичной отправной точкой, но мы также рассмотрим, как превратить ее в более гибкую абстрактную фабрику, когда существует несколько семейств продуктов.
Основная мотивация для использования фабрики в доступе к данным .NET Core заключается в поддержке открытого / закрытого принципа: классы должны быть открыты для расширения, но закрыты для модификации. Путем направления всех объектов доступа к данным через фабрику вы можете ввести новые реализации (например, переход от ядра Entity Framework к Dapper), не затрагивая бизнес-логику. Кроме того, фабричный шаблон способствует инверсии зависимости - модули высокого уровня больше не зависят от деталей низкого уровня; оба зависят от абстракций (интерфейсов).
Фабричный шаблон не является серебряной пулей. Он наиболее эффективен, когда у вас есть четко определенный набор взаимозаменяемых стратегий доступа к данным и вам нужно изолировать логику создания от остальной части приложения. В простых монолитных приложениях с одним источником данных накладные расходы завода могут быть не оправданы.
Оригинальное название: Designing the Abstraction: The Interface Contract
Первым шагом в использовании Фабричного шаблона для доступа к данным является определение общего интерфейса, который должны реализовать все конкретные репозитории. Этот интерфейс служит контрактом между вашей бизнес-логикой и уровнем данных. В .NET Core такой интерфейс часто отображает стандартные операции CRUD, но вы можете адаптировать его к потребностям вашего домена.
Пример: Общий репозиторийный интерфейс
public interface IDataRepository<TKey, TEntity> where TEntity : class
{
Task<IEnumerable<TEntity>> GetAllAsync();
Task<TEntity?> GetByIdAsync(TKey id);
Task AddAsync(TEntity entity);
Task UpdateAsync(TEntity entity);
Task DeleteAsync(TKey id);
}
Этот общий интерфейс хорошо работает, когда вам нужны согласованные операции с данными по разным типам объектов. Однако для простоты в этой статье мы будем придерживаться необщего интерфейса, который работает на одном типе объектов. Принципы остаются идентичными.
Специализированные интерфейсы для продвинутых сценариев
В реальных проектах вам могут понадобиться методы репозитория, которые выходят за рамки базового CRUD, такие как заданные запросы, фильтрация или агрегация. Рассмотрим определение отдельных интерфейсов для операций только для чтения и записи, чтобы следовать принципу разделения интерфейса. Например:
public interface IDataReader<TEntity>
{
Task<IEnumerable<TEntity>> QueryAsync(Expression<Func<TEntity, bool>> predicate);
Task<TEntity?> GetByIdAsync(int id);
}
public interface IDataWriter<TEntity>
{
Task InsertAsync(TEntity entity);
Task UpdateAsync(TEntity entity);
Task DeleteAsync(int id);
}
Затем Factory Pattern может создать комбинированную реализацию, которая удовлетворяет оба интерфейса, когда это необходимо, или вернуть отдельные объекты для чтения и записи, если вы выберете подход CQRS.
Реализация конкретных классов доступа к данным
После определения интерфейса вы создаете конкретные реализации для каждой технологии доступа к данным. Ниже приведены примеры с использованием Entity Framework Core и Dapper, двух наиболее распространенных .NET Core фреймворков доступа к данным.
Основополагающая реализация Entity Framework
public class EfDataRepository : IDataRepository
{
private readonly AppDbContext _context;
public EfDataRepository(AppDbContext context)
{
_context = context;
}
public async Task<IEnumerable<DataItem>> GetAllAsync()
{
return await _context.Set<DataItem>().AsNoTracking().ToListAsync();
}
public async Task<DataItem?> GetByIdAsync(int id)
{
return await _context.Set<DataItem>().FindAsync(id);
}
// Additional methods omitted for brevity
}
Обратите внимание, что ожидает экземпляр , который в приложении .NET Core обычно вводится через контейнер DI. Это согласуется с заводским шаблоном: заводу потребуется доступ к контейнеру DI для разрешения таких зависимостей.
Реализация Dapper
public class DapperDataRepository : IDataRepository
{
private readonly IDbConnection _connection;
private readonly string _connectionString;
public DapperDataRepository(IConfiguration configuration)
{
_connectionString = configuration.GetConnectionString("DefaultConnection");
_connection = new SqlConnection(_connectionString);
}
public async Task<IEnumerable<DataItem>> GetAllAsync()
{
var sql = "SELECT * FROM DataItems";
return await _connection.QueryAsync<DataItem>(sql);
}
public async Task<DataItem?> GetByIdAsync(int id)
{
var sql = "SELECT * FROM DataItems WHERE Id = @Id";
return await _connection.QueryFirstOrDefaultAsync<DataItem>(sql, new { Id = id });
}
}
Обе реализации выполняют один и тот же контракт, но используют совершенно другую механику. Завод решит, какую из них выполнить на основе условий выполнения.
Строительство завода: от простого к абстрактному
Сама фабрика инкапсулирует логику принятия решений. Статической фабрики достаточно, когда выбор реализации зависит исключительно от значения конфигурации (например, ключа настроек приложения). Однако, когда решение требует контекста времени выполнения (роль пользователя, арендатора, флаг функции), более уместна нестатическая фабрика, которая принимает дополнительные параметры.
Статическая простая фабрика (настроенная на конфигурацию)
public static class DataRepositoryFactory
{
public static IDataRepository Create(IServiceProvider serviceProvider, string provider)
{
return provider switch
{
"EntityFramework" => serviceProvider.GetRequiredService<EfDataRepository>(),
"Dapper" => ActivatorUtilities.CreateInstance<DapperDataRepository>(serviceProvider),
_ => throw new NotSupportedException($"Data provider '{provider}' is not supported.")
};
}
}
Эта фабрика использует для создания типов, которые имеют зависимости, зарегистрированные в контейнере DI. решена непосредственно, потому что она уже зарегистрирована (наряду с . создается с использованием , который обрабатывает впрыск конструктора без необходимости ручной регистрации самого хранилища — только его зависимости (например, ) должны быть в контейнере.
Абстрактная фабрика для нескольких семейств продуктов
Когда ваше приложение требует различных типов объектов доступа к данным (например, один для заказов, другой для инвентаря, каждый потенциально с использованием другого двигателя хранения), Simple Factory становится громоздким. Абстрактная фабрика определяет интерфейс для создания семейств связанных объектов без указания их конкретных классов. Каждая конкретная фабрика производит полный набор объектов доступа к данным для конкретного технологического стека.
public interface IDataAccessFactory
{
IDataRepository CreateOrderRepository();
IDataRepository CreateInventoryRepository();
// etc.
}
public class EfDataAccessFactory : IDataAccessFactory
{
private readonly AppDbContext _context;
public EfDataAccessFactory(AppDbContext context) => _context = context;
public IDataRepository CreateOrderRepository() => new EfOrderRepository(_context);
public IDataRepository CreateInventoryRepository() => new EfInventoryRepository(_context);
}
public class DapperDataAccessFactory : IDataAccessFactory
{
private readonly string _connectionString;
public DapperDataAccessFactory(IConfiguration configuration) => _connectionString = configuration.GetConnectionString("DefaultConnection");
public IDataRepository CreateOrderRepository() => new DapperOrderRepository(_connectionString);
public IDataRepository CreateInventoryRepository() => new DapperInventoryRepository(_connectionString);
}
Абстрактная фабрика более мощная, но и более тяжелая. Забронируйте ее для приложений, где вам нужно заменить целые стека доступа к данным (например, заменив все репозитории Entity Framework репозиториями Dapper) сразу, а не отдельные реализации.
Интеграция фабрики с инъекцией .NET Core Dependency
Истинная сила Фабричного шаблона в .NET Core возникает, когда вы комбинируете его с контейнером DI. Вместо регистрации конкретных типов хранилища регистрируйте завод и позвольте ему производить соответствующую реализацию по требованию.
Регистрация в Program.cs (или Startup.cs)
builder.Services.AddTransient<EfDataRepository>();
builder.Services.AddTransient<IDataRepository>(sp =>
{
var config = sp.GetRequiredService<IConfiguration>();
var provider = config.GetValue<string>("DataProvider");
return DataRepositoryFactory.Create(sp, provider);
});
В этой регистрации бетон регистрируется временно (чтобы контейнер DI мог разрешать его внутри завода). регистрация использует делегата завода, который читает значение от и делегатов на статический завод. Теперь любой потребитель, который вводит , получит правильную реализацию, не зная, какой именно.
Использование названных сервисов для поддержки мульти-провайдеров
Если вашему приложению нужны несколько репозиториев, использующих разных поставщиков одновременно (например, один для исторических данных с использованием Dapper и один для данных в реальном времени с использованием Entity Framework), вы можете зарегистрировать названные методы производства или использовать шаблон словаря.
builder.Services.AddSingleton<IDataRepositoryFactory>(sp =>
{
var config = sp.GetRequiredService<IConfiguration>();
var providers = config.GetSection("DataProviders").Get<Dictionary<string, string>>();
return new DataRepositoryFactory(sp, providers);
});
Затем завод может представить метод , который возвращает соответствующую реализацию на основе параметра , который вы можете передать в качестве зависимости с помощью или пользовательского шаблона впрыска.
Реальные выгоды и сценарии
Давайте рассмотрим конкретные ситуации, когда фабричная модель сияет в слоях доступа к данным .NET Core.
1. Тестирование и пересмешивание
Бизнес-логика тестирования блоков становится тривиальной, когда вы можете заменить макетное хранилище. Фабрика может быть настроена на тестовой установке для возврата макета или реализации в памяти. Например, во время интеграционных тестов установите ключ конфигурации на и установите завод, который возвращает , поддерживаемый .
2. Многопользовательские приложения
Каждый арендатор может потребовать другую технологию хранения данных из-за лицензирования, унаследованных ограничений или географического распределения. Фабрика может изучить метаданные арендатора во время выполнения и создать соответствующее хранилище - возможно, один арендатор использует SQL Server через Entity Framework, другой использует PostgreSQL через Dapper, а третий использует Azure Cosmos DB.
3.Особенности Toggles и постепенная миграция
При переходе с одного ORM на другой (например, с Entity Framework на Dapper для критически важных запросов производительности) завод позволяет вам постепенно маршрутизировать трафик. Вы можете создать систему флага функций, которая для процента пользователей или конкретных конечных точек возвращает новый репозиторий Dapper, в то время как большая часть приложения по-прежнему использует Entity Framework. Если возникают проблемы, верните флаг с нулевыми изменениями кода.
Тестирование абстрактного уровня доступа к данным
Хорошо спроектированная фабрика делает тестирование простым. Вы можете создать тестовую фабрику, которая возвращает макеты реализаций или легкие версии в памяти ваших хранилищ данных. Например:
public class TestDataRepositoryFactory
{
public static IDataRepository CreateInMemory()
{
return new InMemoryDataRepository();
}
}
просто содержит и реализует интерфейс с использованием операций в памяти. Единичные тесты могут затем инстанцировать бизнес-услуги с этой тестовой фабрикой без необходимости подключения к базе данных или макетирования фреймворков. Интеграционные тесты все еще могут использовать реальную фабрику для проверки против тестовой базы данных.
Лучшие практики и общие подводные камни
Не преувеличивайте
Если ваше приложение никогда не будет переключать поставщиков доступа к данным, абстракция только увеличивает сложность без пользы. Используйте ее только тогда, когда у вас есть четкое, текущее требование для нескольких взаимозаменяемых реализаций.
Избегайте строго настроенных поставщиков
Использование магических строк для имен провайдеров (например, или ) является хрупким. Вместо этого определите перечисление или используйте объект конфигурации с сильными типами.
public enum DataProvider
{
EntityFramework,
Dapper,
InMemory
}
Тщательно управлять жизнью
Репозитории часто содержат соединения или экземпляры DbContext. Убедитесь, что контейнер DI правильно управляет их областью применения. Для веб-приложений срок службы с прицелом (по запросу) обычно подходит для EF Core DbContext, но соединения Dapper могут нуждаться в переходном или прицельном использовании на основе стратегии объединения соединений. Завод не должен кэшировать репозитории на неопределенный срок, если они не являются апатридами.
Подумайте об использовании фабрики с регистром
Для крупных приложений рассмотрите возможность реализации Плана реестра рядом с заводом. Реестр хранит предварительно настроенные заводы, на которых подается ключ с помощью некоторого идентификатора (например, идентификатор арендатора, логическое имя). Клиент запрашивает реестр для соответствующей фабрики, которая затем создает хранилище. Этот шаблон особенно полезен в архитектурах с несколькими арендаторами, где каждый арендатор может иметь другую стратегию доступа к данным.
Сравнение с альтернативными моделями
Модель «Фабрика» — не единственный способ абстрактного доступа к данным. Вот как она сравнивается с другими распространенными подходами в .NET Core:
- Стратегический шаблон — Похож на Factory, но основное внимание уделяется инкапсулирующим алгоритмам (например, различным стратегиям сортировки или фильтрации), а не созданию объектов. Фабричный шаблон — это креационный шаблон; Стратегический шаблон — поведенческий. Они могут дополнять друг друга: фабрика может вернуть стратегию.
- Паттер декоратора — Полезно для добавления сквозных проблем (кэширование, ведение журнала, повторная запись) в репозиторий без изменения его кода.Фабрика может украсить созданный ею репозиторий, объединив оба шаблона.
- План репозитория (без заводской) — Прямое впрыскивание бетонного хранилища через DI работает для простых приложений.Фабрика добавляет гибкость, когда тип бетона меняется.
Внешние ресурсы и дальнейшее чтение
Чтобы углубить свое понимание структуры фабрики и доступа к данным .NET Core, обратитесь к следующим авторитетным источникам:
- Руководство Microsoft по архитектуре приложений .NET — шаблоны проектирования доступа к данным
- Документация на инъекцию базовой зависимости ASP.NET
- Рефакторинг гуру — шаблон фабричного метода (с примерами кода)
- Microsoft — DDD-ориентированные микросервисы: применение шаблона хранилища с заводом
Вывод: создание для перемен
Фабричный шаблон обеспечивает дисциплинированный способ управления вариациями уровней доступа к данным, позволяя вам менять бэкэнды хранения, внедрять новые технологии и тестировать бизнес-логику в изоляции. В .NET Core объединение Фабричного шаблона со встроенным DI-контейнером дает чистую, поддерживаемую архитектуру, которая уважает принципы SOLID. Начните с малого: определите интерфейс, реализуйте два конкретных класса, создайте статический завод и проведите его через DI. По мере роста вашего приложения развивайте завод в абстрактный завод или завод, управляемый реестром, для обработки нескольких семейств репозиториев. Инвестируя в эту абстракцию, вы в будущем защитите свой код доступа к данным от неизбежных изменений в требованиях к хранению и выборе технологий.