Внедрение зависимостей + внешний контекст + локатор сервисов

В последнее время я много читал о шаблонах проектирования приложений: о DI, антипаттернах SL, AOP и многом другом. Причина в том, что я хочу прийти к компромиссу в дизайне: слабо связанный, чистый и простой в работе. DI кажется ПОЧТИ как решение, за исключением одной проблемы: сквозных и необязательных зависимостей, ведущих к загрязнению конструктора или свойства. Итак, у меня есть собственное решение этой проблемы, и я хочу знать, что вы о нем думаете.

Марк Симанн (автор книги DI и известного заявления «SL - это анти-шаблон») в своей книге упоминает паттерн, называемый «Окружающий контекст». Хотя он говорит, что ему это не очень нравится, этот шаблон по-прежнему интересен: он похож на старый добрый синглтон, за исключением того, что он ограничен и предоставляет значение по умолчанию, поэтому нам не нужно проверять значение null. У него есть один недостаток - его нет и он не может знать о своей сфере действия и о том, как избавиться от него.

Итак, почему бы не применить здесь Service Locator? Он может решить проблему как определения объема, так и удаления объектов окружающего контекста. Прежде чем вы скажете, что это антипаттерн: это когда вы скрываете контракт. Но в нашем случае мы скрываем ДОПОЛНИТЕЛЬНЫЙ контракт, так что это не так уж и плохо, ИМО.

Вот код, чтобы показать, что я имею в виду:

public interface ILogger
{
    void Log(String text);
}

public interface ISomeRepository
{
    // skipped
}


public class NullLogger : ILogger
{
    #region ILogger Members

    public void Log(string text)
    {
        // do nothing
    }

    #endregion
}

public class LoggerContext
{
    public static ILogger Current
    {
        get
        {
            if(ServiceLocator.Current == null)
            {
                return new NullLogger();
            }
            var instance = ServiceLocator.Current.GetInstance<ILogger>();
            if (instance == null)
            {
                instance = new NullLogger();
            }
            return instance;
        }
    }
}

public class SomeService(ISomeRepository repository)
{
    public void DoSomething()
    {
        LoggerContext.Current.Log("Log something");
    }
}

Изменить: я понимаю, что задание не конкретного вопроса противоречит дизайну переполнения стека. Поэтому я отмечу как ответ сообщение, которое лучше всего описывает, почему этот дизайн плохой ИЛИ лучше дает лучшее решение (или, может быть, дополнение?). Но не предлагайте АОП, это хорошо, но это не решение, когда вы действительно хотите что-то сделать внутри своего кода.

Изменить 2: я добавил проверку для ServiceLocator.Current имеет значение null. Это то, что я намереваюсь сделать в своем коде: работать с настройками по умолчанию, когда SL не настроен.


person Dmitry Golubets    schedule 21.05.2012    source источник
comment
В вашем вопросе мне не хватает примера, в котором вы четко показываете, что вам нужно использовать LoggerContext.Current вместо того, чтобы вводить ILogger в свой код. По моему опыту, если вам нужно внедрить много ILogger зависимостей в ваш код, вы либо слишком много ведете журнал (вместо того, чтобы генерировать исключения), либо не соблюдаете SRP (и вам действительно нужен AOP). Взгляните на этот ответ: stackoverflow.com/questions/9892137/.   -  person Steven    schedule 21.05.2012


Ответы (2)


Вы можете добавить сквозные проблемы, используя созданные вручную декораторы или какой-то перехват (например, Castle DynamicProxy или расширение перехвата Unity).

Таким образом, вам вообще не нужно вводить ILogger в свои основные бизнес-классы.

person Sebastian Weber    schedule 21.05.2012
comment
Да, я везде читаю одно и то же предложение, но мне оно кажется неправильным. Иногда журналы нужно писать внутри метода в некоторой ветке if. Перехват не решает этого. И ILogger - это просто пример, но могут быть и другие интерфейсы с методами для чтения некоторых значений. - person Dmitry Golubets; 21.05.2012
comment
@DmitryGolubets: если вы считаете, что ведение журнала является важным аспектом вашего класса, сделайте это очевидным и введите его с помощью инъекции конструктора. Если это для отслеживания входных значений и исключений журналирования (я рассматриваю ведение журнала и трассировку как принципиально разные вещи!), Используйте декораторы или перехватчики. Но никогда не скрывайте зависимости! Это тебя укусит скорее раньше, чем позже. Всегда. - person Sebastian Weber; 21.05.2012
comment
Если вы хотите, чтобы регистратор был частью логики инкапсулированного класса, может быть, лучше разделить ваш класс на несколько детализированных классов и украсить каждый из них? - person Dmitriy Startsev; 21.05.2012
comment
Я предпочитаю более мелкозернистую структуру классов, а не несколько больших классов. Но если возможность использовать декораторы для ведения журнала - единственная причина разделить ваш код на более мелкие части, я бы поставил под сомнение такой подход. - person Sebastian Weber; 21.05.2012

Проблема с окружающим контекстом, который вы предлагаете, заключается в том, что он значительно усложняет тестирование. По нескольким причинам:

  1. Во время выполнения ваших модульных тестов всегда должен быть действительный экземпляр, зарегистрированный в "ServiceLocator.Current". Но не только это, он должен быть зарегистрирован с действительным ILogger.
  2. Когда вам нужно использовать поддельный регистратор в тесте (кроме простого NullLogger), вам нужно будет настроить свой контейнер, поскольку нет возможности подключиться к нему, но поскольку контейнер является синглтоном, все другие тесты будут использовать тот самый регистратор.
  3. Будет нетривиально (и пустой тратой времени) создать решение, которое будет работать, когда ваши модульные тесты выполняются параллельно (как это делает MSTest по умолчанию).

Все эти проблемы могут быть решены путем простого внедрения ILogger экземпляров в сервисы, которые в этом нуждаются, вместо использования внешнего контекста.

И если многие классы в вашей системе зависят от этой ILogger абстракции, вы должны серьезно спросить себя не слишком ли много вы регистрируетесь.

Также обратите внимание, что зависимости не должны быть необязательными .

person Steven    schedule 21.05.2012
comment
1. Стивен, я забыл о проверке ServiceLocator.Current на null, спасибо, что указали на это. При этой проверке тесты не заставляют инициализировать SL. 2. зачем мне вообще тестировать что-то вроде ILogger? Но если я захочу, это выполнимо. 3. Согласен - это проблема, если хотите потестить. Но опять же - это выполнимо. В конце концов, что важнее: легко писать реальный код или легко писать тесты? - person Dmitry Golubets; 21.05.2012
comment
3. Существует множество исследований, которые доказывают, что нельзя писать качественный код, не имея возможности его протестировать. Поэтому возможность легко писать тесты имеет решающее значение для написания качественных приложений. - person Sebastian Weber; 21.05.2012
comment
@DmitryGolubets: Позвольте мне изменить ситуацию. Почему бы вам не протестировать использование ILogger внутри класса? Поскольку вы это написали, это должна быть ценная бизнес-логика, и вы должны знать, верен ли этот код. Если этот код не особо ценен, почему вы вообще его не написали? Возможно, эта линия является сквозной проблемой (АОП) и не важна для бизнес-логики. В этом случае вам не следует усложнять свою бизнес-логику, и вы должны передать эту логику в декоратор или что-то еще. - person Steven; 21.05.2012
comment
О пункте 3. Похоже, что MSTest по умолчанию запускает тесты последовательно (stackoverflow.com/questions/154180/) - person Karsten; 26.02.2014