可扩展的 IoC 容器






4.93/5 (66投票s)
学习如何创建一个非常小(小于 3KB)但可扩展性极强的 IoC 容器
背景
我最近写了一篇文章《软件架构》,其中我谈到了对许多框架的第一个“修复”,我认为这种修复通常在许多 IoC 容器中都是必需的。
但即使其中一些不需要任何修复,我通常工作的公司也不喜欢使用外部代码(.NET Framework 除外)。这些公司通常害怕使用“难以维护”或“已停产”的解决方案,因此,它们完全避免使用 IoC 容器,因为 .NET 没有自带一个默认的 IoC 容器,并且创建 IoC 容器被认为是一件困难的事情,没有人能够维护。也许他们是对的,因为我经常看到许多文章解释如何创建难以(编写,有时也难以使用)且通常有限的 IoC 容器,因此有人需要更改其代码,并且会因此而受苦。
所以我的想法是展示如何仅使用 .NET 基类创建一个易于使用的 IoC 容器,它将真正具有可扩展性,这意味着为了扩展它以支持不同的场景,你不需要更改其源代码。
注意
我将不在这里讨论什么是 IoC 容器或其优点。如果你需要了解什么是 IoC 容器,那么我建议你从阅读 维基百科中关于控制反转的内容开始。
字典
IoC 容器的核心是“请求”和“结果生成器”之间的映射。
例如,如果我们想调用 factory.Create("Button");
,我们希望将 string
映射到一个能够创建按钮的委托,但是,由于我们可能希望能够创建其他对象,我们可能希望有一个能够创建对象的函数,在该特定用法中,它将创建一个按钮。
为此,我们可以使用 Dictionary<string, Func<object>>
。但是由于我们不想向用户公开这样的字典,我们可能会得到一个像这样的类
public sealed class Factory
{
private readonly Dictionary<string, Func<object>> _dictionary =
new Dictionary<string, Func<object>>();
public void Register(string name, Func<object> creator)
{
_dictionary[name] = creator;
}
public object Create(string name)
{
Func<object> creator = _dictionary[name];
return creator();
}
}
// Note, I've omitted null validations and checking if the name exists or not
// to make the code smaller. Of course in the final code, we should have the right
// parameter validations and exceptions.
有了这个简单的解决方案,我们可以拥有一个可配置的工厂,我们可以说它已经是一种 IoC 容器。毕竟,执行 factory.Create("Button");
的代码可能期望一个 Button
实例,但它可能是 Button
的任何子类。
使用类型而非字符串
但正如你可能已经想象的那样,通过 string
搜索事物并不好,特别是因为我们可能有多个同名但命名空间不同的类,甚至重构也可能变得更难。因此,考虑到 IoC 通常基于接口,并且即使我们使用工厂,我们也通常会知道对象的预期类型,我们可以将用作字典键的 string
更改为 Type
。
所以,我们可能会有这样的东西
public sealed class Factory
{
private readonly Dictionary<Type, Func<object>> _dictionary =
new Dictionary<Type, Func<object>>();
public void Register(Type type, Func<object> creator)
{
_dictionary[type] = creator;
}
public object Create(Type type)
{
Func<object> creator = _dictionary[type];
return creator();
}
}
// Note, I've omitted null validations and checking if the type exists or not
// to make the code smaller. Of course in the final code, we should have the right
// parameter validations and exceptions.
有了这个新版本,我们不再这样做
Button button = (Button)factory.Create("Button");
而是这样做
Button button = (Button)factory.Create(typeof(Button));
这可能看起来不是一个很大的改进,因为我们仍然需要将结果转换为 Button
,但现在我们已经可以改进解决方案以使用泛型。所以,Create
方法可以像这样
public T Create<T>()
{
Func<object> creator = _dictionary[typeof(T)];
object untypedResult = creator();
return (T)untypedResult;
}
多亏了这一改进,用户可以这样做
var button = factory.Create<Button>();
无需进行任何类型转换,并且只需编写一次类型 (Button
)。(是的,我故意使用了 var
,这样我就可以只写一次 Button
)。
编译时验证?
到目前为止,存在一个问题。用户可以这样做
factory.Register(typeof(Button), () => "Test");
而且,由于我们无法分析委托内部的内容,我们无法验证此类委托的结果,因此它将被注册并在调用 Create
时抛出异常,而不是在调用 Register
时抛出异常。
所以,为了解决这个问题,我们可以像这样重写这个类
public sealed class Factory
{
private readonly Dictionary<Type, Delegate> _dictionary = new Dictionary<Type, Delegate>();
public void Register<T>(Func<T> creator)
{
_dictionary[typeof(T)] = creator;
}
public T Create<T>()
{
Func<T> creator = (Func<T>)_dictionary[typeof(T)];
return creator();
}
}
// Note, I've omitted null validations and checking if the type exists or not
// to make the code smaller. Of course in the final code, we should have the right
// parameter validations and exceptions.
在这个解决方案中,我们不再将委托存储为 Func<object>
。有些人可能认为这会奏效,因为 Func<Button>
可以被视为 Func<object>
,但这在值类型被使用时会失败,而且由于我没有试图在这里设置任何约束,我将委托类型更改为 Delegate
。
现在注册也是泛型的,并且它接收一个 Func<T>
,因此它不允许进行 Register<Button>(() => "Test")
,这在编译时就得到了验证。
而且,我们不再在创建时进行结果转换(这会导致值类型的装箱和拆箱),而是转换字典中的委托,这样我们就会得到一个类型化的结果。
所以现在我们可以做到
factory.Register(() => new Button());
或者更明确地写成
factory.Register<Button>(() => new Button());
事实上,考虑到工厂和 IoC 容器的使用方式,我们更有可能做类似的事情
factory.Register<IButton>(() => new ButtonThatImplementsIButton());
因此,我不喜欢我们不能禁用类型推断,因为这可能会引起一些混淆。但是,好吧,没有什么完美无缺。
通过实际的实现,我们已经拥有了一个非常强大的 工厂/IoC 容器/服务定位器,我想你可以看到它非常小。
有了它,我们已经可以在应用程序的一部分创建工厂实例,以我们想要的方式对其进行初始化(例如,创建真实的类或存根类进行单元测试),而应用程序的另一部分可以依赖于“接口”,而无需知道将创建的真实类是什么。
我们已经可以创建一个使用这种 Factory
的大型应用程序。也许我们希望应用程序可以访问单个工厂实例,如果应用程序是多线程的,这将需要一些锁定,但我们可以通过编写另一个负责静态和执行锁定的类来做到这一点,这样我们就不需要更改实际的类了。
它可能看起来像这样
public static GlobalFactory
{
private static readonly Factory _factory = new Factory();
public static void Register<T>(Func<T> creator)
{
lock(_factory)
_factory.Register(creator);
}
public static T Create<T>()
{
lock(_factory)
return _factory.Create<T>();
}
}
这个新类性能不是最好的,但如果我们要一个全局工厂并且不想再改变我们的工厂,这会是一个解决方案。
缺少第一个修复
我认为目前的工厂存在一个更大的问题,我不是在谈论线程安全、速度或缺乏 null
检查。它的可扩展性不如应有的。
回到文章《软件架构》,我说的许多框架需要的第一个修复是在失败之前触发一个事件。在这段特定的代码中,如果字典中找不到类型,我们的 Create
方法将失败(并且在我添加正确的验证之前将使用字典异常)。
也就是说:如果你执行 Factory.Create<SomeType>()
,并且没有为此类型注册任何内容,它将失败。我们当然可以将其视为配置错误,但想象一下,我们想将工厂配置为使用 List<T>
创建 IList<T>
的实例。我们会配置所有可能的 T
吗?也就是说,我们会将 IList<int>
配置为返回 new List<int>()
,将 IList<string>
配置为返回 new List<string>
,以及所有可能的配置吗?
我的回答是我们不应该这样做。此外,试图支持泛型类型的特殊注册会使代码过于复杂,并且不会像它应该的那样强大。事实上,它会解决这个问题,但仍然可能在其他情况下失败(例如,仅在第一次请求时才从外部程序集加载实现)。
要解决这个问题,只需要一个事件。有这样一个事件
public event EventHandler<ResolveCreationEventArgs> ResolveCreation;
为了支持它,我们将有以下类型
public sealed class ResolveCreationEventArgs:
EventArgs
{
public Type RequestedType { get; internal set; }
public object Result { get; set; }
}
就足以解决问题。当然,我们需要更改 Create
方法,以便它像这样
public T Create<T>()
{
Delegate untypedDelegate;
if (_dictionary.TryGetValue(typeof(T), out untypedDelegate))
{
Func<T> creator = (Func<T>)untypedDelegate;
return creator();
}
var handler = ResolveCreation;
if (handler != null)
{
var args = new ResolveCreationEventArgs();
args.RequestedType = typeof(T);
handler(this, args);
if (args.Result != null)
return args.Result;
}
throw new InvalidOperationException
("It is not possible to find an instance to the given type: " + typeof(T).FullName);
}
差不多了
Create
方法现在更大了,但我不能说它过于复杂。有了这个解决方案,已经可以通过按需加载外部程序集来查找实例。
然而,我不喜欢这个解决方案。这里的问题可能与性能有关。事实上,如果处理程序编写得好,它就不需要存在,但我真的不想指望这一点。例如,我可以添加一个处理程序来尝试从外部程序集加载内容,另一个处理程序专门用于泛型列表。这里的问题由以下因素组成
- 如果我真的添加了查找外部程序集的处理程序,然后是专门用于列表的处理程序,它将首先查找程序集(涉及磁盘读取),然后才使用内存中工作的列表实现。但事实上,这可能是我们想要的,因为可能存在一个包含列表类型特定实现的程序集(例如,
IList<MyObservatleType>
的特定实现)。 - 没有缓存。所以,如果我创建一个列表,考虑到第一个问题,它会查找一个文件,然后使用列表的处理程序。当我创建第二个列表时,它会重新做所有工作。
- 即使处理程序顺序颠倒,我也可以请求一个列表,第一个处理程序执行并创建结果,然后第二个处理程序执行,如果它不检查是否存在结果,它将浪费时间尝试查找合适的程序集。
事实上,我将不触及顺序问题。如果有两个事件处理程序可以为同一类型生成结果,则将处理程序的顺序放置到工厂初始化的代码负责。
但为了解决其他问题,我们不再调用事件来获取结果,而是调用事件来尝试获取一个委托,我们也会将其注册到字典中,这样下次我们就不需要再次调用事件了。
所以,ResolveCreationEventArgs
不再有 Result
属性。它将是一个 Creator
属性,像这样
public Delegate Creator { get; set; }
或者,更好的是,由于我们不允许无效委托,我们可以立即进行验证。像这样
private Delegate _creator;
public Delegate Creator
{
get
{
return _creator;
}
set
{
if (value != null)
{
Type funcType = typeof(Func<>).MakeGenericType(RequestedType);
if (value.GetType() != funcType)
throw new InvalidOperationException
("The Creator must be of type: " + funcType.FullName);
}
_creator = value;
}
}
而 Create
方法不仅会使用新属性并在返回委托时将其存储在字典中,它还会获取 ResolveCreation
的调用列表,一旦其中一个处理程序提供一个创建者,它就能够停止
public T Create<T>()
{
Delegate untypedDelegate;
if (!_dictionary.TryGetValue(typeof(T), out untypedDelegate))
{
var allHandlers = ResolveCreation;
if (allHandlers != null)
{
var args = new ResolveCreationEventArgs();
args.RequestedType = typeof(T);
foreach(EventHandler<ResolveCreationEventArgs>
handler in allHandlers.GetInvocationList())
{
handler(this, args);
if (args.Creator != null)
{
untypedDelegate = args.Creator;
_dictionary.Add(typeof(T), args.Creator);
break;
}
}
}
}
if (untypedDelegate == null)
throw new InvalidOperationException
("It is not possible to find an instance to the given type: " + typeof(T).FullName);
Func<T> creator = (Func<T>)untypedDelegate;
return creator();
}
最后,我们有了一个高度可扩展的 Factory/IoC Container/Service Locator,它能够“延迟加载”创建者委托,仅在首次请求特定类型时才在事件上耗费时间。
这意味着它已经能够处理泛型类型,甚至从外部程序集加载实现。
当然,它并非开箱即用就能从外部程序集加载数据或处理泛型类型,但我们可以在不触及此类的源代码的情况下添加此类支持。
所以,一个可能的处理程序实现,可以从外部程序集加载类型,可以是这样的
ioc.ResolveGet += (sender, args) =>
{
string name = args.RequestedType.Name;
if (!name.StartsWith("I"))
return;
name = name.Substring(1);
string dllName = name + ".dll";
if (!File.Exists(dllName))
return;
var externalAssembly = Assembly.LoadWithPartialName(name);
var type = externalAssembly.GetTypes()[0];
var newExpression = Expression.New(type);
var funcType = typeof(Func<>).MakeGenericType(args.RequestedType);
var lambda = Expression.Lambda(funcType, newExpression);
args.Getter = lambda.Compile();
};
而一个处理泛型类型的方法可以是这样的
ioc.ResolveGet += (sender, args) =>
{
var type = args.RequestedType;
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IList<>))
{
var getListCreator = typeof(Program).GetMethod("GetListCreator");
getListCreator = getListCreator.MakeGenericMethod(type.GetGenericArguments());
args.Getter = (Delegate)getListCreator.Invoke(null, null);
}
};
public static Func<IList<T>> GetListCreator<T>()
{
return () => new List<T>();
}
命名问题
你可能已经看到我将类命名为 Factory
,但在文本中我通常说 Factory/IoC Container/Service Locator。
事实上,Factory
是一个不好的名字。每个创建者委托都是一个工厂。但是,好吧,它们甚至不需要是工厂。
一些 IoC 容器允许用户配置他们想要“单例实例”还是每次都创建新实例。通过使用“创建者”委托,我们可以自由地每次都创建新实例,或者始终返回相同的实例。
所以,为了更正命名,我决定
- 将类重命名为
ExpandableIocContainer
- 将
Create
方法重命名为Get
,并将创建者参数重命名为 getter - 我添加了一些方法以求完整
- 好吧,可能还有其他重命名,我决定添加缺失的参数验证。所以,代码是这样的
using System;
using System.Collections.Generic;
namespace ExpandableIocSample
{
public sealed class ResolveGetEventArgs:
EventArgs
{
public Type RequestedType { get; internal set; }
private Delegate _getter;
public Delegate Getter
{
get
{
return _getter;
}
set
{
if (value != null)
{
Type funcType = typeof(Func<>).MakeGenericType(RequestedType);
if (value.GetType() != funcType)
throw new InvalidOperationException
("The Getter must be of type: " + funcType.FullName);
}
_getter = value;
}
}
}
public sealed class ExpandableIocContainer
{
private readonly Dictionary<Type, Delegate> _dictionary =
new Dictionary<Type, Delegate>();
public void Register<T>(Func<T> getter)
{
if (getter == null)
throw new ArgumentNullException("getter");
_dictionary[typeof(T)] = getter;
}
public bool Unregister<T>()
{
return _dictionary.Remove(typeof(T));
}
public Func<T> GetGetter<T>()
{
var getter = TryGetGetter<T>();
if (getter == null)
throw new InvalidOperationException
("It is not possible to find a getter to the given type: " + typeof(T).FullName);
return getter;
}
public Func<T> TryGetGetter<T>()
{
Delegate untypedDelegate;
if (!_dictionary.TryGetValue(typeof(T), out untypedDelegate))
{
var allHandlers = ResolveGet;
if (allHandlers != null)
{
var args = new ResolveGetEventArgs();
args.RequestedType = typeof(T);
foreach (EventHandler<ResolveGetEventArgs>
handler in allHandlers.GetInvocationList())
{
handler(this, args);
if (args.Getter != null)
{
untypedDelegate = args.Getter;
_dictionary.Add(typeof(T), args.Getter);
break;
}
}
}
}
Func<T> getter = (Func<T>)untypedDelegate;
return getter;
}
public T Get<T>()
{
var getter = TryGetGetter<T>();
if (getter == null)
throw new InvalidOperationException
("It is not possible to find an instance to the given type: " + typeof(T).FullName);
return getter();
}
public event EventHandler<ResolveGetEventArgs> ResolveGet;
}
}
如果你查看新代码,你会发现我添加了 Unregister
、GetGetter
和 TryGetGetter
方法。后两个方法的目的不是直接获取值,而是返回用于获取/创建新值的委托。因此,如果调用者想要创建多个实例,它将能够避免不必要的搜索。
我这样做只是为了在需要时提供最快的解决方案,但对于大多数使用 IoC 容器的场景,性能差异可以忽略不计,因为字典搜索非常快。
多线程、AOP 等。
我真的不期望 IoC 容器会被多个线程使用,所以我编写它时没有考虑任何线程支持。
事实上,我也没有进行任何线程检查,即使这与我在《磐石般的品质》文章中所说的相悖,但那是因为我认为使其线程安全的最简单方法是创建另一个类,在调用这个类之前进行锁定,如果我进行线程检查,那将会失败。
此外,目前的解决方案不支持 AOP(面向切面编程)。你当然可以注册你的 getter 委托,创建实例,然后在返回之前添加其他切面,但是如果你想注册 getter 而不关心其他切面,然后告诉要添加哪些切面,那么目前的类就不支持它。
我曾想过添加一个 Got
事件,这样你就可以在返回之前更改结果(添加切面),但我决定做一些更简单、更强大的事情
我建议你不要直接使用 ExpandableIoCContainer
,而是使用 IIocContainer
接口。因此,对该类的实际更改是使其使用此接口,该接口的声明如下:
public interface IIocContainer
{
void Register<T>(Func<T> getter);
bool Unregister<T>();
T Get<T>();
event EventHandler<ResolveGetEventArgs> ResolveGet;
}
通过将 IoC 容器本身视为一个接口,你可以自由地将当前的 IoC 容器替换为可以添加额外关注点(如支持多线程的锁定、AOP 支持等)的容器。也就是说,你将能够向你的 IoC 容器添加关注点,这可能用于向生成的结果添加关注点(是的,你将使用 AOP 来添加 AOP 支持到项目,太棒了,也很令人困惑,对吧?)。
在此接口中,我没有放置 TryGetGetter
和 GetGetter
方法,因为我认为这些方法过于特定于实现,并且会违反可替换解决方案的目的。
服务定位器与依赖注入
大多数 IoC 容器都有一个特点是依赖注入。依赖注入与服务定位器不同,因为目标类不需要知道存在 IoC 容器。事物只是简单地“注入”进去。
为了更好地理解,这是服务定位器的工作方式
public class ClassWithoutDependencyInjection
{
public ClassWithoutDependencyInjection()
{
OtherService = ServiceLocator.Get<IOtherService>();
}
public IOtherService OtherService { get; private set; }
}
这就是依赖注入
public class ClassWithDependencyInjection
{
public ClassWithDependencyInjection(IOtherService otherService)
{
if (otherService == null)
throw new ArgumentNullException("otherService");
OtherService = otherService;
}
public IOtherService OtherService { get; private set; }
}
正如你所看到的,唯一的区别是 ClassWithoutDependencyInjection
知道 ServiceLocator
(对它有一个引用),而在第二种情况下,它根本不知道服务定位器,但它需要提供一个 OtherService
。第二种解决方案被认为是更好的设计。
但我只是把我在这里介绍的类称为 Ioc Container/Service Locator。那么,它是否有限制呢?
答案是否定的。这个类唯一没有提供给你的是一种“自动”填充构造函数参数的方法,但由于所有事情都是通过委托完成的,你可以使用 IoC 容器注册 ClassWithDependencyInjection
的创建。为此,注册将是这样的
ioc.Register<ClassWithDependencyInjection>
(
() =>
{
IOtherService otherService = ioc.Get<IOtherService>();
return new ClassWithDependencyInjection(otherService);
}
);
而且,由于这是配置 IoC 容器的代码,你无需担心对 IoC 容器的引用。ClassWithDependencyInjection
将正确支持依赖注入,而 IoC 容器的最终用户将无需了解 ClassWithDependencyInjection
的实例是如何创建的,他们只会请求一个。唯一缺少的是一个“注册”功能,我们只告诉要创建的类型,它会自动发现构造函数中的参数并完成工作,但这并不是一个真正的要求,只是一个“锦上添花”的功能。
这种功能可以通过反射轻松创建,但这会很慢。所以,为了使解决方案更完整一些,我决定使用一种非常快速的方法来支持这种注册。但我不确定将来是否会有更好的方法来做这件事,甚至你是否真的想使用它,我将把这种方法放在一个扩展类中。即使看起来我很奇怪地提供了一个单一的解决方案并使用了一个扩展方法,但目的是保持主类“原封不动”。
支持真正依赖注入的代码如下
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
namespace ExpandableIocSample
{
public static class ExpandableIocContainerExtensions
{
// Note: I usually use my ReflectionHelper to be sure that I will get the right method
// in case there's a future change (be it to the method name or even if a new overload
// is added). But as I don't want to add any extra reference, I am using normal reflection.
// If the get is renamed or overload, the next line should be corrected.
private static readonly MethodInfo _getMethod =
typeof(ExpandableIocContainer).GetMethod("Get");
public static void RegisterCallingConstructor<T>
(this IIocContainer iocContainer, ConstructorInfo constructor=null)
{
RegisterCallingConstructor<T, T>(iocContainer, constructor);
}
public static void RegisterCallingConstructor<TPublic, TImplementation>
(this IIocContainer iocContainer, ConstructorInfo constructor=null)
{
if (iocContainer == null)
throw new ArgumentNullException("iocContainer");
if (constructor == null)
{
var constructors = typeof(TImplementation).GetConstructors();
if (constructors.Length == 0)
throw new InvalidOperationException("The type " +
typeof(TImplementation).FullName + " doesn't have any public constructor.");
if (constructors.Length == 1)
constructor = constructors[0];
else
{
var maxParameters = constructors.Select(c => c.GetParameters().Length).Max();
constructor = constructors.Where
(c => c.GetParameters().Length == maxParameters).Single();
}
}
var iocContainerExpression = Expression.Constant(iocContainer);
var parameters = constructor.GetParameters();
int parameterCount = parameters.Length;
var arguments = new Expression[parameterCount];
for(int i=0; i<parameterCount; i++)
{
var parameter = parameters[i];
var typedGet = _getMethod.MakeGenericMethod(parameter.ParameterType);
var argument = Expression.Call(iocContainerExpression, typedGet);
arguments[i] = argument;
}
var newExpression = Expression.New(constructor, arguments);
var lamda = Expression.Lambda<Func<TPublic>>(newExpression);
var implementedDelegate = lamda.Compile();
iocContainer.Register<TPublic>(implementedDelegate);
}
}
}
因此,要在容器中注册一个类型,该类型将使用容器本身来填充构造函数参数,只需这样做即可
iocContainer.RegisterCallingConstructor
</*PossibleInterfaceHere,*/ ClassWithDependencyInjection>();
由于这是一个锦上添花的功能,我让它通过搜索参数最多的构造函数来工作,这样你的构造函数就可以重载。但是为了不限制解决方案,如果你不想使用参数最多的构造函数,你可以自由地将正确的构造函数作为参数。
比较
我经常看到人们比较 IoC 容器的速度。我已经相信这个解决方案因其简洁性而速度很快。为了测试它,我将其与 SimpleInjector
进行了比较,结果是,当我的容器访问单例需要 5 秒时,SimpleInjector
需要 7 秒。我还做了一些其他测试,差异是相同的。
但我的目的不是提供完整的性能比较。我想要比较的是常见 IoC 容器中发现的某些功能。
瞬态/单例
通过使用委托,我们可以轻松支持瞬态和单例模式。
单例模式无非就是始终返回同一个项,因此可以这样做
SomeType instance = new SomeType();
iocContainer.Register<SomeType>(() => instance);
而瞬态,嗯,它甚至更简单
iocContainer.Register<SomeType>(() => new SomeType());
向构造函数传递非 IoC 值
一些 IoC 容器的一个问题是它们希望使用自己的解析来填充所有构造函数参数。
但是由于委托的简洁性,我们可以混合 IoC 返回值和固定值。
就像这样简单
iocContainer.Register<SomeType>(() =>
new SomeType(iocContainer.Get<SomeReference>(), "Our non-IOC value"));
在这行代码中,iocContainer.Get<SomeReference>()
将使用 IoC 容器来解析引用,而“Our non-IOC value”则不来自 IoC。
向 Get 方法传递参数
我刚才谈到了向构造函数调用传递非 IoC 值,但该值仍然是“注册时的固定值”。那么,在调用 Get()
方法时我想传递的值呢?
例如,像这样
iocContainer.Get<ISecureName>("name here");
可能吗?
答案是:是的,但不是直接的。
我曾想过给 Get
方法一个额外的参数,然后可以将其传递给每个委托。但这也会强制委托接收一个额外的参数,或者用一个接收并忽略参数的委托“包装”一个不带参数的委托。
这会降低性能并损害 Unregister
方法。此外,当你构造一个具有许多依赖项并因此构造许多子对象的对象时,很难判断要传递哪个正确的参数。
因此,将参数传递给 Get
方法以最终创建正确实例的“变通方法”是使用“上下文”变量,或者更确切地说,是 [ThreadStatic]
变量。
基本思想是:你设置 [ThreadStatic]
变量,调用 get 方法,为了避免泄漏,清除 [ThreadStatic]
变量。我说 [ThreadStatic]
变量是因为这就是“上下文”变量的创建方式。它们是“静态的”(因此任何实例都可以访问),但不与其他线程共享,因此没有线程问题的风险。
由于这可能很丑陋,如果你确实需要传递特定参数,最好创建一个更好的解决方案来隐藏此类 [ThreadStatic]
变量的使用,但几乎每个“上下文”都是通过使用 [ThreadStatic]
变量创建的。
所以,最简单的解决方案是
SecureNameContext.Value = "name here";
try
{
iocContainer.Get<ISecureName>();
}
finally
{
SecureNameContext.Value = null;
}
// And the SecureNameContext.Value should be a
// [ThreadStatic] field or a property that reads/writes
// a [ThreadStatic] field.
但是我们可以改进它,使其看起来更像这样
using(new SecureNameContext("name here"))
iocContainer.Get<ISecureName>();
当然,我们还有很多可以改进的地方。所以,我不会尝试给出解决方案,如果你需要,我会让你来做。
属性
这个容器没有使用任何属性,因为,嗯,它不需要。
在我看来,拥有像 [Import]
这样的属性几乎等同于在类的构造函数中调用服务定位器。区别在于你将那行代码放在属性的“外部”,而不是放在构造函数内部(这通常会使事情更难理解,而不是更容易)。
所以,如果你想要正确的依赖注入,你的对象应该在构造函数中请求它们的依赖项,然后填充其值的构造函数调用解决方案将起作用。当然,如果你需要填充属性而不是调用构造函数,这个 IoC 将允许你这样做,因为在你给出的委托中,你可以创建所需的obeject,填充其属性(手动或再次使用 IoC 容器),最后返回它。
注意:在展示 ServiceLocator
用法的示例中,我将其用作 static
类。但如果你通过接口将服务定位器作为参数接收,那么你的解决方案比使用 [Attributes]
更好。当然,你将引用声明接口的程序集,但你将能够通过简单地为新的 IoC 容器创建适配器来替换一个 IoC 容器(如果你真的想),这样你就可以将更改集中在一个地方。但是通过使用属性,你将需要一种方法来告诉新的 IoC 容器如何使用该属性(如果可能的话),或者你将需要在使用该属性的每个地方进行适当的更改。
一次性注册多个
许多 IoC 容器使用流畅的方法来注册程序集中符合特定条件的所有类型。
好吧,这个类没有任何流畅的方法,但可以将其添加为扩展方法。事实上,可以急切加载所有需要的类型,也可以延迟加载它们。
无论如何,有了现在的 IoC 容器,你应该编写代码在程序集中搜索类型。但如果你想要预加载,你可能会对程序集中的所有类型进行 foreach
循环,如果它们有效,你将立即注册它们。如果你想要延迟加载,你将只向 ResolveGet
事件添加一个处理程序,该处理程序只会在需要时尝试为类型找到正确的委托。
事实上,示例中提供的用于加载外部程序集的“解决方案”可以通过从接口名称中删除 I
来更改为在实际程序集中查找类型。这样我们就可以轻松地“一次性”注册示例中使用的接口的所有实现。
重要的是要注意,许多 IoC 容器只有急切加载的解决方案。但现在想象一下,由于某种原因,公司拥有的所有库都在同一个文件夹中,你不知道筛选出正确库的特定规则,但你知道当你请求一个实现时,它总是位于一个命名为接口命名空间的库中。急切加载解决方案可能会加载所有程序集,而延迟加载只会在我请求 MyNamespace.ISomething 时加载 MyNamespace。如果我只使用 MyNamespace 中的项目,那么我将只加载一个程序集。好多了,不是吗?
递归
当前的解决方案不支持递归引用。它最终会导致 StackOverflowException
。
事实上,对象 A
无法在构造函数中引用对象 B
,而对象 B
又在构造函数中引用对象 A
。在这种情况下,至少有一个对象应该通过属性设置来接收对另一个对象的引用。因此,正确的方法是创建 A
,创建 B
,然后才填充相互之间的引用。
这样做是可能的,但在注册对象时,需要在填充属性之前创建这两个对象。对于最终用户来说,他们可以请求 A
并收到一个包含引用回 A
的 B
的 A
,但没有什么可以帮助你自动完成。不幸的是,这是一个真正的限制。可能可以通过编写一个更好的 RegisterCallingConstructor
方法(或者,实际上,一个 RegisterCallingConstructorAndFillingProperties
,或者你称之为任何名称)来解决这个问题,这也是我希望将该方法放在一个单独的类中的另一个原因。没有这种方法,主类就不太容易受到更改的影响。
生命周期管理
当前的实现不跟踪创建的对象,因此它没有为创建的对象提供生命周期管理。但与多线程和 AOP 一样,可以创建另一个类,该类使用此 IoC 容器来创建对象,然后自行存储创建的对象,同时还提供额外的方法来告知这些对象何时应该销毁(从而强制不再使用的“引用对象”销毁/释放)。
这样的解决方案也能够处理“上下文”,即使它们是瞬态的,也能有效地返回给定类型的相同对象实例。也就是说,可以做一些像这样
using(iocContainerWithLifetimeManagement.CreateContext())
{
var a1 = iocContainerWithLifetimeManagement.Get<A>();
var a2 = iocContainerWithLifetimeManagement.Get<A>();
}
并且 a1
和 a2
可以是同一个实例,即使它们被写成瞬态的。当然,在上下文之外,它们的瞬态性质将保持不变,并且一个好的容器可以知道如何处理绑定到上下文的瞬态对象和不绑定到上下文的瞬态对象,但是,再次强调,目前尚未提供。
命名注册
在《使用 Unity 进行依赖注入》(第 33 页)一书中,有一个这样的例子
container
.RegisterType<ISurveyAnswerStore, SurveyAnswerStore>(
new InjectionFactory((c, t, s) => new SurveyAnswerStore(
container.Resolve<ITenantStore>(),
container.Resolve<ISurveyAnswerContainerFactory>(),
container.Resolve<IMessageQueue<SurveyAnswerStoredMessage>>(
"Standard"),
container.Resolve<IMessageQueue<SurveyAnswerStoredMessage>>(
"Premium"),
container.Resolve<IBlobContainer<List<string>>>())));
重要的部分是 Resolve()
方法中使用的“Standard
”和“Premium
”参数。
事实上,我最初的解决方案,它使用字符串来查找委托,已经准备好支持这种情况,而当前使用类型的解决方案则不支持。也许为了支持它,我应该回到使用 string
注册,并且可以使用 Type.FullName
作为默认注册,而带名称的注册可以使用类似 Type.FullName + ":" + 用户给定的名称
。
所以,我的可扩展解决方案确实因为我一开始做出的一个决定而存在局限性。当然,我可以很容易地改变它,但我会改变我不想再改变的代码。但为了自卫,我应该说我认为命名注册是一种糟糕的做法。
如果你可以搜索具有不同名称的类型注册,那为什么不创建新类型呢?这样,你就可以搜索 IMessageQueue<T>
或 IPremiumMessageQueue<T>
,它们甚至可以在需要时拥有新方法,并且具有重构友好的优点。
结论
正如你可能已经从比较中看到的那样,这个 IoC 容器与最流行的容器不同,尤其是在注册时不使用“瞬态”和“单例”等术语。
在我看来,它没有真正的限制,但功能不完整。就我个人而言,我没有理由使用其他解决方案,因为我认为它们中的许多都有阻塞性限制(例如,要求类上带有属性,或者只是无法在需要时加载外部程序集),即使它们具有一些额外的功能。
而且,在我的特殊情况下,我知道我可以在需要时添加许多那些缺失的功能。我没有立即这样做,因为我的目的是展示一个简单的解决方案如何通过正确的接口和事件变得真正可扩展。添加其他功能只会使文章变得比需要更复杂。也许将来,我可以添加这些功能并提供单独的文章来解释每个功能的工作原理。
好吧,如果你喜欢使用 IoC 容器(或者如果你现在想开始使用它们),我希望你试用一下这个 IoC 容器。即使你不使用它,我也希望它能帮助你理解 IoC 容器的工作原理,并理解如何普遍地创建“可扩展组件”。
额外内容 - 最终对象与聚合对象
我在文章开头说“IoC 容器的核心是‘请求’和‘结果生成器’之间的映射”,但我随后给出了一个完整的解决方案,它使用 Type
作为输入,也作为要创建的对象的类型。
这使得调用 Get
时可以提供泛型类型,并保证结果将是该类型。
但这与许多 IoC 容器的工作方式有关:它们期望输入类型与输出类型相同。但有许多情况并非如此。
事实上,WPF 中使用的数据模板的整个概念都处于这种情况:我们已经拥有数据,并且我们想要该数据的“视觉表示”。我们的输入类型是数据的类型,但我们的结果是显示该数据的模板。
实现这样的解决方案也不难,但需要注意的是,当输入类型和输出类型相同时,这样的解决方案会比实际的类型化程度低。所以,这个解决方案并没有真正的限制,它有不同的目的。但我认为我会将这样的替代解决方案留作读者的练习。
历史
- 2013 年 12 月 5 日:初始版本