9.78 Typed Factory Facility - interface-based factories

优质
小牛编辑
117浏览
2023-12-01

Interface-based typed factories are extremely useful in following situation:

Object needs dependency provided after it was created:

An example of a component like that would be a message dispatcher which waits for a message to arrive, then when that happens it pulls appropriate message handler and delegates the handling of the message to it. Using a typed factory the dispatcher can easily pull message handlers from the container, without having to explicitly use it. This gives you full power of the container and keeps your code expressive and free from service location.

Registering factories

Once you have created a factory interface:

public interface IDummyComponentFactory
{
     IDummyComponent Create();
     void Release(IDummyComponent dummyComponent);
}

you can then register this with the Windsor framework using the extension method AsFactory:

kernel.AddFacility<TypedFactoryFacility>();
kernel.Register(
    Component.For<IDummyComponentFactory>()
        .AsFactory()
);

(This requires the namespace Castle.Facilities.TypedFactory)

:information_source: That's right - no implementation: Notice we specify an interface and don't provide any implementation. By using AsFactory extension method we're telling Windsor to provide its own implementation that adheres to the default convention for typed factories (discussed below)

Factory requirements

Not just any type may be used as typed factory. The following requirements must be met:

  • The type must be an interface
  • None of its methods can have out parameters.

Using typed factories

You use of typed factories much like you would use any other service. There are three kinds of methods it treats specially:

  • methods with no return value (having 'void' as return type)
  • methods with return value (having something different than 'void' as return type)
  • Dispose method (if typed factory implements IDisposable)

:warning: Lifetime and releasing components: Remember that by default all components in Windsor are singletons and that using default release policy container keeps reference to all components, even transient ones. That's why it's important to release components via typed factory. Also pay attention to lifetime you assign to components resolved via the facility.

We will now go over them in turn.

Resolving methods

Methods with non-void return type are used for supplying components to the caller. Say we register a component and a factory that will be used to create it.

kernel.Register(
    Component.For<IDummyComponent>()
        .ImplementedBy<Component2>()
        .Named("SecondComponent")
        .LifeStyle.Transient,
    Component.For<IDummyComponentFactory>()
        .AsFactory());

now we can resolve the factory:

var factory = kernel.Resolve<IDummyComponentFactory>();

and someplace else in the code we can use the factory to give us the component we need

var component = factory.GetSecondComponent();

Resolving with arguments

You can also use methods that take parameters from the caller to resolve components. The argument you pass in, will be passed on to the container's resolution pipeline.

kernel.Register(
    Component.For<ICalendarFactory>().AsFactory(),
    Component.For<ICalendar>().ImplementedBy<Calendar>().LifeStyle.Transient
);

Now you can resolve the factory

var calendarFactory = kernel.Resolve<ICalendarFactory>();

And use it passing some arguments in.

var today = DateTime.Today;
var calendar = calendarFactory.CreateCalendar(today);
Debug.Assert(calendar.Today == today);

:information_source: How arguments are bound: Specifics about how method being invoked and its arguments are bound to resolved component are determined by an implementation of ITypedFactoryComponentSelector interface. By default the facility uses DefaultTypedFactoryComponentSelector (discussed below) but you can supply your own to customize its behavior.

Releasing methods

Windsor always tracks disposable non-singleton components resolved via typed factory. That means that if you resolve a component from the container you should also release it as soon as you're done using it, to allow Garbage Collector to reclaim memory and resources occupied by it. How do you do it when you're using Typed Factory? With Releasing methods.

Releasing methods are counterparts to Resolving methods. They release components resolved by the latter. Any "void method" other than Dispose is a releasing method (actually Dispose is a releasing method as well, just a very special one). It tries to release all objects passed to them as parameters.

Releasing example

Given you resolved a component from a typed factory:

var component = factory.Create();

You can then pass it as an argument to any of factory's void methods to have the factory release the component from the container

factory.Destroy(component);

You can also pass more components at once, and the facility will release them in turn.

:information_source: Releasing the factory releases all components: Typed factory (both interface and delegate-based) owns the components you resolve through it. That means that when you release the factory, all the components you resolved from the factory will be released as well.

Dispose

When your typed factory interface implements IDisposable it gains a powerful ability - disposing the factory releases all components created via the typed factory

var factory = kernel.Resolve<IDisposableFactory>();
var component = factory.Create();
Debug.Assert(component.Disposed == false);

Now you can dispose the factory, and all non-singleton components will be released as well.

factory.Dispose();
Debug.Assert(component.Disposed == true);

Mapping calls to typed factory to kernel's arguments

Typed Factory Facility uses implementation of ITypedFactoryComponentSelector interface to decide how information it received from the factory (factory Resolving method and its arguments) should be mapped to information forwaded to the container to perform actual resolution of the component. The facility comes with one implementation: DefaultTypedFactoryComponentSelector but if you need custom behavior you can supply your own.

DefaultTypedFactoryComponentSelector

