← Назад к вопросам

Что такое DependencyResolver?

2.7 Senior🔥 101 комментариев
#C# и ООП#Паттерны проектирования

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI7 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Что такое DependencyResolver в контексте Unity?

DependencyResolver (резолвер зависимостей) — это не встроенный класс движка Unity, а паттерн проектирования и инфраструктурный компонент, широко используемый при создании масштабируемых и тестируемых проектах на Unity. По своей сути, это сервис или контейнер, ответственный за автоматическое создание объектов и внедрение в них зависимостей (Dependency Injection, DI) в соответствии с заданной конфигурацией.

В Unity, в отличие от ASP.NET Core, нет "коробочного" DependencyResolver. Однако эта концепция реализуется с помощью популярных библиотек, таких как Zenject (Extenject), VContainer, StrangeIoC или собственного простого решения на основе паттерна Service Locator или Inversion of Control (IoC) контейнера.

Основная цель и принцип работы

Главная цель — отделить создание и связывание зависимостей (логики, сервисов, моделей данных) от кода, который их использует. Это делает код:

  • Слабо связанным (loosely coupled): классы зависят от абстракций (интерфейсов), а не от конкретных реализаций.
  • Тестируемым: в юнит-тесты легко подставлять заглушки (mock-объекты).
  • Масштабируемым: добавление новой реализации или изменение зависимостей происходит в одном месте — конфигурации резолвера.

Принцип работы можно описать так:

  1. Регистрация: на этапе инициализации приложения (например, в Installer или Composition Root) в контейнер регистрируются типы (например, интерфейс IAudioService и его реализация AudioService).
  2. Разрешение (Resolve): Когда какому-то объекту (например, PlayerController) требуется зависимость IAudioService, он запрашивает её не через new AudioService(), а у контейнера. Контейнер находит зарегистрированную реализацию, создаёт её экземпляр (возможно, синглтон) и возвращает.
  3. Внедрение: Зависимость может быть внедрена через конструктор, публичное поле или метод (в зависимости от возможностей библиотеки).

Пример реализации в Unity с использованием Zenject

Рассмотрим на практическом примере с игровым сохранением.

  1. Определяем интерфейс и его реализацию:
// Абстракция
public interface ISaveService
{
    void SaveData(string key, object data);
    T LoadData<T>(string key);
}

// Конкретная реализация, использующая PlayerPrefs
public class PlayerPrefsSaveService : ISaveService
{
    public void SaveData(string key, object data)
    {
        string json = JsonUtility.ToJson(data);
        PlayerPrefs.SetString(key, json);
        PlayerPrefs.Save();
    }

    public T LoadData<T>(string key)
    {
        if (PlayerPrefs.HasKey(key))
        {
            string json = PlayerPrefs.GetString(key);
            return JsonUtility.FromJson<T>(json);
        }
        return default;
    }
}
  1. Класс, который зависит от этого сервиса:
public class GameProgressManager
{
    private readonly ISaveService _saveService; // Зависимость

    // Внедрение зависимости через конструктор
    public GameProgressManager(ISaveService saveService)
    {
        _saveService = saveService;
    }

    public void SaveLevelProgress(int level, int score)
    {
        var progress = new LevelProgress { Level = level, Score = score };
        _saveService.SaveData("LevelProgress", progress);
    }
}
  1. Установка (Installer) — место конфигурации DependencyResolver:
using Zenject;

public class GameInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        // Регистрируем ISaveService как синглтон, связав его с реализацией PlayerPrefsSaveService.
        // Контейнер (DependencyResolver) теперь "знает", что при запросе ISaveService
        // нужно вернуть один общий экземпляр PlayerPrefsSaveService.
        Container.Bind<ISaveService>().To<PlayerPrefsSaveService>().AsSingle();

        // Регистрируем сам GameProgressManager.
        // Когда Zenject будет создавать GameProgressManager, он автоматически
        // "разрешит" (resolve) зависимость ISaveService и передаст её в конструктор.
        Container.Bind<GameProgressManager>().AsSingle();
    }
}
  1. Использование: Класс GameProgressManager никогда не создаёт PlayerPrefsSaveService напрямую. За него это делает контейнер (наш DependencyResolver). В другом месте кода мы можем запросить GameProgressManager, и все его зависимости будут автоматически удовлетворены.

Преимущества использования данного подхода в Unity

  • Упрощение модификации: Чтобы сменить систему сохранения на файловую или облачную, нужно изменить лишь одну строку в Installer, не трогая GameProgressManager.
  • Управление жизненным циклом: Контейнер позволяет легко управлять временем жизни объектов (AsTransient, AsSingle, AsCached), что критично для синглтонов (например, GameManager, AudioManager).
  • Улучшение тестирования: Для тестов можно создать TestInstaller, который привяжет ISaveService к MockSaveService.
  • Чистота кода: Классы становятся более чистыми и сфокусированными на своей прямой ответственности.

Сравнение с Service Locator

Часто в качестве простейшего DependencyResolver выступает паттерн Service Locator (статический класс, хранящий ссылки на сервисы). Однако DI-контейнеры предпочтительнее, так как они делают зависимости явными (через конструктор) и избегают скрытых глобальных зависимостей, которые есть у Service Locator.

Вывод: В Unity DependencyResolver — это ключевой компонент архитектуры, реализуемый с помощью DI-контейнеров. Он выступает центральным "мозгом", который знает, как создавать и связывать все части приложения, значительно повышая гибкость, поддерживаемость и тестируемость кода. Использование библиотек вроде Zenject или VContainer является стандартом для средних и крупных проектов.