Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
207 views
in Technique[技术] by (71.8m points)

factory - Castle Windsor: A better way to implement 2 levels of (nested) factories?

We have a pattern we've used several times, whereby we implement handlers and factories in separate Dlls. We configure exe's at runtime saying what dlls are loaded, and therefore what handlers are available to the app.

We do this because we have custom handling for some customers, also it allows great flexibility because we can quickly develop new handlers in isolation, and test and deploy them with confidence that we haven't even touched any other parts of a running application. We can also patch handlers by simply dropping in a single replacement dll, we have customers with strict change management procedures and they adore this.

To do this the pattern relies on two levels of factories, specific factories that implement specific handlers, and an overarching factory (which we call a Provider). The Provider chooses which handler factory to use to create a handler.

The question: Does Windsor contain something that would simplify this process for us?
Specifically I'm looking for something that could omit the Handler factory objects, it feels like something it should be able to do.
I've read up on the Typed Factory Facility and the UsingFactory & UsingFactoryMethod methods, but I can't see how they'd be any help here.
That said I often find the Castle Windsor documentation obtuse so I could be missing something obvious Or is there just a better way of getting the same end goal that I haven't considered.

Here's some code to illustrate, first message, handler and factory interfaces

public interface IMessage
{
    string MessageType { get; }
}
public interface IMessageHandler
{
    void Process(IMessage message);
}
public interface IMessageHandlerFactory
{
    bool CanProcessType(string type);
    IMessageHandler Create();
}

In a second DLL we implement a handler and factory for Type1

public class Type1MessageHandler
    : IMessageHandler
{
    public void Process(IMessage message) { }
}
public class Type1MessageHandlerFactory
    : IMessageHandlerFactory
{
    public bool CanProcessType(string type)
    {
        return type == "Type1";
    }
    public IMessageHandler Create()
    {
        return new Type1MessageHandler();
    }
}

In a third Dll we implement a handler and factory for Type2

public class Type2MessageHandler
    : IMessageHandler
{
    public void Process(IMessage message) { }
}
public class Type2MessageHandlerFactory
    : IMessageHandlerFactory
{
    public bool CanProcessType(string type)
    {
        return type == "Type2";
    }
    public IMessageHandler Create()
    {
        return new Type2MessageHandler();
    }
}

In a windows service we implement the provider

public interface IMessageHandlerProvider
{
    IMessageHandler Create(string messageType);
}
public class MessageHandlerProvider
    : IMessageHandlerProvider
{
    IEnumerable<IMessageHandlerFactory> factories;
    public MessageHandlerProvider(IWindsorContainer wc)
    {
        factories = wc.ResolveAll<IMessageHandlerFactory>();
    }
    public IMessageHandler Create(string messageType)
    {
        foreach (var factory in factories)
            if (factory.CanProcessType(messageType))
                return factory.Create();
        throw new UnableToFindMessageHandlerFactoryForType(messageType);
    }
}

The service that actually needs the handlers only uses the Provider

public class MessageService
{
    public MessageService(IMessageHandlerProvider handlerProvider) {}
}
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

What you are asking is indeed possible in Windsor with typed factories; instead of resolving all the factories in your provider and then looking for the ones that can process the message, you could ask Windsor for the handler that is linked to the message type and just use it. You don't really need the second level factory (IMessageHandlerFactory), because the handler can tell what message it will link to.

Here is a nice resource for this architecture (you've probably read this one already) which I'll summarize very quickly.

Given your interfaces, you start by registering all your handlers

container.Register(Classes.FromAssemblyInThisApplication()
    .BasedOn<IMessageHandler>()
    .WithServiceAllInterfaces());

Ok, now let's tell Windsor we want a factory that will return a IMessageHandler. What is nice is that we don't actually have to code anything for the factory.

container.AddFacility<TypedFactoryFacility>();
container.Register(Component.For<IMessageHandlerProvider>().AsFactory());

Now we can start using the factory

var provider = container.Resolve<IMessageHandlerProvider>();
var msg = new Type2Message();
var msgHandler = provider.Create(msg.MessageType);

The problem is that since there is no link between our message handlers and the string we pass to the factory, Windsor returns the first registered instance of a IMessageHandler it finds. In order to create this link we can name each message handler after the message type it is supposed to handle.

You can do it in a variety of ways, but I like to create a convention where a message handler type tells what messages it can handle:

container.Register(Classes.FromAssemblyInThisApplication()
    .BasedOn<IMessageHandler>()
    .WithServiceAllInterfaces().Configure(c => {
        c.Named(c.Implementation.Name.Replace("MessageHandler", string.Empty));
    }));

Now you need to tell your factory that the message type must be used as the name of the handler you want to resolve. To do that, it is possible to use a class inheriting the DefaulTypedFactoryComponentSelector. We just override the way component names are determined and return the message type we are receiving:

public class MessageHandlerSelector : DefaultTypedFactoryComponentSelector
{
    protected override string GetComponentName(MethodInfo method, object[] arguments)
    {
        return arguments[0].ToString();
    }
}

Now we can plug this selector in the factory

container.AddFacility<TypedFactoryFacility>();
container.Register(Component.For<IMessageHandlerProvider>()
     .AsFactory(c =>c.SelectedWith(new MessageHandlerSelector())));

Here is the full code to handle any messages:

var container = new WindsorContainer();
container.Register(Classes.FromAssemblyInThisApplication()
    .BasedOn<IMessageHandler>()
    .WithServiceAllInterfaces().Configure(c => {
        c.Named(c.Implementation.Name.Replace("MessageHandler", string.Empty));
}));

container.AddFacility<TypedFactoryFacility>();
container.Register(Component.For<IMessageHandlerProvider>().AsFactory(c =>c.SelectedWith(new MessageHandlerSelector())));

var provider = container.Resolve<IMessageHandlerProvider>();
var msg = new Type2Message();
var msgHandler = provider.Create(msg.MessageType);
msgHandler.Process(msg);

Here are some points I would like to underline:

  • as you guessed, you don't need the two factories: one is enough
  • the naming convention for the message handlers is not set in stone, and you could decide to have another mechanism in place to override the convention
  • I am not talking about releasing the components, but the links contain some info about it which you should look into
  • I didn't handle the case where no handler can be found, but Castle will throw by itself when it cannot resolve the handler with a ComponentNotFoundException
  • The system could perhaps be more robust if the handlers were explicit about the message types they handle. For example changing the interface to IHandlerOf<T> with T being a message type implementation.

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...