服务定位器和依赖注入

5
我认为人们普遍认为以下内容是不好的:
public class Foo
{
    private IService _service;
    public Foo()
    {
        _service = IocContainer.Resolve<IService>();
    }
}

以下是首选的方式(依赖注入)

public class Foo
{
    private IService _service;
    public Foo(IService service)
    {
    }
}

现在消费者需要提供服务了。当然,消费者也可以在构造函数中要求IService,但是当层级变得更深时,这似乎很烦人。在某个时刻,有人需要从IoC容器中请求IService——但什么时候呢?

我的一个前同事曾经为UoW/Repository模式编写了一个UnitOfWork类,如下所示(使用Microsoft ServiceLocator):

public static UnitOfWork
{
    public static IUnitOfWork Current
    {
        get { return ServiceLocator.Current.GetInstance<IUnitOfWork>(); }
    }

    public static void Commit()
    {
        Current.Commit();
    }

    public static void Dispose()
    {
        Current.Dispose();
    }

    public static IRepository<T> GetRepository<T>() where T : class
    {
        return ServiceLocator.Current.GetInstance<IRepository>();
    }
}

使用 Ninject 连接 IoC,这样对 IRepository 的请求会找到当前的 UoW,如果需要的话会创建一个新的(如果当前的已经被释放)。使用方法如下:

public class MyController
{    
    public void RunTasks()
    {
        var rep = UnitOfWork.GetRepository<Tasks>();
        var newTasks = from t in rep.GetAll()
                       where t.IsCompleted == false
                       select t;

        foreach (var task in newTasks)
        {
            // Do something
        }

        UnitOfWork.Commit();
    }
}

然而,它仍然存在静态IoC(服务定位器)类的问题,但是否有更智能的解决方案呢?在这种情况下,不需要了解内部依赖关系(静态类没有逻辑),并且为了测试目的,备用IoC配置可以使用模拟设置所有内容 - 并且很容易使用。
编辑: 我将尝试通过不同的示例来澄清我的困惑。假设我有一个标准的winforms应用程序,其中包含一个MainWindow类。当用户单击按钮时,我需要从数据库加载一些数据,并将其传递给处理数据的类:
public class MainWindow : Form
{
    public MainWindow()
    {
    }

    private void OnUserCalculateClick(object sender, EventArgs args)
    {
        // Get UoW to connect to DB
        // Get instance of processor
    }
}

我该如何获取处理器和工作单元的实例?它可以注入到表单类中吗?

我的问题归结为:如果我在一个没有Ioc容器构建的类中,这个类可能是winform、ria服务类等等——是否可以引用服务定位器/IoC控制器来解析依赖项实例,或者有没有更好的处理这些情况的方法?或者是我的做法不对...


1
这个问题有点难回答,因为这两个例子都有内置的问题:第一个UnitOfWork示例存在问题(正如你自己指出的那样),它并没有任何事情;完全是多余的,可以从代码库中删除。第二个示例存在内置问题,因为(由于框架限制),表单必须有一个默认构造函数。不过,总是有方法来解决这些问题的。一个可能对你有帮助的核心概念是组成根 - Mark Seemann
从你的谈话中我知道你会受益匪浅(像我之外的许多人一样),赶紧购买manning.com/seemann并阅读Mark的最佳答案,同时等待它的到来。 - Ruben Bartelink
看起来很有趣,物有所值 - 巧合的是他实际上就住在我家附近 :) - sondergard
3个回答

1

关于问题的第一部分:

消费者当然也可以在构造函数中要求IService,但是当层次结构变得更深时,这似乎会很烦人。

不,消费者不需要一个IService,它需要一个IFoo。它不知道它将得到的IFoo依赖于IService,只有您的DI配置知道。所以,不用担心,你不会陷入你描述的这种依赖层次地狱中

在某个时候,有人需要从IoC容器中请求IService——但是何时呢?

这只会在您的组合根中发生。因此,如果它是一个MVC应用程序,您必须以某种方式配置MVC框架,以在需要实例化控制器时使用您的DI配置,因此内部框架决定(从路由)需要一个MyController,并执行类似于resolver.Get<MyController>()的操作。因此,服务位置仅在那里使用,而不是在您的控制器或任何其他地方。

