Griffin.Container 玩得开心
一个具有模块、装饰器、命令、领域事件等的控制反转容器。
Griffin.Container 是一个性能相当不错的控制反转容器。它的目标不是成为最快的容器,也不是拥有最丰富功能的 API。
相反,我尝试采用一种“便利优于配置”的方法。你可能需要更好地设计你的应用程序,而不是使用大量的容器功能。有关最佳实践的更多信息,请阅读我以前的IoC / 依赖注入文章。
本文的目的是向您展示 Griffin.Container 的强大功能及其易于使用的 API。关于示例 zip 文件的快速说明:示例是本文的补充。阅读本文并下载 zip 文件以充分利用所有内容。
让我们从一个简单的“hello world”项目开始。创建一个新的控制台项目,然后在程序包管理器控制台中运行“install-package griffin.container
”。然后添加以下代码:
public class Hello
{
public void World()
{
Console.WriteLine("Hello world");
}
}
public class Program
{
public static void Main(string[] args)
{
// setup.
var registrar = new ContainerRegistrar();
registrar.RegisterConcrete<Hello>(Lifetime.Transient);
var container = registrar.Build();
var hello = container.Resolve<Hello>();
hello.World();
Console.WriteLine("Press enter to quit");
Console.ReadLine();
}
}
就是这样。
当您调用 Resolve()
时,容器将创建一个 Hello
类型的对象。每次调用 resolve 时,您都会获得一个新的实例。这就是 Transient
的含义。让我们来演示一下
public class Hello
{
private static int Counter;
private int _myId;
public Hello()
{
_myId = Counter++;
}
public void World()
{
Console.WriteLine("Hello world " + _myId);
}
}
public class Program
{
public static void Main(string[] args)
{
// setup.
var registrar = new ContainerRegistrar();
registrar.RegisterConcrete<Hello>(Lifetime.Transient);
var container = registrar.Build();
container.Resolve<Hello>().World();
container.Resolve<Hello>().World();
container.Resolve<Hello>().World();
container.Resolve<Hello>().World();
Console.WriteLine("Press enter to quit");
Console.ReadLine();
}
}
这将显示
Hello world 0 Hello world 1 Hello world 2 Hello world 3
因为我们调用了 Resolve()
四次。如果我们每次都想要同一个实例,我们可以改为声明“Hello”类是 Singleton。这只需要更改一行代码
registrar.RegisterConcrete<Hello>(Lifetime.Singleton);
现在再次运行代码,您会看到“Hello world 0”重复四次。有限的生命周期 / 范围 / 清理
容器的另一个重要方面是能够在类使用后将其处置。让我们更改Hello
类,使其实现 IDisposable
。public class Hello : IDisposable
{
// [...]
public void Dispose()
{
Console.WriteLine("I'm being disposed like trash :(");
}
}
容器使用称为“范围”的东西来实现这一点。范围仅仅意味着容器会跟踪所有具有有限生命周期的已创建对象。实际生命周期在您处置范围时结束。范围的使用方式如下:using (var scope = container.CreateChildContainer())
{
var hello = scope.Resolve<Hello>();
}
由于我们没有将类标记为范围,因此 Hello
对象没有任何反应。让我们这样做:registrar.RegisterConcrete<Hello>(Lifetime.Scoped);
就这样。完整示例public class Hello : IDisposable
{
private static int Counter;
private int _myId;
public Hello()
{
_myId = Counter++;
}
public void World()
{
Console.WriteLine("Hello world " + _myId);
}
public void Dispose()
{
Console.WriteLine("I'm being disposed like trash :(");
}
}
public class Program
{
public static void Main(string[] args)
{
// setup.
var registrar = new ContainerRegistrar();
registrar.RegisterConcrete<Hello>(Lifetime.Scoped);
var container = registrar.Build();
using (var scope = container.CreateChildContainer())
{
scope.Resolve<Hello>().World();
scope.Resolve<Hello>().World();
scope.Resolve<Hello>().World();
scope.Resolve<Hello>().World();
}
Console.WriteLine("Press enter to quit");
Console.ReadLine();
}
}
您将看到为您创建和处置了四个对象。范围对于请求/响应式应用程序(例如 Web 应用程序或 Web 服务)非常有用,因为所有内容都会自动为您创建和清理。无需手动管理状态。
我用 Griffin.Container 做的一个设计决定是,永远不允许您像这样从主容器创建范围对象:
public class Program
{
public static void Main(string[] args)
{
// setup.
var registrar = new ContainerRegistrar();
registrar.RegisterConcrete<Hello>(Lifetime.Scoped);
var container = registrar.Build();
var hello = container.Resolve<Hello>();
}
}
该示例将抛出异常,因为您已将 Hello
配置为范围内的,但却在主容器中创建它,这实际上使其成为单例。这是一个潜在的错误。Hello
类可能使用了 TCP 连接或数据库连接。这些连接可能不会永远打开,因此当连接断开时,该类将无法处理所有后续调用(因为连接状态未被管理,而且该类并非为长期运行而设计)。使用接口
开始使用 IoC 容器时,您可能也希望更频繁地使用接口。使用容器,您永远不必关心超过类契约(通常是接口)的内容。也就是说,您永远不必知道何时以及如何创建和处置对象。容器会为您处理这些问题。让我们让Hello
实现另一个接口:// I print state, get it? muahahaha.
public interface IPrintState
{
void PrintState(TextWriter writer);
}
public class Hello : IDisposable, IPrintState
{
private static int Counter;
private int _myId;
public Hello()
{
_myId = Counter++;
}
public void World()
{
Console.WriteLine("Hello world " + _myId);
}
public void PrintState(TextWriter writer)
{
writer.WriteLine("Hello #" + _myId + " is alive and kicking!");
}
public void Dispose()
{
Console.WriteLine("I'm being disposed like trash :(");
}
}
public class SomeOther : IPrintState
{
public void PrintState(TextWriter writer)
{
writer.WriteLine("I do something else");
}
}
保持接口小巧且定义明确的好处是它们易于重用和实现。以下代码片段是打印实现该接口的所有类的状态所需的全部内容:public class Program
{
public static void Main(string[] args)
{
var registrar = new ContainerRegistrar();
registrar.RegisterConcrete<Hello>(Lifetime.Transient);
registrar.RegisterConcrete<SomeOther>(Lifetime.Transient);
var container = registrar.Build();
// Go through all classes that implements IPrintState
var sb = new StringBuilder();
var writer = new StringWriter(sb);
foreach (var stateWriter in container.ResolveAll<IPrintState>())
{
stateWriter.PrintState(writer);
}
Console.WriteLine(sb.ToString());
Console.WriteLine("Press enter to quit");
Console.ReadLine();
}
}
请注意,您不必执行任何特殊操作即可将 Hello
注册为 IPrintState
的实现。registrar.RegisterConcrete()
方法会将该类注册为所有非 .NET 接口。依赖注入
之前的示例仅向您展示了如何创建对象和控制对象生命周期。这就是控制反转部分。然而,IoC 容器也用于依赖注入。也就是说,识别并将所有依赖项传递给您的类。
这通常通过构造函数注入完成(这也是 Griffin.Container 中唯一支持的方法)。构造函数注入很有用,因为您可以直接识别类拥有的依赖项。
public class UserService
{
public UserService(IUserRepository repository)
{
}
}
该类告诉我们,用户服务需要一个数据源来加载/存储它对用户所做的更改。以下代码段执行相同操作:public class UserService
{
public UserService()
{
_userRepository = new UserRepository("MyConnectionString");
}
}
不同之处在于第一个示例不依赖于特定的实现(数据源)。我们还可以独立于 UserService 实例来控制存储库的生命周期。例如,我们可以让所有 UserService
实例共享同一个存储库实例,以便在其中进行缓存。
至于注册,您不必执行任何特殊操作即可利用依赖注入。容器会自动为您处理。请记住,优先使用接口而不是类作为依赖项。并尽量保持它们尽可能小,以使代码更具灵活性,更易于重构。
最佳实践
依赖于抽象(接口)而不是具体(类)。您的应用程序会更容易重构。尝试将接口分成更小的部分。例如,IUserRepository
可以是 IUserQueries
和 IUserStorage
。但是,它们仍然可以由同一个类实现(但之后可以移动查询、缓存等,而无需修改依赖代码)。
注册
注册过程是所有容器有所不同的地方。我建议您不要手动注册每个类(通过调用 registrar.RegisterXXX()
方法之一)。当然,您应该避免更改/重构代码(请参阅 SOLID)。但实际上,我们很少有人能写出无需重构的代码。相反,我们会更改类。这些更改将影响注册。使它们保持同步的最佳方法是从类本身管理注册(生命周期)。
最佳实践
如果可能,请使用 [Component]
属性。它在 Sandcastle 生成的文档中可见,并且使重构应用程序更容易。尽量对所有类使用相同的生命周期(即,不要在属性中指定,而是在 LoadComponents()
行中指定)。
[Component] 属性
要使Hello
类成为范围内的,您只需编写:[Component(Lifetime.Scoped)]
public class HelloWorld
{
// ....
}
并在注册器中将其注册为:registrar.RegisterComponents(Lifetime.Scoped, Assembly.GetExecutingAssembly());
注意三点RegisterComponents
将注册程序集中所有带有[Component]
属性的类。- 我们在
[Component]
属性中指定了生命周期。这明确告诉我们该类必须具有特定的生命周期。 - 我们在
RegisterComponents()
属性中指定了一个附加的生命周期,该生命周期将用于所有在[Component]
中未指定生命周期的类。
[Component(Lifetime = Lifetime.Scoped)]
public class Hello
{
private static int Counter;
private int _myId;
public Hello()
{
_myId = Counter++;
}
public void World()
{
Console.WriteLine("Hello world " + _myId);
}
}
[Component]
public class MyTransient
{
private static int Counter;
private int _myId;
public MyTransient()
{
_myId = Counter++;
}
public void Print()
{
Console.WriteLine("Transient Id " + _myId);
}
}
public class Program
{
public static void Main(string[] args)
{
var registrar = new ContainerRegistrar();
registrar.RegisterComponents(Lifetime.Transient, Assembly.GetExecutingAssembly());
var container = registrar.Build();
using (var scope = container.CreateChildContainer())
{
// should print the same id
scope.Resolve<Hello>().World();
scope.Resolve<Hello>().World();
scope.Resolve<Hello>().World();
// should print three different ids
scope.Resolve<MyTransient>().Print();
scope.Resolve<MyTransient>().Print();
scope.Resolve<MyTransient>().Print();
}
Console.WriteLine("Press enter to quit");
Console.ReadLine();
}
}
同一注册行(对 RegisterComponents()
的调用)应该适用于您自己的大多数类。自定义注册
有时您需要更精细地控制注册过程。NHibernate/EF4/RavenDB 连接就是一个典型的例子。RavenDb 的注册如下所示:registrar.RegisterService<IDocumentSession>(container => _documentStore.OpenSession(), Lifetime.Scoped);
Griffin.Container 中的注册接口如下所示:public interface IContainerRegistrar
{
IEnumerable<ComponentRegistration> Registrations { get; }
// Component attribute
void RegisterComponents(Lifetime defaultLifetime, string path, string filePattern);
void RegisterComponents(Lifetime defaultLifetime, params Assembly[] assemblies);
// Modules, will come back to those
void RegisterModules(string path, string filePattern);
void RegisterModules(params Assembly[] assemblies);
// Register classes (as themselves if no interfaces is implemented, or as all non-dotnet interfaces)
void RegisterConcrete<TConcrete>(Lifetime lifetime) where TConcrete : class;
void RegisterConcrete(Type concrete, Lifetime lifetime);
// Register an interface using your own factory method
void RegisterService<TService>(Func<IServiceLocator, TService> factory, Lifetime lifetime);
void RegisterService(Type service, Func<IServiceLocator, object> factory, Lifetime lifetime);
// Register using a specific 1-1 mapping
void RegisterType<TService, TConcrete>(Lifetime lifetime) where TService : class where TConcrete : TService;
void RegisterType(Type service, Type concrete, Lifetime lifetime);
// Register a previously created instance (an object which already exists)
void RegisterInstance<TService>(TService instance) where TService : class;
void RegisterInstance(Type service, object concrete);
}
可以在本文底部找到在线文档的链接。RegisterComponents
用于注册所有带有[Component]
属性的类。它可以扫描一个/多个指定的程序集,或加载/扫描特定文件夹中的程序集。RegisterModules
模块是将注册过程从一个位置转移到多个位置的绝佳方式,从而使您系统中的每个模块都能负责自己的注册。这使得您的应用程序更易于维护,并让您更好地了解每个模块注册了什么。稍后我将详细介绍模块。RegisterConcrete
RegisterConcrete
将类本身注册到容器中public class MyClass
{
}
registrar.RegisterConcrete<MyClass>();
// [...]
container.Resolve<MyClass>();
或作为实现的接口(如果存在且不是 .NET Framework 接口)。public class MyClass : IEnumerable<Item>, IMyService
{
}
registrar.RegisterConcrete<MyClass>();
// [...]
// works
container.Resolve<IMyService>();
// wont work
contianer.Resolve<IEnumerable<Item>>();
RegisterService
使用自定义工厂方法注册服务(通常是接口)。通常仅用于在容器中注册外部依赖项(例如数据库库)。RegisterType
将特定的服务(接口)注册到特定的具体(类)。通常仅用于在容器中注册外部依赖项(例如数据库库)。RegisterInstance
注册已在某处创建的单例。模块
Griffin.Container 允许您使用模块将注册责任从一个位置转移到应用程序中的每个模块/包。您需要做的就是创建一个新类并让它继承 IContainerModule
。
public class UserRegistrations : IContainerModule
{
public void Register(IContainerRegistrar registrar)
{
registrar.RegisterConcrete<UserService>(Lifetime.Scoped);
registrar.RegisterConcrete<UserCache>(Lifetime.Singleton);
registrar.RegisterConcrete<UserRepository>(Lifetime.Scoped);
}
}
接下来,您必须加载所有模块public class Program
{
public static void Main(string[] args)
{
var registrar = new ContainerRegistrar();
// will load all modules from all assemblies which starts with "MyApp."
registrar.RegisterModules(Environment.CurrentDirectory, "MyApp.*.dll");
var container = registrar.Build();
}
}
使用此技术,插件体系结构将变得过时(除非您需要能够禁用/卸载插件)。只需将新程序集放入应用程序文件夹并重新启动应用程序即可。
最佳实践
在每个项目中使用组合根,并使用它为当前程序集调用 registrar.RegisterComponents()
(即,将 ContainerModule
放在项目的根命名空间中)。
领域事件
Griffin.Container 内置支持领域事件(在同一进程内处理)。领域事件意味着当发生某事时,您会发送一个事件。例如 UserCreated
、AccountLocked
、ReplyPosted
等。这些事件使您能够创建松耦合的应用程序。例如,您可以订阅 ReplyPosted
事件来发送电子邮件通知(而不必更改创建/保存回复的类)。
.NET 中的事件模型与 Griffin.Container 中的事件模型之间的区别在于后者是松耦合的。
订阅
订阅很容易。只需让任何类实现IHandlerOf<T>
[Component]
public class ReplyEmailNotification : IHandlerOf<ReplyPosted>
{
ISmtpClient _client;
IUserQueries _userQueries;
public ReplyEmailNotification(ISmtpClient client, IUserQueries userQueries)
{
_client = client;
_userQueries = userQueries;
}
public void Invoke(ReplyPosted e)
{
var user = _userQueries.Get(e.PosterId);
_client.Send(new MailMessage(user.Email, "bla bla"));
}
}
派发
领域事件使用DomainEvent
类进行派发。实际的领域事件可以是任何类,没有限制。但我建议您将其视为 DTO。public class UserCreated
{
public UserCreated(string id, string displayName)
{
}
}
public class UserService
{
public void Create(string displayName)
{
//create user
// [...]
// fire the event.
DomainEvent.Publish(new UserCreated(user.Id, user.DisplayName));
}
}
最佳实践
避免将领域对象/模型附加到事件,而是尝试将领域事件视为不可变 DTO。如果您以后需要扩展应用程序,这会容易得多。当您收到领域事件时,您始终可以通过存储库/查询加载正确的领域模型。
命令
Griffin.Container 还内置了命令模式实现。我选择将实际处理(ICommandHandler<T>
)与命令类(ICommand
)分开。这使我们能够随处处理命令。例如,您可以开始在同一个服务器/进程中处理所有命令,但稍后将一些命令路由到不同的服务器。
始终尝试为每个用例创建一个命令,而不是创建小的命令然后将它们链接起来。随着应用程序的增长,链接的命令很可能会导致混乱。
如前所述,我们可以通过将命令处理移到不同的进程来扩展应用程序。要实现这一点,我们必须将所有命令视为异步的。这意味着两件事:
- 命令永远不能返回值。
- 即使
ICommandDispatcher.Dispatch()
已返回,也不要期望命令已被执行。
相反,等待领域事件并使用它们进行额外处理。
设置
命令调度器没有默认实现。相反,我们必须为其分配一个。有一个内置的,称为ContainerDispatcher
。您可能已经猜到,它使用 IoC 容器来查找正确的处理程序。CommandDispatcher.Assign(new ContainerDispatcher(myContainer));
但是,由于命令通常从服务、控制器等处执行,因此我建议您在容器中注册 ICommandDispatcher
实现并通过依赖注入获取它。(尽管调度程序可以被视为基础设施组件,但有待讨论)。
一个命令
命令是一个普通的 .NET 类,用于将命令及其参数传递给命令处理程序。
示例命令:
public class CreateUser
{
public CreateUser(string userName)
{
if (userName == null) throw new ArgumentNullException("userName");
UserName = userName;
}
public string UserName { get; private set; }
public string DisplayName { get; set; }
}
请注意,userName
是通过构造函数传递的(并具有私有设置器),而 DisplayName
可以通过属性设置。这表明 UserName
是必需的,而 DisplayName
是可选的。始终尝试遵循此模式,因为它使区分所需信息更容易。处理命令
命令由实现IHandlerOf<T>
接口的类处理。public class CreateUserHandler : IHandlerOf<CreateUser>
{
IUserRepository _repos;
public CreateUserHandler(IUserRepository userRepository)
{
_repos = userRepository;
}
public void Invoke(CreateUser cmd)
{
var user = _repos.Create(cmd.UserName, cmd.DisplayName);
DomainEvent.Dispatch(new UserCreated(user));
}
}
在此示例中,我使用了存储库,但它也可以是普通的 ADO.NET 或 Web 服务。它真的无关紧要,因为没有代码依赖于实现。调用命令
简单CommandDispatcher.Dispatch(new CreateUser("arne"));
我建议您在容器中注册调度程序,并将其作为依赖项引入您的类中。这可以使意图更清晰(尽管它可能被视为基础设施依赖项)。public class SampleService
{
ICommandDispatcher _dispatcher;
public SampleService(ICommandDispatcher dispatcher)
{
if (dispatcher == null) throw new ArgumentNullException("dispatcher");
_dispatcher = dispatcher;
}
public void ProcessStuff()
{
// [...]
_dispatcher.Invoke(new StoreResult(someResult));
}
}
public class Program
{
public static void Main(string[] args)
{
var registrar = new ContainerRegistrar();
registrar.RegisterConcrete<ContainerDispatcher>(Lifetime.Singleton);
registrar.RegisterConcrete<SampleService>(Lifetime.Transient);
var container = registrar.Build();
}
}
装饰器
装饰器模式允许我们“装饰”一个类以赋予它更多功能。Griffin.Container 使您可以通过实现IInstanceDecorator
接口来创建装饰器。public interface IInstanceDecorator
{
void PreScan(IEnumerable<Type> concretes);
void Decorate(DecoratorContext context);
}
手动操作可能会有些麻烦。因此,您可以为此容器使用 Griffin.Container.Interception
nuget 包,使其更容易一些。
让我们记录容器中注册的所有类的所有方法调用。听起来可能很难,但其实很简单。
首先,在程序包管理器控制台中运行 install-package griffin.container.interception
。然后输入以下代码:
public class ConsoleLoggingDecorator : CastleDecorator
{
public override void PreScan(IEnumerable<Type> concretes)
{
}
protected override IInterceptor CreateInterceptor(DecoratorContext context)
{
return new LoggingInterceptor();
}
}
public class LoggingInterceptor : IInterceptor
{
public void Intercept(IInvocation invocation)
{
var args = string.Join(", ", invocation.Arguments.Select(x => x.ToString()));
Console.WriteLine("{0}({1})", invocation.Method.Name, args);
invocation.Proceed();
}
}
请注意,所有拦截的类都必须具有虚方法才能起作用(Castle.Proxy 的限制)。
在容器中注册装饰器
container.AddDecorator(new ConsoleLoggingDecorator());
让我们用我们的存储库试试container.Resolve<IUserRepository>().Get(10);
控制台输出:
Get(10)
请注意,装饰器会影响性能。但并没有您想象的那么严重(搜索 Castle Proxy 的性能基准)。
内置装饰器
Interception 包中内置了一个装饰器。
异常记录器
此装饰器将拦截所有类,并记录所有异常以及方法名称和所有参数值。
示例输出
SampleApplication.Users.UserService.Get(10) threw an exception:
(exception info would be here if this were not an article, which it is)
当然,异常仍将被抛出。这取决于您如何处理它。
(装饰器只是为了说明如何创建您自己的装饰器而添加的)
装饰器总结
将命令处理程序和装饰器结合起来可能非常有用。例如,您可以使用装饰器通过 DataAnnotations 验证所有命令,然后再调用实际的处理程序。这样,验证就集中在一个装饰器中,而不是分散在每个命令处理程序中。您还可以创建一个命令处理程序装饰器来处理数据库事务。
扩展容器
在核心中,我像避瘟疫一样避免了扩展方法,因为扩展方法会阻止通过继承进行扩展(您们都知道这是 OOP 的基础之一)。核心本身已分为三个接口。
在阅读本节时,请随时查看源代码,这样会更有意义。
第一个是 IContainerRegistrar
。它的目的是公开用于在容器中注册服务和具体项的注册接口。这是它的唯一职责。所有注册项都通过 Registrations
属性公开。
下一个接口是 IContainerBuilder
。它的目的是分析所有注册项,并为每个服务创建构建计划(IBuildPlan
)。构建计划用于创建/访问实现服务的所有类。为此,它为每个具体项/类存储一个 IInstanceStrategy
。该策略决定何时创建新实例以及何时从存储中获取实例。
最后一部分是服务定位。核心接口是 IServiceLocator
,它包含基本解析方法。它被 IParentContainer
继承,该接口添加了 CreateChildContainer()
方法,以及 IChildContainer
接口,该接口也继承了 IDisposable
。
框架库
还有一些框架库可供您喜欢的框架使用。
欢迎贡献更多包。
总结
我希望您喜欢这篇文章,并会尝试使用该容器。
不要忘记下载示例代码,它会一步一步地引导您了解 griffin.container 的功能。
代码也可在 GitHub 上找到: