окружающий контекст против внедрения конструктора

9

У меня много базовых классов, которым требуется ISessionContext базы данных, ILogManager для журнала и IService, используемый для связи с другими сервисами. Я хочу использовать внедрение зависимостей для этого класса, используемого всеми основными классами.

У меня есть две возможные реализации. Базовый класс, который принимает IAmbientContext со всеми тремя классами или вводит для всех классов три класса.

public interface ISessionContext 
{
    ...
}

public class MySessionContext: ISessionContext 
{
    ...
}

public interface ILogManager 
{

}

public class MyLogManager: ILogManager 
{
    ...
}

public interface IService 
{
    ...
}

public class MyService: IService
{
    ...
}

Первое решение:

public class AmbientContext
{
    private ISessionContext sessionContext;
    private ILogManager logManager;
    private IService service;

    public AmbientContext(ISessionContext sessionContext, ILogManager logManager, IService service)
    {
        this.sessionContext = sessionContext;
        this.logManager = logManager;
        this.service = service;
    }
}


public class MyCoreClass(AmbientContext ambientContext)
{
    ...
}

второе решение (без ambientcontext)

public MyCoreClass(ISessionContext sessionContext, ILogManager logManager, IService service)
{
    ...
}

Что является лучшим решением в этом случае?

user3401335
источник
Что « IServiceиспользуется для общения с другими службами»? Если IServiceпредставляет неопределенную зависимость от других сервисов, то это звучит как поиск сервисов и не должно существовать. Ваш класс должен зависеть от интерфейсов, которые явно описывают, что их потребитель будет делать с ними. Ни одному классу не нужна услуга для предоставления доступа к услуге. Классу нужна зависимость, которая делает что-то конкретное, что нужно классу.
Скотт Ханнен

Ответы:

4

«Лучший» здесь слишком субъективен. Как и в случае с такими решениями, это компромисс между двумя одинаково действенными способами достижения чего-либо.

Если вы создаете AmbientContextи впрыснуть , что на многие классы, вы потенциально обеспечивая более подробную информацию для каждого из них , чем они нуждаются (например , класс Fooможет использовать только ISessionContext, но сосчитанную о ILogManagerи ISessionтоже).

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

Так что это случай балансировки двух и выбора подходящего для ваших обстоятельств. Если бы у вас был только один класс и три параметра, лично я бы не стал беспокоиться AmbientContext. Для меня переломный момент, скорее всего, будет четыре параметра. Но это чистое мнение. Ваш переломный момент, скорее всего, будет отличаться от моего, так что следуйте тому, что вам подходит.

Дэвид Арно
источник
4

Терминология из вопроса на самом деле не совпадает с примером кода. Это Ambient Contextшаблон, используемый для получения зависимости от любого класса в любом модуле настолько легко, насколько это возможно, не загрязняя каждый класс, чтобы принять интерфейс зависимости, но сохраняя идею инверсии управления. Такие зависимости обычно предназначены для ведения журналов, безопасности, управления сеансами, транзакций, кэширования, аудита, а также для любых сквозных задач в этом приложении. Это как - то раздражает добавить ILogging, ISecurity, ITimeProviderконструкторам и большую часть времени не все классы нужно в одно и тоже время, поэтому я понимаю вашу потребность.

Что если время жизни ISessionэкземпляра отличается от времени жизни ILogger? Возможно, экземпляр ISession должен быть создан при каждом запросе, а ILogger - один раз. Поэтому все эти зависимости, управляемые одним объектом, который не является самим контейнером, не выглядят правильным выбором из-за всех этих проблем с управлением временем жизни и локализацией, а также с другими, описанными в этом потоке.

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

Таким образом, самый простой способ - НЕ использовать инжекцию конструктора или любой другой механизм внедрения для решения сквозных зависимостей, а использовать статический вызов . На самом деле мы видим этот шаблон довольно часто, реализуемый самой платформой. Проверьте Thread.CurrentPrincipal, который является статическим свойством, которое возвращает реализацию IPrincipalинтерфейса. Это также настраивается, так что вы можете изменить реализацию, если хотите, таким образом, вы не связаны с ней.

MyCore выглядит сейчас что-то вроде

public class MyCoreClass
{
    public void BusinessFeature(string data)
    {
        LoggerContext.Current.Log(data);

        _repository.SaveProcessedData();

        SessionContext.Current.SetData(data);
        ...etc
    }
}

Этот шаблон и возможные реализации были подробно описаны Марком Зееманом в этой статье . Могут быть реализации, основанные на самом контейнере IoC, который вы используете.

Вы хотите , чтобы избежать AmbientContext.Current.Logger, AmbientContext.Current.Sessionпо тем же причинам , как описано выше.

Но у вас есть другие варианты решения этой проблемы: используйте декораторы, динамический перехват, если ваш контейнер имеет такую ​​возможность, или AOP. Окружающий контекст должен быть последним средством, потому что его клиенты скрывают свои зависимости через него. Я бы по-прежнему использовал Ambient Context, если интерфейс действительно имитирует мой импульс использовать статическую зависимость, например DateTime.Nowили, ConfigurationManager.AppSettingsи эта потребность возникает довольно часто. Но, в конце концов, внедрение в конструктор может быть не такой уж плохой идеей для получения этих вездесущих зависимостей.

Адриан Ифтоде
источник
3

Я бы избежать AmbientContext.

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

Во-вторых, если вы используете его, чтобы избежать множественных зависимостей конструктора, этот подход будет стимулировать других разработчиков (включая вас) добавлять новых членов в этот класс окружающего контекста. Тогда первая проблема усложняется.

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

В-четвертых, ему не хватает сплоченности и он нарушает принцип единой ответственности. Вот почему у него есть имя, такое как «AmbientContext», потому что он делает много несвязанных вещей, и нет способа назвать его в соответствии с тем, что он делает.

И это, вероятно, нарушает принцип разделения интерфейса, вводя элементы интерфейса в классы, которые в них не нуждаются.

Скотт Ханнен
источник
2

Второй (без обертки интерфейса)

Если не существует какого-либо взаимодействия между различными сервисами, которые необходимо инкапсулировать в промежуточный класс, это только усложнит ваш код и ограничит гибкость, когда вы вводите «интерфейс интерфейсов».

Ewan
источник