DefaultTypedFactoryComponentSelector obeys few conventions:

'Collection' methods resolve multiple components

When you have a factory method with one of the following return types:

  • IFoo[][]
  • IEnumerable<IFoo>
  • ICollection<IFoo>
  • IList<IFoo>

The factory will recognize that you want a collection, not a single component and it will return collection of all components for service IFoo rather than single component.

For example, given the factory interface:

public interface IMessageHandlersFactory
{
    IMessageHandler[] GetAllHandlersForMessage(IMessage message);
}

calling the method:

var handlers = factory.GetAllHandlersForMessage(new HelloWorldMessage());

is direct equivalent of calling:

var handlers = container.ResolveAll<IMessageHandler>(new { message = new HelloWorldMessage()});

'Get' methods lookup components by name

When you have a factory method named GetSomething, selector will ask the container for a service named Something.

kernel.Register(
    Component.For<IDummyComponent>()
        .ImplementedBy<Component2>()
        .Named("SecondComponent")
        .LifeStyle.Transient
);

Now if we have a method called GetSecondComponent:

var component = factory.GetSecondComponent();

Call to the factory's method is direct equivalent of calling:

var component = kernel.Resolve<IDummyComponent>("SecondComponent");

non-'Get' methods lookup by type

For all other methods, the return type of the method is used as type of component to look-up, so call to factory method:

var component = factory.CreateSecondComponent();

is direct equivalent of calling: (assuiming IDummyComponent is the return type of the method)

var component = kernel.Resolve<IDummyComponent>();

Method parameters are forwarded to the caller by name

When factory method has parameters, their names and values are forwarded to the kernel, so call to the following typed factory method:

IComponent CreateComponent(string componentName, int someParameter);
var component = factory.CreateComponent("foo", 3);

is direct equivalent of calling:

var component = kernel.Resolve<IComponent>(new Dictionary<string, object>`"componentName", "foo"}, {"someParameter", 3`);

Custom ITypedFactoryComponentSelectors

:information_source: Utilize DefaultTypedFactoryComponentSelector: When creating your custom ITypedFactoryComponentSelector consider inheriting DefaultTypedFactoryComponentSelector instead of implementing the interface directly. DefaultTypedFactoryComponentSelector is designed with extensibility in mind, so chances are, overriding single virtual method with few lines of code will get you what you need.

This custom implementation resolve component by id when a method with the following signature is called on the factory:

factory.GetById(string)

Here is the implementation:

public class CustomTypedFactoryComponentSelector : DefaultTypedFactoryComponentSelector
{
    protected override string GetComponentName(MethodInfo method, object[] arguments)
    {
        if (method.Name == "GetById" && arguments.Length == 1 && arguments[0] is string)
        {
            return (string)arguments[0];
        }
        return base.GetComponentName(method, arguments);
    }
}

And the configuration:

var ioc = new WindsorContainer();
ioc.AddFacility<TypedFactoryFacility>();
ioc.Register(Component.For<ITypedFactoryComponentSelector>().ImplementedBy<CustomTypedFactoryComponentSelector>());

Registering factories with custom ITypedFactoryComponentSelector

The facility will use default ITypedFactoryComponentSelector unless you explicitly override it for your factories (on a one-by-one basis).

You can specify the selector explicitly in three ways:

Specifying custom selector as instance

If you don't want to register the selector as a service in your container, you can explicitly provide an instace.

container.Register(
     Component.For<DummyComponentFactory>().AsFactory(c => c.SelectedWith(new MyCustomSelector()))
);

This is useful in one-case scenario where you want to use a non-default selector for a single factory among many.

Specifying custom selector by type

When your selector is registered as a service in the container, you can specify it by type

container.Register(
      Component.For<DummyComponentFactory>().AsFactory(c => c.SelectedWith<MyComponentSelector>()),
      Component.For<MyComponentSelector, ITypedFactoryComponentSelector>()
);

Specifying custom selector by name

When your selector is registered as a service in the container, you can specify it by name as well.

container.Register(
    Component.For<DummyComponentFactory>().AsFactory(c => c.SelectedWith("factoryTwo")),
    Component.For<ITypedFactoryComponentSelector>().ImplementedBy<MyComponentSelector>().Named("factoryOne")
);

XML configuration

In addition to code the facility exposes also XML configuration.

<configuration>
   <facilities>
      <facility
         id="typedFactory"
         type="Castle.Facilities.TypedFactory.TypedFactoryFacility, Castle.Windsor">
         <factories>
            <factory id="handlerFactory"
               interface="Acme.Crm.IHandlerFactory, Acme.Crm" selector="${mySelectorComponent}" />
            <factory id="repositoryFactory"
               interface="Acme.Crm.IRepositoryFactory, Acme.Crm" />
         </factories>
      </facility>
   </facilities>
</configuration>

:information_source: Configuration goes first: When using XML configuration the configuration must be installed before you add the facility to the container. Otherwise the facility will not read the configuration.

See also