环境上下文与构造函数注入


9

我有很多核心类,它们需要数据库的ISessionContext,用于日志的ILogManager和用于与其他服务进行通信的IService。我想对所有核心类使用的此类使用依赖项注入。

我有两个可能的实现。接受所有三个类的IAmbientContext或为所有三个类注入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)
{
    ...
}

第二种解决方案(无环境上下文)

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

在这种情况下,维奇是最好的解决方案?


什么是“ IService用于与其他服务进行通信”?如果IService表示对其他服务的模糊依赖,那么这听起来像是服务定位符,并且不应该存在。您的类应依赖于明确描述其使用者将如何使用它们的接口。任何班级都不需要服务来提供对服务的访问。类需要一个依赖项,该依赖项可以执行该类需要的特定操作。
Scott Hannen

Answers:


4

“最佳”在这里过于主观。与此类决策一样,这是在两种同样有效的达成目标的方式之间进行的权衡。

如果创建AmbientContext并注入到这一点许多类,你都可能对他们每个人提供了更多的信息,比他们需要(如类Foo才可以使用ISessionContext,但被告知ILogManagerISession太)。

如果通过参数传递每个参数,则只告诉每个类需要了解的那些东西。但是,参数的数量会迅速增加,您可能会发现您有太多的构造函数和方法,其中包含许多重复性很高的参数,可以通过上下文类简化这些方法。

因此,这是平衡两者并为您的情况选择合适的一种情况。如果您只有一个类和三个参数,那么我个人就不用理会AmbientContext。对我而言,转折点可能是四个参数。但这是纯粹的意见。您的转折点可能与我的不同,因此请选择适合您的感觉。


4

问题中的术语与示例代码并不完全匹配。这种Ambient Context模式用于尽可能容易地从任何模块的任何类中获取依赖,而不会污染每个类以接受依赖的接口,但仍保留控制反转的思想。这样的依赖关系通常专用于日志记录,安全性,会话管理,事务,缓存,审计,因此对于该应用程序中的任何交叉关注点。这是莫名其妙地讨厌的添加ILoggingISecurityITimeProvider来构造和大部分的时间并非所有的类都需要在同一时间,让我明白你的需要。

如果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
    }
}

Mark Seemann在本文中详细描述了这种模式和可能的实现。可能有些实现依赖于您使用的IoC容器本身。

由于上述相同的原因AmbientContext.Current.Logger,您想要避免AmbientContext.Current.Session

但是,您还有其他方法可以解决此问题:如果容器具有此功能或AOP,则可以使用装饰器,动态拦截。环境上下文应该是最后的选择,因为它的客户通过它隐藏了他们的依赖关系。如果该接口确实模仿了我使用类似DateTime.Now或的静态依赖项的冲动,ConfigurationManager.AppSettings并且这种需求经常出现,我仍然会使用环境上下文。但是最后,构造器注入对于获得这些无处不在的依赖可能不是一个坏主意。


3

我会避免AmbientContext

首先,如果班级依赖,AmbientContext那么您实际上并不知道它的作用。您必须查看其对那个依赖项的使用,以找出它使用了哪个嵌套依赖项。您也无法查看依赖项的数量并判断类是否做得太多,因为其中一个依赖项实际上可能代表多个嵌套的依赖项。

其次,如果您使用它来避免多个构造函数依赖性,那么这种方法将鼓励其他开发人员(包括您自己)向该环境上下文类添加新成员。然后,第一个问题变得更加复杂。

第三,AmbientContext模拟对它的依赖更加困难,因为在每种情况下,您都必须弄清楚是模拟其所有成员还是仅模拟所需的成员,然后设置一个模拟以返回那些模拟(或测试双打)。您的单元测试更加难以编写,阅读和维护。

第四,缺乏凝聚力,违反了单一责任原则。这就是为什么它具有“ AmbientContext”之类的名称的原因,因为它做了很多无关的事情,并且无法根据其功能来命名。

通过将接口成员引入不需要它们的类中,可能违反了接口隔离原则。


2

第二个(不带接口包装器)

除非需要封装在中间类中的各种服务之间存在某种交互,否则在引入“接口接口”时,只会使您的代码复杂化并限制灵活性。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.