Внедрение через конструктор и интерфейсы
Применяйте внедрение через конструктор и проектирование на основе интерфейсов, чтобы ослабить связанность компонентов и обеспечить модульное тестирование.
«Внедрение через конструктор и интерфейсы» — бесплатный урок C# Academy на CoddyKit. Это урок 3 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения C# Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс C# Academy содержит 4 уроков всего.
Основы внедрения через конструктор
Внедрение через конструктор — наиболее распространённый шаблон DI. Класс объявляет свои зависимости как параметры конструктора, а контейнер предоставляет их во время создания экземпляра.
Благодаря этому зависимости становятся явными, обязательными и видимыми.
Определение интерфейса
Начните с интерфейса, задающего контракт. Это позволяет заменять разные реализации (настоящую, имитацию или реализацию в памяти), не изменяя зависимый код.
public interface IProductRepository
{
Task<Product?> GetByIdAsync(int id);
Task<IEnumerable<Product>> GetAllAsync();
Task AddAsync(Product product);
}Реализация интерфейса
Конкретная реализация выполняет настоящую работу — обращается к базе данных, отправляет HTTP-запросы и так далее. Потребителю не нужно знать, какую именно реализацию он получает.
public class SqlProductRepository : IProductRepository
{
private readonly AppDbContext _db;
public SqlProductRepository(AppDbContext db) => _db = db;
public async Task<Product?> GetByIdAsync(int id)
=> await _db.Products.FindAsync(id);
public async Task<IEnumerable<Product>> GetAllAsync()
=> await _db.Products.ToListAsync();
public async Task AddAsync(Product product)
{
_db.Products.Add(product);
await _db.SaveChangesAsync();
}
}Использование через конструктор
Слой служб принимает IProductRepository как параметр конструктора. Он ничего не знает о SqlProductRepository — только о контракте интерфейса.
public class ProductService
{
private readonly IProductRepository _repo;
private readonly ILogger<ProductService> _logger;
public ProductService(
IProductRepository repo,
ILogger<ProductService> logger)
{
_repo = repo;
_logger = logger;
}
public async Task<Product?> GetProductAsync(int id)
{
_logger.LogInformation("Fetching product {Id}", id);
return await _repo.GetByIdAsync(id);
}
}Регистрация цепочки
Зарегистрируйте каждую службу в цепочке. Контейнер автоматически разрешит весь граф зависимостей — ProductService → IProductRepository → AppDbContext.
builder.Services.AddDbContext<AppDbContext>(opt =>
opt.UseSqlite("Data Source=app.db"));
builder.Services.AddScoped<IProductRepository, SqlProductRepository>();
builder.Services.AddScoped<ProductService>();Проверка с имитацией
Поскольку зависимости внедряются через интерфейсы, модульные проверки могут передать имитацию или подставной объект, не обращаясь к базе данных. Это одно из главных преимуществ DI.
// Using NSubstitute or Moq in a unit test
var mockRepo = Substitute.For<IProductRepository>();
mockRepo.GetByIdAsync(1).Returns(new Product { Id = 1, Name = "Widget" });
var logger = Substitute.For<ILogger<ProductService>>();
var svc = new ProductService(mockRepo, logger);
var result = await svc.GetProductAsync(1);
Assert.Equal("Widget", result?.Name);Несколько параметров конструктора
У класса может быть много параметров конструктора. Контейнер разрешит их все, если каждый параметр зарегистрирован. Делайте конструкторы целенаправленными: слишком большое количество параметров указывает на то, что у класса слишком много обязанностей.
public class OrderService
{
public OrderService(
IOrderRepository orders,
IProductRepository products,
IEmailSender email,
ILogger<OrderService> logger)
{
// all injected by DI container
}
}Отказ от антишаблона локатора служб
Не (NOT) внедряйте IServiceProvider и не вызывайте GetService внутри бизнес-методов. Это скрывает зависимости и усложняет проверки. Предпочитайте внедрение через конструктор, чтобы код был понятным и пригодным для проверки.
// BAD
public void Process(IServiceProvider sp)
{
var repo = sp.GetService<IProductRepository>(); // hidden dep!
}
// GOOD
public class Processor
{
private readonly IProductRepository _repo;
public Processor(IProductRepository repo) => _repo = repo;
}Синтаксис основного конструктора (C# 12)
В C# 12 появились основные конструкторы для всех типов классов. Они позволяют объявлять параметры непосредственно в объявлении класса и писать более лаконичный код DI.
// C# 12 primary constructor
public class ProductService(
IProductRepository repo,
ILogger<ProductService> logger)
{
public async Task<Product?> GetAsync(int id)
{
logger.LogInformation("Getting {Id}", id);
return await repo.GetByIdAsync(id);
}
}Замена реализаций
Ключевое преимущество DI на основе интерфейсов: реализацию можно заменить одной строкой, не изменяя код, который её использует. Это удобно при смене баз данных, служб рассылки или флагов возможностей.
// Switch from SQL to in-memory for integration tests
if (environment.IsEnvironment("Test"))
builder.Services.AddScoped<IProductRepository, InMemoryProductRepository>();
else
builder.Services.AddScoped<IProductRepository, SqlProductRepository>();Практический пример: контроллер с DI
Контроллеры ASP.NET Core изначально поддерживают внедрение через конструктор. Платформа разрешает все параметры и передаёт их контроллеру.
[ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase
{
private readonly ProductService _svc;
public ProductsController(ProductService svc) => _svc = svc;
[HttpGet("{id}")]
public async Task<IActionResult> Get(int id)
{
var product = await _svc.GetProductAsync(id);
return product is null ? NotFound() : Ok(product);
}
}Быстрая проверка
Каково главное преимущество программирования через интерфейсы при использовании DI?
Итоги: внедрение через конструктор и интерфейсы
Главное:
- Объявляйте зависимости как параметры конструктора — контейнер предоставит их
- Определяйте интерфейсы для контрактов служб; внедряйте интерфейс, а не конкретный тип
- Интерфейсы упрощают создание имитаций в модульных проверках и замену реализаций
- Основные конструкторы C# 12 сокращают шаблонный код в классах с большим количеством зависимостей DI
- Избегайте антишаблона локатора служб — зависимости должны оставаться явными
Часто задаваемые вопросы
Урок «Внедрение через конструктор и интерфейсы» бесплатный?
Да — полный текст урока «Внедрение через конструктор и интерфейсы» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс C# Academy, подпишись на CoddyKit PRO. Курс C# Academy содержит 4 уроков всего.
Чему я научусь в уроке «Внедрение через конструктор и интерфейсы»?
Применяйте внедрение через конструктор и проектирование на основе интерфейсов, чтобы ослабить связанность компонентов и обеспечить модульное тестирование. Ты практикуешь C# Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать C# Academy?
Предыдущий опыт не требуется. C# Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 3 из 4.
Сколько времени занимает урок «Внедрение через конструктор и интерфейсы»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке C# Academy?
Да. Каждый урок C# Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Основы контейнера внедрения зависимостей
- Время жизни служб: Transient, Scoped, Singleton
- Внедрение через конструктор и интерфейсы
- Шаблоны фабрики и параметров