Первичные Конструкторы для Внедрения Зависимостей. Окончание
Начало
Ловушка, которая отталкивает многих
Параметры первичного конструктора не являются полями только для чтения. Когда вы используете параметр первичного конструктора непосредственно в теле класса (как мы делали в классе сервиса), компилятор захватывает его как изменяемую переменную. За кулисами не генерируется поля только для чтения.
Т.е. вы можете изменить значение параметра:
public class OrderService(
IOrderRepository repo,
ILogger<OrderService> logger)
{
// …
public void SomeOtherMethod()
{
repo = null!;
logger = null!;
}
}
Этот код компилируется без ошибок и предупреждений. Если вам необходимы гарантии неизменяемости, явно присвойте параметр полю только для чтения:
public class OrderService(
IOrderRepository repo,
ILogger<OrderService> logger)
{
private readonly IOrderRepository _repo = repo;
private readonly ILogger<OrderService> _logger = logger;
// …
}
Но теперь мы потеряли большую часть преимуществ первичных конструкторов. Мы вернулись к объявлениям полей и присваиваниям, только с другим синтаксисом.
На практике вряд ли вы столкнётесь с этой проблемой в классе сервиса, получаемого из DI. Маловероятно, что вы случайно измените значение logger посреди метода. Но это может создать проблемы в классах сущностей или объектов-значений, где неизменяемость действительно важна. Это единственное место, где по-прежнему стоит проявлять осторожность.
Где использовать традиционные конструкторы
Вот случаи, когда стоит придерживаться традиционного подхода:
1. Сложная логика валидации
Если нужно проверять параметры перед их присваиванием, понадобится тело конструктора:
public class EmailAddress
{
private readonly string _value;
public EmailAddress(string value)
{
if (string.IsNullOrWhiteSpace(value)
|| !value.Contains('@'))
throw new ArgumentException(
"Неверный email.", nameof(value));
_value = value;
}
}
Первичные конструкторы не позволяют разместить логику валидации до выполнения тела класса.
2. Множественные перегрузки конструкторов
Первичные конструкторы поддерживают одну сигнатуру конструктора. Если вам нужны перегрузки, придётся связывать обычные конструкторы с помощью
this(…), что быстро приводит к беспорядку.3. Слишком много параметров
Как только вы достигнете 5 и более зависимостей, строка первичного конструктора станет трудночитаемой. Хотя, тогда класс, вероятно, будет иметь слишком много обязанностей, и рефакторинг будет лучшим решением, чем ухищрения с форматированием.
Итого
- Можно использовать первичные конструкторы для всех классов сервисов в DI. Экономия на шаблонном коде того стоит.
- Они полезны для создания сущностей, когда вы хотите обеспечить обязательность параметров на уровне типа.
- Параметры основного конструктора сохраняются как изменяемые переменные, а не поля только для чтения. Это единственное, о чём нужно помнить. Эта изменяемость не страшна в классах сервисов, потому что в этом контексте это вряд ли вызовет реальные ошибки.
- Стоит придерживаться традиционных конструкторов для типов с валидацией, множественными перегрузками или со слишком большим количеством зависимостей.
Переход того стоит. Классы сервисов короче, их проще сканировать, а ловушка изменяемости не так уж страшна.
Источник: https://www.milanjovanovic.tech/blog/why-i-switched-to-primary-constructors-for-di-in-csharp