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