不使用任何DI库进行依赖注入

10

我对仓库和依赖注入还不熟悉,正在尝试在我的MVC 5项目中实现。

我实现了构造函数注入,在我的控制器中有一个像这样的构造函数:

IBook _ibook;
public Test(IBook ibook)
{
   _ibook = ibook;
}

没有任何依赖注入库,它会抛出错误:没有空构造函数。

为了避免这种情况,我添加了以下一个构造函数:

public Test ():this(new Book())
{    
}

因为我对DI不熟悉,所以我不想冒险使用可能会在以后引发一些我无法解决的错误的DI库来进行我的项目。

我想知道如果我不使用DI库会遇到什么问题。

如果推荐使用,哪个DI库适合初学者?我看过NInject和Unity的一些视频。


4
我会推荐使用SimpleInjector,因为它可以在启动之前验证配置。很少有容器能够做到这一点。因此,任何问题通常都会在开始时被发现和解决。 - jgauffin
如果您计划将项目升级到ASP.NET vNext,值得一提的是它将包括一些基本的DI容器实现,可以满足您的需求(http://blogs.msdn.com/b/webdev/archive/2014/06/17/dependency-injection-in-asp-net-vnext.aspx)。 - Alex Art.
1
我认为最好从Pure DI开始,而不是使用ASP.NET vNext内置的DI库。当你编写SOLID应用程序时,内置的DI库是无用的。 - Steven
4个回答

18

在使用工具或库之前,推荐延迟决策直至最后负责时机。通过良好的设计,可以随后添加依赖注入库,这意味着你在实践纯 DI

MVC 的首选拦截点是 IControllerFactory 抽象类,因为它允许你拦截 MVC 控制器的创建,并且这样做可以避免你必须实现第二个构造函数(这是一个反模式)。虽然可以使用 IDependencyResolver,但使用该抽象类要麻烦得多,因为它还会被 MVC 调用以解决通常不感兴趣的问题。

以下是实现作为 组合根 的自定义 IControllerFactory

public sealed class CompositionRoot : DefaultControllerFactory
{
    private static string connectionString = 
        ConfigurationManager.ConnectionStrings["app"].ConnectionString;
    private static Func<BooksContext> bookContextProvider = GetCurrentBooksContext;
    private static IBookRepository bookRepo = new BookRepository(bookContextProvider);
    private static IOrderBookHandler orderBookHandler = new OrderBookHandler(bookRepo);

    protected override IController GetControllerInstance(RequestContext _, Type type) {
        // Unfortunately, because of MVC's design, controllers are not stateless, and 
        // you will have to create them per request.

        if (type == typeof(OrderBookController))
            return new HomeController(orderBookHandler);

        if (type == typeof(BooksController))
            return new BooksController(bookRepo);

        // [other controllers here]

        return base.GetControllerInstance(_, type);
    }

    private static BooksContext GetCurrentBooksContext() {
        return GetRequestItem<BooksContext>(() => new BooksContext(connectionString));
    }

    private static T GetRequestItem<T>(Func<T> valueFactory) where T : class {
        var context = HttpContext.Current;
        if (context == null) throw new InvalidOperationException("No web request.");
        var val = (T)context.Items[typeof(T).Name];
        if (val == null) context.Items[typeof(T).Name] = val = valueFactory();
        return val;
    }
}

你的新控制器工厂可以按照以下方式与MVC集成:
public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start() {
        ControllerBuilder.Current.SetControllerFactory(new CompositionRoot());

        // the usual stuff here
    }
}

