可扩展组件框架
一个基于组件的编程框架(使用依赖注入)。
引言
本文介绍了一个组件框架。基于组件的编程用于创建易于测试和维护的松耦合类。组件通常在其构造函数中获取依赖项,但也可以通过属性分配。
组件框架用于创建和维护组件以及处理依赖项。大多数框架可以每次创建一个新实例,或者让组件成为单例,这意味着每次都返回相同的实例。
该框架使用依赖注入(目前仅支持构造函数注入)来添加依赖项。您可以在此处阅读 Martin Fowler 关于依赖注入和控制反转的文章:此处。
背景
我喜欢学习和提高我的编码技能。我通常通过尝试自己做所有事情来做到这一点,这就是我创建自己的框架的原因。我知道市面上已经有很多不同的组件框架(例如,Castle 项目),这是我的贡献。这是我第二次尝试修复先前尝试中的设计错误。
如果您看过我的Web服务器,您就会知道我喜欢(尝试)编写可扩展和模块化的代码。这个框架也不例外。我将向您展示如何使用属性加载组件,如何使用app.config加载组件,如何从外部程序集加载组件,如何使用版本化的组件,如何创建远程组件(在客户端调用但在服务器端执行或反之亦然的组件),最后是如何添加内部组件(只能从声明它们的同一程序集中检索的组件)。
使用代码
框架中的核心类是ComponentManager
,用于创建和访问组件。它使用反射来识别和加载依赖项。它无法读取配置文件或自动查找组件;需要使用外部帮助类来完成这些操作。
组件使用接口类型和实例类型定义。接口类型用于访问组件,而实例类型用于创建组件。ComponentManager
可以在启动时(使用CreateAll
方法)创建组件,或者在请求组件时创建。如果您使用后者,您可能需要运行ValidateAll
方法来确保所有组件依赖项都已满足。
自动加载组件
要自动加载组件,我们使用一个名为ComponentFinder
的类,该类可以扫描外部程序集或方法调用中指定的程序集以查找组件。它会扫描所有程序集中带有Component
属性的类。
在下面的示例中,我们有一个消息组件实现,它将消息作为电子邮件发送。该组件具有用户管理器和 SMTP 服务器作为依赖项。依赖项将由ComponentManager
分配,并且如果这些组件尚未创建,则由管理器自动创建。
[Component(typeof(IMessageManager))]
public class EmailManager : IMessageManager
{
SmtpServer _server;
IUserManager _userManager;
public EmailManager(IUserManager userMgr, SmtpServer smtp)
{
_userManager = userMgr;
_server = smtp;
}
public void Send(IMessage msg)
{
string email = _userMgr.Get(msg.FromUserId).Email;
_server.Send(email, msg.To, msg.Subject, msg.Body);
}
}
创建ComponentManager
并加载组件很容易
ComponentManager mgr = new ComponentManager();
// Only load components from the current assembly.
ComponentFinder finder = new ComponentFinder();
finder.Find(new List<assembly>() { GetType().Assembly });
mgr.Add(finder.Components);
// And to access/create a component:
IMessageManager messageMgr = mgr.Get<IMessageManager>();
从 app.config 加载
组件可以使用app.config定义,其中还可以指定简单的构造函数参数。
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="Components"
type="Fadd.Components.ConfigSectionHandler,Fadd.Components"/>
</configSections>
<Components>
<Component
Interface="Fadd.Components.Sample.SmtpServer, Fadd.Components.Sample"
Instance="Fadd.Components.Sample.SmtpServer, Fadd.Components.Sample"
IsSingleton="true">
<Parameter Name="hostName">smtp.yourserver.com</Parameter>
<Parameter Name="port" Type="System.Int32">25</Parameter>
</Component>
<Component
Interface="YourApplication.Shared.IMessageManager, MessageManagers"
Instance="MessageManagers.Email.Manager, YourApplication" />
</Components>
</configuration>
配置文件将加载两个组件。SmtpServer
在其构造函数中有hostName
和port
参数。在本例中,我们正在使用一个电子邮件管理器,但我们也可以通过仅修改配置文件轻松切换到其他东西。这是可能的,因为其他所有内容都访问接口IMessageManager
而不是实现。
从外部程序集加载组件
同样,我们将使用ComponentFinder
来查找组件。这次,我们将扫描应用程序文件夹中的所有 DLL 以查找组件。此扫描在一个单独的 AppDomain 中完成,以避免所有扫描过的程序集都加载到主应用程序域中。
ComponentFinder finder = new ComponentFinder();
finder.Find("*.dll");
componentManager.Add(finder.Components);
版本化组件
假设一个第三方程序员创建了一个包含许多有用组件的非常好的程序集。问题是,其中一个组件必须被替换,而所有其他组件都能正常工作。您可以创建自己的ComponentFinder
来忽略有问题的组件。或者,您可以指定一个版本号更高的实现。
[Component(typeof(IUsefulComponent), 2)]
class MyReplacement : IUsefulComponent
{
}
您可以拥有任意数量的实现,但只会使用版本号最高的那个。
RunAt 属性
还有另一种确定使用哪个实现的方法。那就是RunAt
属性。您可能希望在客户端使用一个实现,而该实现会联系服务器端的另一个实现。
在此示例中,我们有一个用户管理器,它不仅从数据库检索用户,还在内存中维护状态信息等。这意味着客户端无法使用相同的组件,因为状态会在客户端和服务器之间有所不同。
[Component(typeof(IUserManager), "Server")]
public class ServerUserManager : IUserManager
{
}
[Component(typeof(IUserManager), "Client")]
public class ClientUserManager : IUserManager
{
}
Remoting
我还没有时间研究动态代理,但我的目的是在框架的未来版本中能够动态地创建远程组件。我已经想出了一个处理事件的解决方案,但我真的不知道是否会实现它,因为远程事件可能会对服务器端产生很大的性能影响。
远程处理目前是通过使用RemotingChannel
完成的;您需要在客户端和服务器端分别创建一个。
private void Remoting()
{
// This is typically done in your server.
ComponentManager server = SetupServerRemoting();
// Typically done in your client applications.
ComponentManager client = SetupClientRemoting();
// Invoke a method in your client to get it executed in your server.
string myMessages = client.Get<IMessageManager>().GetMessages();
}
private ComponentManager SetupClientRemoting()
{
// We'll create a new component manager for this example only.
// Normally you have already created a component manager in your system,
// which also is used for the remoting.
ComponentManager clientManager = new ComponentManager {Location = "Client"};
// Find all components in our current assembly.
// ComponentManager will only add components with the correct RunAt property.
ComponentFinder finder = new ComponentFinder();
finder.Find(new List<Assembly>() { GetType().Assembly });
clientManager.Add(finder.Components);
// Define where we should connect
RemotingChannel client = new RemotingChannel(clientManager, false);
client.Start(new IPEndPoint(IPAddress.Loopback, 8334));
return clientManager;
}
private ComponentManager SetupServerRemoting()
{
// We'll create a new component manager for this example only.
// Normally you have already created a component manager in your system,
// which also is used for the remoting.
ComponentManager manager = new ComponentManager { Location = "Server" };
// Find all components in our current assembly.
// ComponentManager will only add components
// with the correct RunAt property.
ComponentFinder finder = new ComponentFinder();
finder.Find(new List<Assembly>() { GetType().Assembly });
manager.Add(finder.Components);
// Setup remoting, we should accept connections on port 8834.
RemotingChannel server = new RemotingChannel(manager, true);
server.Start(new IPEndPoint(IPAddress.Loopback, 8334));
return manager;
}
客户端组件基本上是一个使用远程处理通道的骨架,看起来像这样
[Component(typeof(IMessageManager), "Client")]
class ClientMessageManager : IMessageManager
{
private readonly RemotingChannel _channel;
public ClientMessageManager(RemotingChannel channel)
{
_channel = channel;
}
public void Send(string receiver, string message)
{
_channel.Invoke(typeof(IMessageManager),
"Send", receiver, message);
}
public string GetMessages()
{
return (string)_channel.Invoke(
typeof (IMessageManager), "GetMessages");
}
}
内部组件
当您开始进行基于组件的编程时,您很可能会有private
/internal
组件,它们只能从声明它们的程序集内部访问。但是,您希望利用组件系统来创建和访问您的内部组件并满足它们的依赖项。这可以通过在添加组件时通过调用传递ComponentFlags.Internal
标志来实现。
特殊启动
我有一些需要在所有组件添加和创建后启动的组件。这可以通过首先定义一个接口来实现
public interface IStartable
{
void Start();
}
然后,让所有需要启动的组件实现该接口。最后,我们使用一个名为VisitAll
的方法来启动组件。
_manager.VisitAll((type, instance) => { if (instance is IStartable)
((IStartable) instance).Start(); });
最后的寄语
代码仍然非常年轻(几周),可能还有一些错误需要修复。但是,代码应该是可用的,我的目标是尽快修复您和我找到的所有错误。
该项目也可以在CodePlex找到。
历史
- 2009-03-10:第一个版本。