Итак, мы уже разобрались, что зависимость должна приходить в класс снаружи, а не создаваться им самостоятельно. Теперь логично возникает следующий вопрос:
А как именно передавать эти зависимости внутрь?
На самом деле, способов не так много — всего три. Но каждый имеет своё предназначение, свои плюсы и свои опасные моменты.
public class OrderService
{
private readonly ILogger _logger;
public OrderService(ILogger logger)
{
_logger = logger;
}
}
Почему именно этот вариант считается «золотым стандартом»? Потому что он очень честно говорит: «Для того чтобы этот класс работал, ему нужен логгер. Без логгера он существовать не может.»
Это делает зависимость обязательной и явной. После создания объекта ты точно знаешь, что он полностью готов к работе: все обязательные компоненты уже переданы. Это важно и для читаемости, и для архитектуры, и для тестирования.
public class DamageService
{
public void ApplyDamage(Character target, IDamageCalculator calculator)
{
int damage = calculator.Calculate();
target.TakeDamage(damage);
}
}
DamageService не хранит калькулятор урона. Ему он нужен только в момент расчёта — и только для конкретного вызова. Это удобно там, где зависимость является одноразовой, контекстной, или нужна не всегда.
public class Character
{
public IWeapon? Weapon { get; set; }
}
Персонаж может существовать и без оружия — просто он будет бить руками. Поэтому логично сделать оружие опциональным и выдать его позже.
Плюсы: зависимость можно подменить в любой момент; удобно для опциональных или меняющихся зависимостей.
Минусы: объект может оказаться «неполным» (если свойство забыли установить); сложнее гарантировать корректное состояние.
Поэтому этот способ обычно используют для второстепенных зависимостей, а не для ключевых. Большая ошибка новичков — думать, что DI = «всё через конструктор». Нет. DI — это идея передачи зависимостей извне, а как именно вы передадите — зависит от задачи.
Представим, что у нас есть система авторизации. Сервис, который проверяет логин и пароль, не может работать без хранилища пользователей — это его фундаментальная зависимость.
Пример внедрения через конструктор:
public class AuthService
{
private readonly IUserRepository _repository;
public AuthService(IUserRepository repository)
{
_repository = repository;
}
public bool Login(string username, string password)
{
var user = _repository.GetByUsername(username);
return user != null && user.Password == password;
}
}
AuthService без хранилища пользователей — просто бесполезен. Поэтому зависимость передаётся через конструктор: она обязательная.
Пример внедрения через метод. Представьте игру, где урон может считаться по-разному: критический удар, магический удар, урон от яда. Сервису нанесения урона не нужно хранить калькулятор урона всё время — он нужен только в момент атаки.
public class CombatService
{
public void Attack(Character target, IDamageCalculator calculator)
{
int damage = calculator.CalculateDamage();
target.TakeDamage(damage);
}
}
Пример внедрения через свойство. Допустим, у нас есть персонаж в игре. У него может быть оружие, а может не быть — игрок может снять его, поменять или подобрать новое.
public class Player
{
public IWeapon? Weapon { get; set; }
public void Attack()
{
if (Weapon == null)
Console.WriteLine("Игрок бьёт кулаками!");
else
Weapon.Hit();
}
}
Оружие — это не обязательная часть персонажа. Игрок может снять его, поменять, подобрать новое — это зависимость, которая меняется на лету, поэтому её удобно передавать через свойство.
Хотя внедрение через свойства и существует, в реальных проектах оно используется заметно реже. Свойство больше подходит для обычных данных — названия UI-окна, громкости звука, цвета темы. Когда речь идёт именно о поведении или о важной части логики класса, разработчики почти всегда предпочитают метод, ведь метод ясно показывает, что это действие, а не пассивная установка значения.
Constructor Injection — внедрение зависимости через конструктор. Самый распространённый способ — делает зависимость обязательной и явной.
Method Injection — внедрение зависимости через параметр метода. Используется, когда зависимость нужна только на время выполнения одной операции.
Property Injection — внедрение зависимости через публичное свойство. Подходит для опциональных зависимостей, которые могут меняться.
Конструктор — для обязательных зависимостей (золотой стандарт). Метод — для контекстных одноразовых зависимостей. Свойство — для редких опциональных случаев.