Классы инжекторов зависимостей: статический синглтон против параметра

Этот вопрос использует Unity в качестве примера, но сам вопрос может касаться любой инфраструктуры IoC DI.

Чтение документации Unity и чтение рекомендаций для модульных тестов, а также опыт работы с моими прошлыми компаниями, я видел, что есть два, казалось бы, несовместимых шаблона проектирования для настройки Unity и IoC:

  • Оберните IoC в одноэлементном классе. Официальные примеры Unity, а в некоторых ответах SO есть некоторый статический одноэлементный класс DependencyFactory, в котором вы храните контейнер IoC, и в любом месте модуля можно использовать ваш singleton API для разрешения зависимостей, в которых они нуждаются.
  • Передайте контейнер IoC в функцию. Раннее место, над которым я работал, сделал это, когда почти все функции принимали IUnityContainer непосредственно в качестве параметра. Я также видел сообщения о модульном тестировании, предлагающие устанавливать зависимости в параметрах, чтобы явно показывать, на что зависит код.

Эти два, похоже, не согласны, и я не уверен, какой из них считается лучше. Для модульного тестирования, №2 было намного проще, когда я работал в своей старой компании, потому что мне не нужно было беспокоиться об изменении каких-либо синглонов - я просто передал именно то, что было необходимо, и мы закончили. Кроме того, зависимости очень заметны, если у вас есть функция, запрашивающая IDateTimeProvider как параметр вместо того, чтобы просто получать ее из синглтона в другом месте. Unit test сообщения в блоге, такие как этот показывают примеры непосредственного прохождения в контейнере.

Однако, # 1 намного проще для целей разработки из-за того, насколько он доступен. Это делает его очень легким из любого места в коде, чтобы иметь доступ к чему-либо, и это также, по-видимому, является официальным рекомендуемым способом делать что-то.

Предполагая, что либо выбор # 1, либо # 2, мы завершаем собственный IUnityContainer в нашем собственном абстрактном API, который, по вашему мнению, лучше - мы имеем наш контейнер IoC в качестве одноэлементного или в качестве параметра we проходят везде? И, если # 1, как мы изменим наши однопользовательские зависимости на основе каждого теста, чтобы получить ту же гибкость управления, которую мы получаем С# 2 для наших модульных тестов и макетов?

Ответ 1

Я попробую ответить на вопрос "И, если # 1, как мы изменим наши однопользовательские зависимости на основе каждого теста, чтобы получить ту же гибкость управления, что и мы, с №2 для тестирования модулей и макетов?"

Проблема:

Синглтон имеет проблемы с тестовой способностью, вы не можете тестировать поддельные объекты, потому что singleton использует жесткую кодировку для экземпляра. это потому, что, когда вы начинаете использовать одноэлементный класс во всем мире, вы жестко кодируете этот экземпляр!

В приведенном ниже примере обратите внимание, что SingletonDatabase.Instance жестко закодирован, и "bruce" должно быть настоящим студенческим именем со степенью на вашем db.

public class SingletonPublicApi
{
    public int GetGrade(string student)
    {
        return SingletonDatabase.Instance.GetGrade(student);
    }
}

public class SingletonTests
{
    [Test]
    public void SingletonStudentGradeTest()
    {
        var mySingletonApi = new SingletonPublicApi();
        Assert.IsEqual(mySingletonApi.GetGrade("Bruce"),100);
    }
}

Решение:

Вместо работы с жестким кодированным экземпляром мы можем установить api, способный конфигурировать, который получает объект базы данных в качестве параметра:

public class ConfigurableSingletonPublicApi
{
    private IDatabase _database;
    public ConfigurableSingletonPublicApi(IDatabase database)
    {
        this._database = database //don't forget to check if null
    }
    public int GetGrade(string student)
    {
        return this._database.GetGrade(student);
    }
}

Теперь мы все установили для определения базы данных фиктивных данных:

public class ConfigerableSingletonTests
{
    [Test]
    public void SingletonStudentGradeTest()
    {
        var mySingletonApi = new ConfigurableSingletonPublicApi(new DummyDataBase);
        Assert.IsEqual(mySingletonApi.GetGrade("student1"),100);
        Assert.IsEqual(mySingletonApi.GetGrade("student2"),90);
    }
}

public class DummyDataBase : IDatabase
{
    public int GetGrade(string student)
    {
        return new Dictionary<string,int> { ["student1"] = 100 , ["student2"] = 90 };
    }
}