Non invasive API - Now with an IoC container
I talked about ways to avoid invasive API design, and a lot of people asked about how to handle this with a container. First, I want to make it clear that the previous example assumed that you can't rely on a container, so it used Poor Man IoC to do that. Now that we can assume that there is a container, this is another matter entirely.
The API design now shifts to allow me to select the proper implementation from the container automatically. This generally ends up being either as a naming convention on top of a fixed interface, or as a generic interface with a given type as a parameter. The decision depends on what you are doing, basically, and the capabilities of your tools.
For myself, I strongly favor the generic interface approach, which would give us the following syntax:
public interface IMessageHandler<TMsg> where TMsg : IMessage { void Handle(TMsg msg); } public class EndPoint { public void HandleMsg(IMessage msg) { // this should be cached and turned into a delegate // not reflection call GetGenericHandleMsgMethod().MakeGenericMethod(msg.GetType()) .Invoke(this, new object[]{msg}); } public void HandleMsg<TMsg>(TMsg msg) where TMsg : IMessage { IoC.Resolve<IMessageHandler<TMsg>().Handle(msg); } }
There is some ugliness in invoking the method with the generic parameter, but this allows you to handle a wide variety of cases very easily.
Let us take another example, this time it is from the Prism.Services.RegionManagerService:
public void SetRegion(DependencyObject containerElement, string regionName) { IRegion region = null; if (containerElement is Panel) { region = new PanelRegion((Panel)containerElement); } else if (containerElement is ItemsControl) { region = new ItemsControlRegion((ItemsControl)containerElement); } if (region != null) _regions.Add(regionName, region); }
I don't really like this code, let us see what happens when we introduce the idea of the container as a deeply rooted concept:
// only use for selection public interface IRegion<T> : IRegion {} public void SetRegion(DependencyObject containerElement, string regionName) { IRegion region = (IRegion) IoC.TryResolve(typeof(IRegion<>).MakeGenericType(containerElement.GetType())); if (region != null) _regions.Add(regionName, region); }
Now the code is much clearer, we can extend it from the outside, without modifying anything when we add a new region type.
Comments
Prism authors are IoC shy.
Consider the following from Bootstrapper.cs
IModuleInitializer newsModule = new NewsModuleInitializercontainer);
IModuleInitializer watchListModule = new WatchListModuleInitializercontainer);
IModuleInitializer marketModule = new MarketModuleInitializercontainer);
IModuleInitializer positionModule = new PositionModuleInitializercontainer);
Provided that container already registered in itsef, they still cannot allow it to instantiate modules.
IModuleInitializer newsModule = new NewsModuleInitializer(container);
IModuleInitializer watchListModule = new WatchListModuleInitializer(container);
IModuleInitializer marketModule = new MarketModuleInitializer(container);
IModuleInitializer positionModule = new PositionModuleInitializer(container);
Have no idea what happened with open parnthesis
I believe the reason for this is that Prism is trying to support multiple IoC containers, so they need to allow for different initialization steps based on which container is in use.
Whats funny is that they add this feature as a reaction to the backlash against the original ObjectBuilder/CAB. And of course the primary reason for that was due to its unnecessarily complex and unwieldy implementation, which was mostly caused by offering too many points of extensibility ; )
A generic interface with an interface restriction on the generic argument... that's really redundant. The thing is: using interfaces you already use generic programming.
So your generic interface can be written as:
public interface IMessageHandler
{
}
public class EndPoint
{
}
Same code, same functionality, only now no generics. Interfaces shouldn't have a generic argument, interfaces by themselves are already generic: use classes with generic arguments and interfaces without generic arguments to access them in situations where the generic argument isn't known:
a List<T> has IList, so in code where you don't know 'T' at compile time, you can access it via IList.
Frans,
How would you select the appropriate IMessageHandler for CreateNewUserMessage ?
My way it would be just defining:
public class CreateNewUserHandler : IMessageHandler<CreateNewUserMessage>
{
... code to handle it
}
Your way it would be...
simply without the generic argument, as it's unnecessary: the signatures in the interface are restricted to the IMessage interface. Implementing the interface forces you to work with what IMessage offers, that's the type you've to work with to make it compile.
Any 'Handle' implementation can only work with IMessage. As I said: interfaces already offer a way to implement generic code: in 'Handle' you work with the IMessage typed parameter 'msg', and that's the only type you can work with anyway, as it's restricted by the where. But because interfaces allow you to pass in instances of different classes as long as they implement 'IMessage', you can write a single interface and single implementation based on the interface, exactly what you'd do with generics, however now without any generic type used :)
Though I think you need the exact type for the IoC resolver, so for that purpose the generic argument is perhaps required, but not for the usage of the interface at all.
So indeed, if you need it for the lookup table inside the IoC container, you need your interface approach. However the generics are not necessary when using the interface.
Frans,
Well, it is nice that I don't need to down cast it from IMessage, and CreateNewUserHandler has no idea how to handle ResetPasswordMessage, so IMessageHandler can't handle any IMessage.
In this case, we use generics both as lookup key and as a way to separate handling.
when we don't care about the separate handling, we can do that using a generic interface for easy lookup and non generic base interface that we can use for it.
You can check the IRegion sample for the details.
@Steve
"...Prism is trying to support multiple IoC containers"
Maybe so, but interface that supposed to reference multiple containers called IUnityContainer so Windsor and Autofac adapters will need to implement IUnityContainer (and finaly unite!!)
@Alex et al
Good feedback. It's not that we are IOC shy, or even about supporting multiple containers (though we are looking to support multiple containers)
We are developing in an interative fashion in which we continually refactor. In this case there were no additional dependencies (other than the container) that the modules needed to be injected with so by the YAGNI rule, we didn't resolve off of the container. If there had been additional dependencies we would of. It's very simply to change the new to a container.Resolve<T>.
That being said, we had already talked about it and decided that it was extremely likely that over time the modules would needed dependencies and so we will be resolving modules off of the container and as such we should. If you look at our original spikes we did that.
On to the topic of supporting multiple containers. You'll notice in the RI everything is using Unity. So where is the multiple support?
Well for one thing we're bringing in the "framework" through composition, meaning you creplace the calls to Unity to use Windsor, Structure Map for injecting.
Second, we have created an IPrismContainer which is essentially an adapter interface. That interface is only for the "framework" so that IF we need to do any imperative resolving within core service, we can. In general we don't need to do this as we can pass in the dependencies through the constructor. But, in some cases such as if we follow Oren's example, we may need to.
Thanks
@Alex
We are not requiring you to use IUnityContainer to use Prism. IPrismContainer does not depend on Unity.
@Glenn
"We are not requiring you to use IUnityContainer "
My bad. I only saw IPrismContainer in the UnityPrismContainer and its tests.
Anyway, TryResolve and batch registration methods (though those can be handled by extension methods) will definitely make IPrismContainer interface much more useful.
@Alex we debated about this. If the container doesn't support TryResolve functionality, then the implementation of the method might be ugly where you end up with a try {} catch in the method. Some containers support ResolveAll<T> functionality which will return whatever instances are found, and TryResolve could wrap that and return the first instance.
Would you want to see a TryResolve even if the implementation might end up being a hack?
Glenn,
Yes, I would.
All containers are fairly responsive in this regard, and there isn't an issue with making this
Accepting this limitation would significantly reduce the power of the framework
Comment preview