Ioc/DI-为什么我必须在应用程序的入口点引用所有层/程序集?

146

(与这个问题相关的是,EF4:为什么启用延迟加载时必须启用代理创建?).

我对DI还不熟悉,请容忍我的问题。我知道容器负责实例化所有已注册的类型,但它需要引用解决方案中所有DLL及其引用。

如果我没有使用DI容器,在我的MVC3应用程序中,我就不必引用EntityFramework库,只需引用业务层,业务层将引用我的数据访问/存储库层。

我知道最终所有DLL都包含在bin文件夹中,但我的问题是必须通过在VS中“添加引用”来显式引用它,以便能够发布包含所有必要文件的WAP。


1
这篇摘录来自《.NET依赖注入第二版》一书,是对Mark和我自己回答的更详细阐述。它详细描述了组合根的概念,以及为什么让应用程序的启动路径依赖于每个其他模块实际上是一件好事。 - Steven
我阅读了那个摘录链接和第一章,因为我非常喜欢对 DI 复杂问题的类比和简单解释,所以我会购买这本书。我认为你应该建议一个新答案,明确回答“除非它也是您的组合根,否则您不必在入口逻辑层中引用所有层/程序集”,链接到摘录,并发布摘录中的图像 Figure 3。 - diegohb
4个回答

212
如果我不使用DI容器,我在我的MVC3应用程序中不需要引用EntityFramework库,只需要引用我的业务层,业务层将引用我的DAL/Repo层。
是的,这正是DI所努力避免的情况 :)
紧耦合的代码中,每个库可能只有几个引用,但这些引用又有其他引用,形成了一个深度依赖的图,如下所示:

Deep Graph

因为依赖图很深,这意味着大多数库会带来许多其他依赖项 - 例如在图表中,Library C 带来了 Library H、Library E、Library J、Library M、Library KLibrary N。这使得每个库独立于其余部分更难重用 - 例如在单元测试中
然而,在松散耦合的应用程序中,通过将所有引用移动到组合根依赖图被严重扁平化

Shallow Graph

正如绿色所示,现在可以重用Library C而不带来任何不必要的依赖关系。

然而,使用许多DI容器时,您不必添加对所有所需库的硬引用。相反,您可以使用延迟绑定,以约定为基础的程序集扫描(首选)或XML配置的形式。

但是,当您这样做时,必须记住将程序集复制到应用程序的bin文件夹中,因为这不再自动发生。个人而言,我很少觉得值得那么额外的努力。

这个答案的更详细版本可以在我的书Dependency Injection, Principles, Practices, Patternsthis excerpt中找到。


4
非常感谢,现在这个意思非常清晰了。我需要知道这是否是有意设计的。至于强制正确使用依赖项,我已经按照 Steven 在下面提到的方式实现了一个单独的项目,并在其中引用了其他库的 DI 引导程序。该项目由入口应用程序引用,在完全构建的末尾,这会导致所有必要的 DLL 文件位于 bin 文件夹中。谢谢! - diegohb
6
此答案适用于除.NET之外的其他领域。您可能需要参考罗伯特·C·马丁所著的《软件包设计原则》一章,例如《敏捷软件开发:原则、模式与实践》 - Mark Seemann
7
组合根是一种依赖注入模式,与服务定位器相反。从组合根的角度来看,所有类型都不是多态的;组合根将所有类型视为具体类型,因此,里氏替换原则不适用于它。 - Mark Seemann
4
通常情况下,客户端应该定义它们使用的接口(参见APP,第11章),因此如果库J需要一个接口,则应该在库J中定义。这是依赖反转原则的一个推论。 - Mark Seemann
3
顺便说一下,你的问题假设接口必须在其他自定义库中定义,但这并不是必须的。例如,在.NET中,我经常使用IEnumerable<T>IObserver<T>作为依赖项。它们在BCL中已经定义,因此对所有消费者和实现者都自动可用。 - Mark Seemann
显示剩余11条评论

71
如果我不使用 DI 容器,我就不必在我的 MVC3 应用程序中引用 EntityFramework 库。
即使使用 DI 容器,您也不必让 MVC3 项目引用 Entity Framework,但是您(隐式地)选择通过在 MVC3 项目内实现Composition Root(启动路径,在其中组合对象图)来执行此操作。如果您非常严格地使用程序集保护您的架构边界,则可以将表示逻辑移动到不同的项目中。
当您将所有 MVC 相关逻辑(控制器等)从启动项目移动到类库中时,它允许此表示层程序集与应用程序的其余部分断开连接。您的 Web 应用程序项目本身将成为一个非常薄的外壳,具有所需的启动逻辑。Web 应用程序项目将是引用所有其他程序集的 Composition Root。
将表示逻辑提取到类库中可能会使在 MVC 中工作变得复杂。由于控制器不在启动项目中(而视图、图像、CSS 文件很可能留在启动项目中),因此将所有内容连接起来将更加困难。这可能是可行的,但需要更多的时间来设置。
由于缺点,我通常建议将组合根保留在Web项目中。许多开发人员不希望他们的MVC程序集依赖于DAL程序集,但这不应该是一个问题。不要忘记,程序集是一种部署工件;您将代码拆分为多个程序集以允许代码分别部署。另一方面,架构层是一种逻辑工件。在同一个程序集中拥有多个层非常可能(也很常见)。
在这种情况下,您将最终在同一个Web应用程序项目中(因此在同一个程序集中)拥有组合根(层)和表示层。即使该程序集引用包含DAL的程序集,表示层仍然不会引用DAL - 这是一个重大区别。
当你这样做时,编译器无法在编译时检查这个架构规则。但实际上大多数架构规则都无法被编译器检查。如果你担心团队不遵循架构规则,建议进行代码审查,这是一个提高代码质量、一致性和团队技能的重要实践。你还可以使用像NDepend(商业工具)这样的工具来验证你的架构规则。当你将NDepend与构建过程集成时,它会警告你是否有人提交了违反此类架构规则的代码。
你可以在我的书《依赖注入:原理、实践、模式》的第4章中阅读更详细的关于组合根的讨论。

