Introducing Castle - Part I(原文)
Introduction
The construction of a well-designed system is challenging, whatever its size, and the main challenges could be summarized as follows:
- Objects' responsibilities
- Extensibility
- Reutilization
By objects' responsibilities I mean the ability of designing classes that have a clear, distinct responsibility. In other words, a class should have only one concern and the concern should be very explicit through its name and operations. It might sound trivial but any developer knows it's not. Unfortunately, this only comes with experience, and there is no shortcut.
不论项目大小,设计一个优秀的系统构架都是一个挑战。概括的来说,主要包括以下几方的挑战:
- Objects' responsibilities
- 可扩展性
- 可重用性
在Objects' responsibilities方面我认为设计的类都必须有清晰可区分的功能。换句话说:一个类仅仅只能一个关注点并且这个关注点可以通个它的名字与操作(方法)清楚的表现。这看起很简单,但每一个有开发经验的人都知道不是这样的。而且不幸的是,要做到这一点都需要经验而没有其它任何捷径。
Extensibility, however, is a polemic topic as XP enthusiasts, like myself, advocates that the extensibility exposed by an application should match the requirements brought by the client. Nothing more, nothing less.
Reuse is tricky. The more your classes depend on each other, the bigger is your headache to reuse them in different applications or different contexts. Usually, we talk in levels of linkage: is your system tight coupled or loosely coupled?
可扩展性。这个主题对于热衷于采用XP方式的开发者来说有点异议,比如我,他们提倡只需要达到客户需求的可扩展性即可,不要多也不要少。
可重性性。类之间的信赖程度越高,那么在不同的应用或环境中重用这些类将越困难。通常也就行语说的:系统是高耦合还低耦合的!(不好译,请高手指点怎样译更好)
This is the first part of three articles intended to show how inversion of control might result in less code, simpler design and testable software. In this part, I'll mainly focus on what is a decoupled design and how an inversion of control container might help you. I'll also describe Castle's Windsor Container, how it works, and how to augment it.
为了向你展示采用IOC你将能用更少的代码,更简单的设计得到有可测试的软件我写下这三篇文章,这里是第一部分:我将主要介绍什么是a decoupled design 和IOC容器。我也会介绍Castle's Windsor Container,以及怎样使用它。
Inversion of Control
What's inversion of control, anyway? Inversion of control is a simple principle that dictates that an external entity should send messages to programmer's objects in order to perform some specialized action or to allow the programmer to override some logic. This is the main difference between an API and a framework. On the first case, your object uses the API in an active fashion; in the second case, the framework uses your objects, so your objects are now in a passive mode.
Putting off all the hype about Inversion of Control and the wrong meaning some legends of software development try to impose to it, inversion of control is a nice design principle to construct a loosely coupled application. It requires that you shift your paradigm, though. So, before you embrace the principle, do some - or several - experimentations. It might be good to browse the code of some projects that use inversion of control too, like Cocoon, James and Turbine.
什么是IOC?简单的来说就是要求一个外部实体要完成某个方法调用时应是发送消息给其它对象来完成或允许开发者重写处理逻辑。这与API或框架有重要的区别:首先,对象调用API是主动,第二,框架使用你的对象时你的对象则被动的。
脱去IOC广告外衣与一些理想软件开发者强加于它的错误概念,IOC应该是构架松耦合应用的绝佳设计。但是它要求改变你的惯例。所以,要你接受这个原则之前,需要动动手来体验它。看看采用IOC的项目如Cocoon,James和Turbine就是一个好的注意。
So, one of the uses of inversion of control that solves the loosely coupled problem (or one aspect of it) is by inverting how your class obtains external object references or configuration. Let's work on a not-so-distant-from-the-real-world example:+
所以,改变对象引用或配置方式可以解决或部分解决这个问题。不扯远了,我们就从一个真实的示例开始:
using System;
using System.Configuration;
public class MyAwfulEmailClass
{
public MyAwfulEmailClass()
{
}
public void SendEmail( String from, String to, String message, String templateName )
{
String host = ConfigurationSettings.AppSettings["smtphost"];
int port = Convert.ToInt( ConfigurationSettings.AppSettings["smtpport"] );
NVelocityTemplateEngine engine = new NVelocityTemplateEngine();
String newMessage = engine.Process( message, templateName );
// Finally send message...
}
}
This class has a few lines `of code, but has screaming problems:
- It obtains configuration from the ConfigurationSettings. What if the configuration is not there? What if you'd like to use Yaml instead of XML to hold configuration?
- It instantiates a template engine. In fact, there are two problems here: one is that this class has more responsibility than the name implies. It sends an email, that's for sure, but it also uses a template engine to process the message contents. The second problem is that the class has deep knowledge about the template engine it uses. If you'd like to change the engine later, you're going to dig code.
这个类仅仅几行代码,但却有令人捧腹大笑的问题:(怎样译,这样的问题不会令人捧股吧)
- 首先它从 ConfigurationSettings. 获取配置项。那么如果配置项不存在会发生什么?如果你喜欢使用Yaml替代XML来存储配置项会发生什么呢?
- 其次它使用了模板引擎。这有两个问题:一是类实现的功能比它名称所能表示更多:它不但发送Email,而且使用模板引擎生成了Email的内容;二是它必须深入了解怎样使用模板引擎。如果引擎以后需要修改,你需要重新研究这段代码还是否可行。
So this simple class has two strong dependencies: the means to obtain the configuration and the template engine. Suppose you'd like to use this very class in another project. In order to do that, you'll have to:
- Remember about the configuration keys.
- Bring the NVelocityTemplateEngine class and all the dependencies it might have, with you.
And this is just a small class! It could be worse, and I'm positive you have seen a similar situation. Usually, the fastest solution is to copy the code and change a few things. But c'mon, there must be another way. Fortunately, there is.
所以这个类有两个严重的依赖点:一是配置的获取二是模板引擎。假如你想在另一个项目中重用它,你将不得不:
- 记住配置对应的键值对。
- 附加带上NVelocityTemplateEngine和其它一切依赖的类。
Component and Services
There were people using an interesting buzzword: COP (component oriented programming). I think we already have enough of buzzwords nowadays and we should save space on our brains for more important things. Nevertheless, I'd like to depict the concepts of components.
A component is a small unit of reusable code. It should implement and expose just one service, and do it well. In practical terms, a component is a class that implements a service (interface). The interface is the contract of the service, which creates an abstraction layer so you can replace the service implementation without effort.
To define our email service, we could use the following interface:
许多人正在使用一个有趣的专业术语:COP(面向组件编程)。我认为现在有太多的专业术语,我们不应该花大多脑精在它们上面,而应节省精力去做更多重要的事件。不过,我仍然要介始一下什么是组件。
简单的说组件就是一个可重用的单元。它应当并仅仅实现良好一个服务。实际应用中,组件应是一个实现某个接口服务的类。接口约定了服务:这个抽象层使随意替换服务的实现而对使用接口服务的代码而任何影响。
To define our email service, we could use the following interface:
定义一个Email服务我们可以这样做:
// The contract
public interface IEmailSender
{
void Send(String from, String to, String message)
}
Please note that this contract is valid for any implementation of e-mail sender, for example, using SMTP, IMAP, SendMail and so on.
Now, we can work on one of those implementations, a simple SMTP e-mail sender:
不管你使用SMTP,IMAP等等来实现该接口时都必须遵守这个约定。
我们来看一下用SMTP怎样来实现它:
// The implementation
public class SmtpEmailSender : IEmailSender
{
private String _host;
private int _port;
public SmtpEmailSender(String host, int port)
{
_host = host;
_port = port;
}
public void Send(String from, String to, String message)
{
// Configures the Smtp class and sends the e-mail
}
}
Better, huh? But where's the inversion of control? Before going further, allow me to make things more complex. You have probably noticed that now the email sender is only responsible for sending the email, no template processing. So let's define the contract for a template processor engine:
哈哈,是不是蛮好的?但是不是要讲IOC吗,它在哪呢?在继续之前,允许我将它们变得再复杂点。你可能已经注意到发送邮件的类仅仅负责邮件发送,而没有处理模板。所以我们得继续定义一个模板处理引擎的接口:
public interface ITemplateEngine
{
String Process(String templateName)
}
This is a fairly naive example. Any decent template engine will use a context or something similar. Now we can have a different component which processes the templates and dispatches the emails:
它非常简单。对于任何的模板引擎实现都可能有许多类似的实现。现在就让我们来看看一个不太一样的组件是怎样实现模板处理与分派邮件的:
public interface INewsletterService
{
void Dispatch(String from, String[] targets, String messageTypeName)
}
Let's speculate about the implementation of INewsletterService. It certainly needs to use the IEmailSender service and the ITemplateEngine service without even caring about their implementation. Fair enough, let's code:
让我思考一下怎样实现INewsletterService:它当然必须使用IEmailSender与ItemplateEngine,但不必关心它们是怎样实现的。就如这样:
public class SimpleNewsletterService : INewsletterService
{
private IEmailSender _sender;
private ITemplateEngine _templateEngine;
public SimpleNewsletterService(IEmailSender sender,
ITemplateEngine templateEngine)
{
_sender = sender;
_templateEngine = templateEngine;
}
public void Dispatch(String from, String[] targets, String messageTypeName)
{
String message = _templateEngine.Process(messageTypeName);
foreach(String target in targets)
{
_sender.Send(from, target, message);
}
}
}
As you can see, with small and well defined responsibilities, it's easier to cope with software development. But the problem now is that we have to assemble the application, in other words, we need to connect everything, instantiating and configuring the IEmailSender and the ITemplateEngine, pass them to INewsletterService and God knows what else. You can do it by hand, but believe me, things can get really complicated with a big system.
如你所见,足够小但功能明确的组件对软件开发来说是容易实现的。但现在却有一个问题,那就是必须在应用中绑定它,换句话说,我们需要关心任何一件事,如配置IEmailSender与ITemplateEngine,并将它们传递给INewsletterService,也许还有其它更多的事需做。你可能随手可做好它,但可以确认,在大的系统中这将变得很复杂。
Castle Windsor
Castle Windsor is an inversion of control container. It's built on top of a MicroKernel responsible for inspecting the classes and trying to understand what they need in order to work and then provide the requirements or fail fast if something is wrong.
Castle Windsor是一个IOC容器。它构建于MicroKernel之上,能检测类并了解使用这些类时需要什么参数,然后提供这些参数并且当某些内容有错误时将来及时报告失败。
For our small example, the following code will take care of everything:
下面的代码就关注这些:
IWindsorContainer container = new WindsorContainer();
container.AddComponent( "newsletter", typeof(INewsletterService),
typeof(SimpleNewsletterService) );
container.AddComponent( "smtpemailsender", typeof(IEmailSender),
typeof(SmtpEmailSender) );
container.AddComponent( "templateengine", typeof(ITemplateEngine),
typeof(NVelocityTemplateEngine) );
// Ok, start the show
INewsletterService service = (INewsletterService) container["newsletter"];
service.Dispatch("hammett at gmail dot com", friendsList, "merryxmas");
First, let me explain what happened in this code snippet:
首先,让我来解释一下这个代码片断:
- You registered the service INewsletterService on the container and you also said that the class SimpleNewsletterService contains an implementation of INewsletterService. The first argument is a key that you use to request this service later. You might also request the service by the Type.
- Windsor inspected the implementation you provided and noticed that it requires two other services to work. It checked itself and realized that it doesn't have such services registered, so if somebody requests the INewsletterService service instance, it will throw an exception.
- You registered the service IEmailSender. Windsor notices that the implementation has just one public constructor that receives two arguments (host and port). It's not able to satisfy this configuration as we haven't provided an external configuration. We'll fix that later.
- You registered the service ITemplateEngine. Windsor registers it successfully and shouts to other components in a WaitingDependency state that a component has been registered. The INewsletterService realizes that one of its dependencies is OK to go, but it still is waiting for the other.
1. 首先在容器中注册了INewsletterService服务并告知容器SimpleNewsletterService实现了这个服务。其中这一个参数是一个Key值,你能使用它在以来后请求这个服务。当然你也能用Type来请求服务。
2. Windsor检测实现的服务并发现需要其它两个服务才可工作。它将会自我检测并发现不存在这样的注册服务,那么些时请求生成这样的组件实例将会抛出一个异常。
3. 当在容器中注册IEmailSender时,Windsor将会发现类的构造函数需要两个参数(host与port)。我们已经对采用外部配置文件的解决方式事感到不满意,所以我们等会儿来纠正它。
4. Windsor注册ITemplateEngine成功,将通知其它处于WaitingDependency(即依赖其它服务但却在容器还没有发现该服务的)状态的组件告诉它们一个组件注册了。然后INewsletterService将会发现需要依赖一个组件都OK了,但是它仍然需要等待。
The fact that a class has a non default constructor has a specific meaning for the container. The constructor says to it: "Look, I really need these in order to work". If our implementation were different, the container would have used a different approach. For example:
如果类没有缺省的构造函数,那么对于容器就有特殊的意义:我需要其它东西才能工作。实现方式不一样,容器也就会有不同的处理方式。例如:
public class SmtpEmailSender : IEmailSender
{
private String _host;
private int _port;
public SmtpEmailSender()
{
_host = "mydefaulthost";
_port = 110; // default port
}
public String Host
{
get { return _host; }
set { _host = value; }
}
public int Port
{
get { return _port; }
set { _port = value; }
}
...
}
In this case, you may or may not specify the host and port in an external configuration and the container will be able to use it. Another approach is to expose more than one constructor. The container will try to use the best constructor - meaning the one it can satisfy more arguments.
在这里无论在外部配置中提供host和port也或不提供,这个容器都将能使用它。另一个方法为类提供多个构造函数。而容器将使用最恰当的构造函数。
"But wait! What about the configuration?"
让我们稍停一下,讲讲配置是怎么一回事。
By default, Windsor will try to obtain the component configuration from the XML file associated with the AppDomain. However, you can override it by implementing the interface IConfigurationStore. There's another implementation of IConfigurationStore that reads the configuration from standard XML files, which is good when you have a configuration for testing environment, another for production, and so on.
缺省的情况下,Windsor将尝试从与AppDomain关联的XML文件中获取组件的配置信息。然后你也可以通过实现IConfigurationStore来取代它。当你需要配置测试环境或在其它产品等上使用其它区别于标准的XML配置读取方式可能更好。
Here is the configuration file you must use to run the example:
要运行这个示例,这是必须使用的配置文件:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="castle"
type="Castle.Windsor.Configuration.AppDomain.CastleSectionHandler,
Castle.Windsor" />
</configSections>
<castle>
<components>
<component id="smtpemailsender">
<parameters>
<host>localhost</host>
<port>110</port>
</parameters>
</component>
</components>
</castle>
</configuration>
From experience, I know that sometimes it's dumb to have interfaces for all your components. There are classes that are unlikely to have a different implementation, like Data Access Objects. In this case, you can register classes into the container as follows:
现实中,在所有组件中某些是不必定义接口的。它们看起来有不同的实现方式,比如数据访问对象。事实上,你也能在容器中像下面一样地注册并使用它们:
public class MyComponent
{
private IEmailSender _sender;
public MyComponent()
{
}
public IEmailSender EmailSender
{
get { return _sender; }
set { _sender = value; }
}
}
IWindsorContainer container = new WindsorContainer();
container.AddComponent( "mycomponent", typeof(MyComponent) );
You might want to request a component by the service:
你可以这样使用组件服务:
IEmailSender emailSender = container[ typeof(IEmailSender) ] as IEmailSender;
But please note that you can register more than one implementation for a given service. The default behavior is to return the first implementation registered for the specified service. If you want a specific implementation, then you need to use the key.
值得注意的是可以注册多个服务的实现到容器中。但上面这个方法会缺省的返回你注册的第一个。如果你想使用某个特殊的实现,你需要使用前面提到的Key。
As a matter of fact, the last paragraph leads us to an interesting situation. Suppose you have implemented several IEmailSender but you want that the INewsletterService implementation uses the SMTP version and nothing else. The better way to do it is to use the configuration to specify the exact implementation you want:
事实上,到了最后有一个有趣的情形。假如你已经实现了许多IEmailSender服务,而你仅仅需要使用SMTP版本的服务。那么这时最好的方式就是使用配置来达到你的目的。
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="castle"
type="Castle.Windsor.Configuration.AppDomain.CastleSectionHandler,
Castle.Windsor" />
</configSections>
<castle>
<components>
<component id="newsletter">
<parameters>
<sender>#{smtpEmailSender}</sender>
</parameters>
</component>
</components>
</castle>
</configuration>
The nodes inside 'parameters' must use the name of a property exposed by the implementation or the name of the argument the implementation's constructor uses. Just to refresh your memory:
’parameters’节点内部节点名称必须与实现的公共属性名称或者构造函数中的参数名称相同。
public class SimpleNewsletterService : INewsletterService
{
private IEmailSender _sender;
private ITemplateEngine _templateEngine;
public SimpleNewsletterService(IEmailSender sender, ITemplateEngine templateEngine)
{
_sender = sender;
_templateEngine = templateEngine;
}
...
Lifecycle and Lifestyle
Lifecycle and lifestyle are useful strategies that Apache Avalon have been using for quite some time (since 1999 to be exact). As a former member of Avalon, I like these ideas and decided to provide it too.
Apache Avalon使用Lifecycel and lifestyle作为一个很有用的策略模式已经很长一段时间(准确来说始自1999年)。作为Avalon的成员,我喜欢它并愿愿意提供它。
The lifestyle of a component is related to its instance. It might be singleton (which is the default lifestyle) that means that only one instance will be created for the whole life of the container. Other supported lifestyles are:
Lifestyle与一个组件的实例相关。它可以是:Singleton(缺省的),这意味着容器中总只存在唯一组件实现。其它可能还有:
- Transient: a new instance is created per request.
- PerThread: just one instance exists per thread.
- Custom: you provide the implementation of ILifestyleManager to implement your own semantics, like poolable objects.
- Transient: 每次请求都会产生新实例。
- PerThread: 每个线程一个实例。
- Custom: 通过实现ILifestyleManager使你的实例具有特殊的Lifestyle,比如对象池。
There are two ways to specify the lifestyle of a component: by using attributes or by using the external configuration. In fact, the configuration overrides whatever is defined on the component. The following have the same result:
有两种方来指定组件的Lifestyle:一是使用特性,另一个则是使用外部配置。任何时候如果两者都存在的时候,配置总是会取代直接在组件上定义的特性。下面两个方式不同但结果却一样:
using Castle.Model;
[PerThread]
public class SimpleNewsletterService : INewsletterService
{
private IEmailSender _sender;
private ITemplateEngine _templateEngine;
...
<castle>
<components>
<component id="newsletter" lifestyle="perthread" />
</components>
</castle>
Lifecycle means the sequence of invocation that happens during the creation of your component (before making it available to the external world) and the sequence of invocation during the destruction. Out of box, Windsor only supports the IInitialize and the standard IDisposable interfaces.
Lifecycle是指创建对象与析构对象时发生的一系列的调用。除此之外,Windsor仅仅支持IInitialize与标准的IDisposable接口。
More about Windsor Container
When you add a component to the container, a few things happen. First, the container creates a ComponentModel which holds a lot of meta information about the component. Then it delegates the inspection of the component to the contributors. Each contributor cares about one specific task. For example, there is a contributor that gathers all public constructors and adds to the ComponentModel, another one checks if the implementation implements specific lifecycle interfaces, another one looks for specific attributes of lifestyle, and so on.
往容器中加入一个组件时,首先容器会新建一个ComponentModel对象,它包含该组件的元数据信息。然后它会作为许多Contributor的代理的侦察员。每一个Contributor都有一个特别的任务。如有一个Contributor专门收所有的公共构造函数并将它加入ComponentModel中,还有专门检查是否实现出某种特定的Lifecycle接口的Contributor,还有寻找Lifestyle特性的Contributor等等。
The real fun begins when you start to use this process by adding your own contributor that checks for _anything_ and gives the component a different semantic or role when found that _anything_.
真正有趣的是你可以添加你自已的Contributor来做你想做的任何事情。
There's also an important entity called Handler. The Handler is the keeper and orchestrator of the component. If the minimal set of dependencies for the component is not satisfied, the handler will not allow the component to be used (by your code or by any other component).
这里有一重要的实体调用处理器。这个处理器是组件的守护者与协调员。如果组件的最小依赖关系没有满足,该处理器将不允许使用该组件(通过你的代码或者其它组件)。
Now, you're ready for the most interesting section: extending the container.
好了,我们进入更有趣的部分,那就是对这个容器进行扩展。
Extending the Container
I'm going to present you a simple and a not so simple sample applications. The first one will introduce a different semantic to components that implement the IStartable interface; the other depicts a simple yet functional transaction infrastructure for your Data Access Objects and two ways of using it.
接下来我将展示给你一个简单和一个略微复杂一点的示例应用。简单的示例将通过实现IStartable接口来说明怎样为组件实现特殊的功能;而复杂点的则仍然会讲讲关于DAO对象的事务问题,在这里用了两种不同的方法来达到这个目的。
Basically, to extend the container, you can use a few extension points as you like. For example, you can add an inspector to look for an attribute, or an entry on the component configuration, or whatever you like. You can also subscribe to the events the MicroKernel exposes. You can even replace some parts of the MicroKernel and implement different semantics for your container (for example, to allow null values be passed to components' constructors).
要扩展这个IOC容器,你可以使用示例提到的增加特性的探测或者增加获取组件配置信息的入口,只要你喜欢,你都可以这个扩展点来达到你的目的。你还可以发送事件给MicroKernel。甚至你还可以重定义MicroKernel某些部分,使你的容器具有某些其它的作用。(比如,允许传送一个空值到组件的构造函数)。
When extending the container, you can give the component instance more information, register interceptors (proxies), change the lifestyle, add more lifecycles and so on. At first, let's talk about implementing a new lifecycle step.
通过扩展这个IOC容器,能够给予组件实例更多的信息,并注入透明动态代理,改变它的Lifestyle和增加更多的Lifecycles等等。首先,就让我们来看看怎样实现新的Lifecycel吧。
Startable Lifecycle
Suppose you'd like that every component that implements an IStartable interface be started as soon as possible. By started, I mean:
- Create an instance of it by requesting it.
- Invoke the Start method defined by IStartable, so the component may start its work.
假如每个组件只有有可能都实现了具有Started的IStartable接口。关于Started,我认为包含两个意义:
- 请求叶创建一个组件实例。
- 调用IStartable的Started方法来组件可就可以开始运行。
For a world usage example, imagine a WinForms application so you can register Forms subclasses as components. One component may run the Application.Run( your main form ). Thus, this component is likely to benefit from the new lifecycle. We'll use the components developed previously just to pretend we have some real action going on.
实际使用时,你可能会想到做WinForm应用程序时可以注册将Forms的子类为组件:它会调用Application.Run(mainform)。你应该想到新添加的Lifecycle有用了吧。我们将继续使用前面的示例,假当它真的能完成那些功能。
When you decide to implement extensions like that, you can do it ad-hoc, or you can create a Facility. Facility is an extension unit so you can create a library of facilities and apply them to other applications, thus reusing your work.
如果决定做类似这样的扩展,你可以做一个Facility或者做Ad-hoc、Facility可以帮助你将这些扩展应用到其它的项目中,从而达以重用的目的。
To implement our requirements, we need to:
- Hook into the component registration process so we can inspect if the component being registered implements the IStartable interface.
- Add a new lifecycle step that executes the Start method accordingly.
So, let's work! The first step: create a new Facility:
为了实现这些需求,我们需要:
1.当某个组件实现了IStartable接口并注册到容器能够检测得到,
2.如果组件实现了IStarted就应用自动添加一个新的Lifecycle用来执行Start方法。
Ok,让我开始吧。首先,创建一个新的Facility。
public class StartableFacility : IFacility
{
private IKernel _kernel;
public void Init(IKernel kernel, IConfiguration facilityConfig)
{
_kernel = kernel;
}
public void Terminate()
{
// Nothing to do
}
}
A Facility can have its own configuration which can be useful for settings (like the NHibernate facility that I'll talk about in the next article).
Now, let's add a hook to the registration process:
Facility可以使用自已配置信息是很有用。(就像我下一篇文章中将提到的NHibernate的Facility)。让我们看看怎样添加一个钩子到组件注册的过程中吧:
public class StartableFacility : IFacility
{
private IKernel _kernel;
public void Init(IKernel kernel, IConfiguration facilityConfig)
{
_kernel = kernel;
kernel.ComponentModelBuilder.AddContributor( new StartableInspector() );
}
public void Terminate()
{
// Nothing to do
}
private class StartableInspector : IContributeComponentModelConstruction
{
public void ProcessModel(IKernel kernel, ComponentModel model)
{
bool startable =
typeof(IStartable).IsAssignableFrom(model.Implementation);
model.ExtendedProperties["startable"] = startable;
if (startable)
{
model.LifecycleSteps.Add(
LifecycleStepType.Commission, new StartableConcern() );
}
}
private class StartableConcern : ILifecycleConcern
{
public void Apply(ComponentModel model, object component)
{
(component as IStartable).Start();
}
}
}
}
So we added a contributor that looks for the IStartable interface. If found, we add a new lifecycle step which implements the ILifecycle interface. Simple, huh? But we still need to implement the logic to start the components as soon as possible. Here we go:
这样我就添加了一个可以查询组件是否实现了IStartable接口的Contributor、如果注册到容器的组件实现了它,组件将会添加一个实现了ILifecycle接口的Lifecyle。但是我们仍然需要实现只要可能就启动组件的处理逻辑,让我们继续吧:
public class StartableFacility : IFacility
{
private ArrayList _waitList = new ArrayList();
private IKernel _kernel;
public void Init(IKernel kernel, IConfiguration facilityConfig)
{
_kernel = kernel;
kernel.ComponentModelBuilder.AddContributor( new StartableInspector() );
kernel.ComponentRegistered +=
new ComponentDataDelegate(OnComponentRegistered);
}
...
private void OnComponentRegistered(String key, IHandler handler)
{
bool startable = (bool)
handler.ComponentModel.ExtendedProperties["startable"];
if (startable)
{
if (handler.CurrentState == HandlerState.WaitingDependency)
{
_waitList.Add( handler );
}
else
{
Start( key );
}
}
CheckWaitingList();
}
/// For each new component registered,
/// some components in the WaitingDependency
/// state may have became valid, so we check them
private void CheckWaitingList()
{
IHandler[] handlers = (IHandler[])
_waitList.ToArray( typeof(IHandler) );
foreach(IHandler handler in handlers)
{
if (handler.CurrentState == HandlerState.Valid)
{
Start( handler.ComponentModel.Name );
_waitList.Remove(handler);
}
}
}
/// Request the component instance
private void Start(String key)
{
object instance = _kernel[key];
}
}
Now, if the component that was just registered is startable, we try to start it by requesting it. Note however that sometimes the components are not ready to be started, so we have to keep them on a list and check if they are OK to be started later.
现在当一个Startable组件注册完毕后,容器就会通过请求开始运行该组件。值得注意的是并不是所有组件随时都可以这样,所以需要维护一个列表在以后检查它们运行条件是否已经具备,如果是则开始运行。
If you'd like to see the application running, check the sample code.
如果你想看看这个运行的应用程序,你可以下载示例代码。
A Transaction Framework for database access
In this example, the goal is to simplify the development of Data Access Objects by automatic handling of the connection and transactions. We purse two possible usages:
在接下来的示例中,将提供两种方法使DAO对象自动处理连接与事件,从而使开发更容易。
Through attributes:
一种是通过特性:
[Transactional]
public class BlogDao
{
private IConnectionFactory _connFactory;
public BlogDao(IConnectionFactory connFactory)
{
_connFactory = connFactory;
}
[RequiresTransaction]
public virtual Blog Create(Blog blog)
{
using(IDbConnection conn = _connFactory.CreateConnection())
{
IDbCommand command = conn.CreateCommand();
// Not the best way, but the simplest
command.CommandText =
String.Format("INSERT INTO blog (name, blog.desc) " +
"values ('{0}', '{1}');select @@identity",
blog.Name, blog.Description);
object result = command.ExecuteScalar();
blog.Id = Convert.ToInt32(result);
}
return blog;
}
[RequiresTransaction]
public virtual void Delete(String name)
{
// We pretend to delete the blog here
}
...
}
And through external configuration:
另外一种则是使用外部配置:
<component id="postDao" transactional="true">
<transaction>
<method>Create</method>
<method>Update</method>
</transaction>
</component>
Might sound complex, but it's not. We need to implement an IConnectionFactory which is aware of transactions. We also need to implement an interceptor that begins/commits or rolls-back transactions.
听起来也许很复杂,其实不然。我们只需要实现IConnectionFactory,它知道事务怎样做。同时我们也需要实现一个具有开始提交或回滚事务的拦截器。
It all starts with a Facility again:
我们仍然从创建一个Facility开始:
public class TransactionFacility : IFacility
{
TransactionConfigHolder _transactionConfigHolder;
public void Init(IKernel kernel, IConfiguration facilityConfig)
{
kernel.AddComponent( "transactionmanager",
typeof(ITransactionManager), typeof(DefaultTransactionManager) );
kernel.AddComponent( "transaction.interceptor",
typeof(TransactionInterceptor) );
kernel.AddComponent( "transaction.configholder",
typeof(TransactionConfigHolder) );
_transactionConfigHolder =
kernel[ typeof(TransactionConfigHolder) ]
as TransactionConfigHolder;
kernel.ComponentModelCreated += new
ComponentModelDelegate(OnModelCreated);
}
public void Terminate()
{
}
private void OnModelCreated(ComponentModel model)
{
if (IsTransactional(model))
{
TransactionConfig config = CreateTransactionConfig(model);
_transactionConfigHolder.Register(model.Implementation, config);
model.Interceptors.Add(
new InterceptorReference(typeof(TransactionInterceptor)) );
}
}
...
}
This Facility registers the components it needs. If the component is transactional, we associate an interceptor with it. Please note that only virtual methods can be intercepted (in future releases, we'd be able to proxy everything through interfaces).
这个Facility添加注册了所有需要的组件。如果组件需要处理事务,它将关联一个拦截器给它。不过请注意目前仅能拦截虚函数的调用(将业发布的版本也许可以通过接口代理一切)。
The interceptor code follows:
这个拦截器的代码是这样的:
public class TransactionInterceptor : IMethodInterceptor
{
private ITransactionManager _transactionManager;
private TransactionConfigHolder _transactionConfHolder;
public TransactionInterceptor(ITransactionManager transactionManager,
TransactionConfigHolder transactionConfHolder)
{
_transactionManager = transactionManager;
_transactionConfHolder = transactionConfHolder;
}
public object Intercept(IMethodInvocation invocation, params object[] args)
{
if (_transactionManager.CurrentTransaction != null)
{
// No support for nested transactions
// is necessary
return invocation.Proceed(args);
}
TransactionConfig config =
_transactionConfHolder.GetConfig(
invocation.Method.DeclaringType );
if (config != null && config.IsMethodTransactional( invocation.Method ))
{
ITransaction transaction =
_transactionManager.CreateTransaction();
object value = null;
try
{
value = invocation.Proceed(args);
transaction.Commit();
}
catch(Exception ex)
{
transaction.Rollback();
throw ex;
}
finally
{
_transactionManager.Release(transaction);
}
return value;
}
else
{
return invocation.Proceed(args);
}
}
}
The rest of the implementation is pretty ADO.NET specific, and I encourage you to see the sample attached to this article. Follows some screenshots of the sample implementation:
剩下的事情就是使用Ado.net来完成,我建议你看看附件中代码。下面是这个示例运行时的快照:
Stepping back: Castle
Castle project's goal is to offer a set of tools and an inversion of control container. The container is the rendezvous of all the tools, but this is a topic for the next article.
Castle项目的目标就是提供一个IOC容器,并提供相关的一系列工具。容器是所有工具的集合点,但这是下一篇文章的主题。
A few values have driven the development of the Windsor (and MicroKernel) container:
使用Windsor(与Microkernel)将使开发获益:
- Extensibility: the container should not stop the programmer from implementing his ideas, so it must be very easy to extend it.
- Orthogonality: extending the container should not impact on other extensions (as you saw).
- Reversibility: the container should be able to run code that does not rely on the container API, so you can reuse your components even on other applications without using an inversion of control container.
- 可扩展性:这个容器没有阻断程序员的思想,你可以非常容易的去扩展它;
- 独立性:扩展容器不会影响到其它的扩展(正如你所见到的);
- Reversibility: 组件运行应该不依赖容器的API,这样在不使用IOC容器的应用时你也能重用你的组件。
We also don't want to be XML or configuration driven as the component holds enough information to assemble itself.
我们也不需要使用XML或者其它形式的配置来保存能够驱动组件的足够信息。
Conclusion
In the next article, I'll talk about the NHibernate and the Prevalence facilities. I'll also explain the integration with Web and the Castle on Rails MVC framework inspired by Ruby on Rails.
下一篇文章中我将谈到NHibernate与Prevalence的Facility,同时也将介绍怎样集成Web与从Ruby on Rails得到灵感而开发的Castle on Rail这个MVC框架进行Web开发。
The Castle project still is on the alpha stage, but the public API is very unlikely to change. There is a lot of work to do, so if you enjoyed the reading, consider joining the team and help us develop tools, facilities and extensions so your next enterprise project can be done in half the time, hopefully!
Castle项目仍然在alpha阶段,但是公开的API很少改变。这儿有大量工作需要去做,如果你喜欢请考虑加入我们的队伍,来与你们一道开发Tools,Facilities与Extensions。这样你的下一个项目也许能节省一半的时间。
Please visit Castle project site for more information.
如果你想了解得更多,请访问Castle project站点。
History
- 26-Dec-2004 - Initial version.
- 29-Dec-2004 - Minor corrections on the text and on the sample.
About Hamilton Verissimo
Hamilton Verissimo has been working with software development for 9 years. He's involved with a lot of open source projects to fill his spare time