关于问题中的MyController部分:
无法真正理解其与前一部分的联系,但仍可以使用构造函数注入。不使用静态类(不进行注入,因此无法用于测试目的进行更改或模拟),也不使用服务定位。
[顺便提一下,您甚至可以避免有关工作单元的额外代码(可能您使用的ORM已经具有工作单元并在实现IRepositories时已经使用它)。也许您的存储库可以有一个SaveChanges方法,它将调用unitOfWork的SaveChanges-但这是偏好问题,并且与先前的讨论无关]。

我有点明白你的意思,但这仍然让我对在没有通过IoC实例化的类的情况感到困惑。我已经编辑了我的问题以反映这一点。如果有成千上万个类的解决方案呢?如果所有类都需要一个接口并且必须绑定到IoC中,那么组合会变得非常复杂,可能会因为缺少绑定和/或循环依赖而导致运行时错误。 - sondergard
@sondergard 有避免将接口显式绑定到实现的方法,可以使用约定(请查看您的 DI 框架如何执行此操作)。您可以将 DI 配置组织在不同的类/模块中,以便每个类/模块都可以管理。从组合根未实例化的类(例如通过工厂实例化)从这些工厂进行管理。再次,创建/管理对象的生命周期的责任被封装在一个地方,而不是分散在这些类的用户内部。 - zafeiris.m

0
我解决这个问题的方式是使用一个 UnitOfWorkFactory,它拥有一个 Create 方法来创建你的 UnitOfWork
public interface IUnitOfWorkFactory
{
    IUnitOfWork Create();
}

public interface IUnitOfWork : IDisposable
{
    T GetRepository<T>();
    void Commit();
}

public class MyController
{    
    private readonly IUnitOfWorkFactory _unitOfWorkFactory;

    public MyController(IUnitOfWorkFactory unitOfWorkFactory)
    {
        _unitOfWorkFactory = unitOfWorkFactory;
    }

    public void RunTasks()
    {
        using (var unitOfWork = _unitOfWorkFactory.Create())
        {
            var rep = UnitOfWork.GetRepository<Tasks>();
            var newTasks = from t in rep.GetAll()
                           where t.IsCompleted == false
                           select t;

            foreach (var task in newTasks)
            {
                // Do something
            }               

            unitOfWork.Commit();
        }
    }
}  

拥有工厂的优势在于它允许用户(控制器)控制工作单元的创建和销毁。

这也使得单元测试更容易,因为您不需要使用您的IoC进行测试。我也不喜欢有一个全局上下文可用(如UnitOfWork.Current),因为很难确定何时UoW将被处理或提交。

如果另一个类需要一个UoW实例来向现有上下文添加其他工作,则可以传递特定的实例。


0

使用你的第一个例子,容器将构建IFooIService。以下是一些真正的代码来说明:

        container.RegisterType<ISubmittingService, GnipSubmittingService>(
            new DisposingTransientLifetimeManager(),
            new InjectionConstructor(
                typeof (IGnipHistoricConnection),
                typeof (IUserDataInterface),
                new EstimateVerboseLoggingService.TitleBuilder(),
                new EstimateVerboseLoggingService.FixedEndDateBuilder(),
                typeof (ISendEmailService),
                addresses,
                typeof (ILog)
                )
            );

        container.RegisterType<IEstimateService, EstimateVerboseLoggingService>(
            new DisposingTransientLifetimeManager(),
            new InjectionConstructor(
                typeof(IEstimateDataInterface),
                typeof(ISubmittingService),
                typeof(ILog)
                )
            );

...

    public EstimateVerboseLoggingService(
        IEstimateDataInterface estimateData,
        ISubmittingService submittingService,
        ILog log)
    {
        _estimateData = estimateData;
        _submittingService = submittingService;
        _log = log;
    }

...

    public GnipSubmittingService(
        IGnipHistoricConnection gnip,
        IUserDataInterface userDb,
        IBuilder<string, int> titleBuilder,
        IBuilder<DateTime, DateTime> endDateBuilder,
        ISendEmailService emailService,
        IEnumerable<MailAddress> errorEmailRecipients,
        ILog log)
    {
        _gnip = gnip;
        _userDb = userDb;
        _titleBuilder = titleBuilder;
        _endDateBuilder = endDateBuilder;
        _emailService = emailService;
        _errorEmailRecipients = errorEmailRecipients;
        _log = log;
    }

在这段代码中,EstimateVerboseLoggingService 使用了一个 ISubmitingService。两个实现都在容器中指定。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接