当您使用Pure DI进行实践时,您通常会看到组合根由一系列if语句组成。每个应用程序中的根对象对应一个语句。
从Pure DI开始有一些有趣的优势,其中最突出的是编译时支持,因为一旦开始使用DI库,您将立即失去这种支持。一些库尝试通过允许您以编译器方式验证配置来最小化此损失; 但是,该验证是在运行时执行的,反馈周期永远不像编译器可以给你的那样短。
请不要尝试通过实现一些使用反射创建类型的机制来简化开发,因为这样做等于构建自己的DI库。这样做有许多缺点,例如,您失去了编译时支持,而没有获得任何现有DI库可以为您提供的好处。
当您的组合根开始难以维护时,这就是您应该考虑从Pure DI切换到DI库的时刻。
请注意,在我的示例组合根中,所有应用程序组件(除控制器外)都被定义为单例。单例意味着应用程序只会有每个组件的一个实例。这种设计需要您的组件是无状态的(因此线程安全),任何具有状态的东西(例如BooksContext不应通过构造函数注入。在示例中,我使用了一个Func<T>作为BooksContext的提供者,该提供者存储在每个请求中。
使您的对象图成为单例具有许多有趣的优点(请参见此处)。例如,它可以防止您犯常见的配置错误,如俘获依赖关系,并迫使您进行更加SOLID的设计。此外,某些DI库非常缓慢,使所有内容都成为单例可能会在以后切换到DI库时防止性能问题。另一方面,这种设计的缺点是团队中的每个人都应理解所有组件必须是无状态的。在组件中存储状态将导致不必要的麻烦和恼怒。我的经验是,与大多数DI配置错误相比,有状态的组件要容易检测得多。我还注意到,对于大多数开发人员,特别是那些没有DI经验的人,拥有单例组件是一种自然的感觉。有关要选择的两种组合模型及其缺点和优点的详细讨论,请查看这一系列博客文章
请注意,在示例中,我手动实现了BooksContext的每个请求生命周期。尽管所有的DI库都支持像每个请求这样的范围生命周期,但我会反对使用这些范围生命周期(除非库保证抛出异常而不是静默失败)。大多数库在您解析活动范围之外的范围实例时不会警告您(例如,在后台线程上解析每个请求的实例)。一些容器将返回单例实例,其他容器将每次都返回一个新实例。这真的很麻烦,因为它隐藏了错误,并可能导致您花费许多时间调试应用程序(我在这里说话的经验)。

12
最简单和最明智的解决方案是使用 Pure DI。在 ASP.NET MVC 中,这可以通过从 DefaultControllerFactory 派生并重写 GetControllerInstance 来最轻松地完成。
protected override IController GetControllerInstance(
    RequestContext requestContext, Type controllerType)
{
    if (controllerType == typeof(Test))
        return new Test(new Book());

    return base.GetControllerInstance(requestContext, controllerType);
}

然后像这样在您的 Global.asax 中注册新的控制器工厂:

ControllerBuilder.Current.SetControllerFactory(new MyControllerFactory());

不幸的是,许多文档会告诉你使用IDependencyResolver混合注入来处理依赖注入,但这些做法并不能使你的代码更易维护

关于如何在ASP.NET MVC中正确使用依赖注入,以及更多详细信息,请参见我的书


2
谢谢Mark。你的书已经在我的亚马逊愿望清单里了 :)。 - RKh

2
如果您只关心依赖注入来实现一定程度的抽象,那么您绝对不需要使用任何IoC框架。
如果您不关心作用域、生命周期和嵌套依赖项,您可能最终得到像这样原始的东西:
internal class MyBasicResolver : IDependencyResolver
{
    private readonly Dictionary<Type, Type> _services = new Dictionary<Type, Type>()
    {
        { typeof(IBook), typeof(Book) }
        // more services registrations
    };


    public object GetService(Type serviceType)
    {
        return _services.ContainsKey(serviceType) ? Activator.CreateInstance(_services[serviceType]) : null;
    }

    public IEnumerable<object> GetServices(Type serviceType)
    {
        yield return GetService(serviceType);
    }
}

然后将其注册为MVC的当前依赖项解析器:

DependencyResolver.SetResolver(new MyBasicResolver());

请看

MSDN


0

Ninject和Unity提供对象容器,其中包含在应用程序启动时注册的对象。

但是为什么需要使用依赖注入(DI)呢?DI表示两个对象不应该依赖于其具体实现,而应该依赖于其抽象。因此,如果将来需要将Book类替换为eBook,这两个类具有相同的功能,但具有不同的具体实现,那么您只需要调整DI配置,而无需重新编写eBook的控制器。

我在大多数项目中都使用Unity DI,我没有遇到任何无法解决的问题,它很容易使用并且是一种好的实践方法,不要害怕使用它。


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