一个单独的引导项目是我的解决方案,因为我们没有 ndepend,而且我以前从未使用过它。不过,当只有一个最终应用程序时,我会研究一下它,因为这听起来像是更好的实现我正在尝试做的事情的方法。 - diegohb
1
最后一段非常好,开始帮助我改变了对于保持层次结构分离的严格要求。如果您在编写代码时采用其他流程(例如代码审查)来确保在 UI 代码中没有引用 DAL 类,反之亦然,则在一个程序集中拥有两个或多个逻辑层实际上是可以接受的。 - BenM

7
如果我不使用DI容器,我就不需要在我的MVC3应用程序中引用EntityFramework库,只需要引用我的业务层,业务层会引用我的数据访问层/存储库层。
您可以创建一个名为“DependencyResolver”的单独项目。在这个项目中,您必须引用所有的库。
现在UI层不需要引用NHibernate/EF或任何其他与UI无关的库,除了Castle Windsor。
如果您想将Castle Windsor和DependencyResolver从UI层隐藏起来,您可以编写一个HttpModule来调用IoC注册表内容。
我这里只有一个StructureMap的例子:
public class DependencyRegistrarModule : IHttpModule
{
    private static bool _dependenciesRegistered;
    private static readonly object Lock = new object();

    public void Init(HttpApplication context)
    {
        context.BeginRequest += (sender, args) => EnsureDependenciesRegistered();
    }

    public void Dispose() { }

    private static void EnsureDependenciesRegistered()
    {
        if (!_dependenciesRegistered)
        {
            lock (Lock)
            {
                if (!_dependenciesRegistered)
                {
                    ObjectFactory.ResetDefaults();

                    // Register all you dependencies here
                    ObjectFactory.Initialize(x => x.AddRegistry(new DependencyRegistry()));

                    new InitiailizeDefaultFactories().Configure();
                    _dependenciesRegistered = true;
                }
            }
        }
    }
}

public class InitiailizeDefaultFactories
{
    public void Configure()
    {
        StructureMapControllerFactory.GetController = type => ObjectFactory.GetInstance(type);
          ...
    }
 }

DefaultControllerFactory不直接使用IoC容器,而是委托给IoC容器方法。

public class StructureMapControllerFactory : DefaultControllerFactory
{
    public static Func<Type, object> GetController = type =>
    {
        throw new  InvalidOperationException("The dependency callback for the StructureMapControllerFactory is not configured!");
    };

    protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
    {
        if (controllerType == null)
        {
            return base.GetControllerInstance(requestContext, controllerType);
        }
        return GetController(controllerType) as Controller;
    }
}

GetController 委托在 StructureMap 注册表中设置(在 Windsor 中应该是一个 Installer)。


1
我甚至比我最终做的更喜欢这个,模块很棒。那么我应该在哪里调用Container.Dispose()呢?是在模块的ApplicationEnd或EndRequest事件中呢? - diegohb
1
@Steven 因为 Global.asax 文件位于你的 MVC UI 层中,而 HttpModule 则应该位于 DependencyResolver 项目中。 - Rookian
2
小小的好处就是没有人能够在UI中使用IoC容器。也就是说,没有人能够将IoC容器用作UI中的服务定位器。 - Rookian
2
此外,它还禁止开发人员在 UI 层意外使用 DAL 代码,因为在 UI 中没有对程序集的硬引用。 - diegohb
1
我已经找到了使用Bootstrapper的通用注册API来完成相同操作的方法。我的UI项目引用了Bootstrapper,这是依赖项解析项目,在其中我连接我的注册,并且引用了我的Core中的项目(用于接口),但没有引用其他任何东西,甚至不包括我的DI框架(SimpleInjector)。我正在使用OutputTo nuget将dll复制到bin文件夹中。 - diegohb
显示剩余2条评论

1
  • 如果一个对象实例化另一个对象,则存在依赖关系。
  • 如果一个对象期望抽象(构造函数注入、方法注入等),则不存在依赖关系。
  • 程序集引用(引用dll、webservices等)与依赖关系概念无关,因为为了解析抽象并能够编译代码,该层必须引用它。

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