Reputation: 2972
I'm doing some tests to dependency injection + mvc and I have a question.
Inside a service, how may I create an object (trough interface) which will be the result of the method?
I've discovered that an abstract factory may resolve this but some people say that it's an anti-pattern and it's a service-locator.
I've done something like this:
public class ObjectFactory : IFactory
{
readonly Container container;
public ObjectFactory(Container container)
{
this.container = container;
}
public T Create<T>()
where T : class
{
return (T)this.container.GetInstance<T>();
}
}
then I use it like this:
public class CacheService : ICacheService
{
readonly IFactory factory;
public CacheService(IFactory factory)
{
this.factory = factory;
}
private void Insert(string name, object value)
{
if (this.CacheAvailable())
{
Remove(name);
// This line where I ask for the container to resolve this interface
ICacheItemWrapper item = factory.Create<ICacheItemWrapper>();
item.Value = value;
HttpRuntime.Cache.Insert(name, item, null, System.Web.Caching.Cache.NoAbsoluteExpiration, System.Web.Caching.Cache.NoSlidingExpiration);
}
}
/* HIDDEN CODE */
}
Is this still an anti-pattern?
What changes should I do?
It feels wrong to do it like this...
Upvotes: 3
Views: 3751
Reputation: 172646
The solution is much simpler than you would expect.
Since the factory you are injecting contains a Create<T>()
method that is unconstraint in the number of types that can be requested, this is an implementation of the Service Locator anti-pattern.
Furthermore, the only service that your CacheService
component really needs is the ICacheItemWrapper
. The IFactory
only adds an extra layer of indirection that increases complexity and complicates testing.
Instead you should inject ICacheItemWrapper
directly into the constructor of CacheService
:
public CacheService(ICacheItemWrapper wrapper) { ... }
You might think this answer is too simplistic, since this would obvously lead to problems in case your CacheService
should outlive the ICacheItemWrapper
. For instance, in case the CacheService
is registered as Singleton
, while the ICacheItemWrapper
is Scoped
, injecting the ICacheItemWrapper
into the CacheService
will lead to a Captive Dependency.
When this happens, you might be tempted to change the ICacheItemWrapper
dependency to a Func<ICacheItemWrapper>
but doing so can cause sweeping changes throughout your application. Furthermore, a Func<T>
dependency is a leaky abstraction, since now the consumer of that dependency now has knowledge about the 'volatiliness' of that dependency. This should be of no concern to its consumers. Injecting a Func<T>
also complicates the consumers and their tests that now have to deal with a delegate returning the abstraction instead of simply having to deal with the abstraction.
In the case of a Captive Dependency, the solution is to wrap the short-lived component into a proxy class that creates the component on depend. This way consumers are unaffected to this change. Example:
public class CacheItemWrapperProxy : ICacheItemWrapper
{
private readonly Func<ICacheItemWrapper> wrapperProvider;
public CacheItemWrapperProxy(Func<ICacheItemWrapper> wrapperProvider) {
this.wrapperProvider = wrapperProvider;
}
// ICacheItemWrapper method(s)
public object GetItem(string key) => this.wrapperProvider().GetItem(key);
}
The CacheItemWrapperProxy
implements ICacheItemWrapper
and this allows it to be injected into its consumers, without the consumers having to change. Do note that the CacheItemWrapperProxy
itself does depend on Func<T>
, but the use of this Func<T>
isolated and the CacheItemWrapperProxy
can be located inside the application's Composition Root. Again, the rest of the application will be unaffected.
This is how you register this in Simple Injector:
container.Register<ICacheItemWrapper, CacheItemWrapperImpl>(Lifestyle.Scoped);
container.RegisterDecorator<ICacheItemWrapper, CacheItemWrapperProxy>(Lifestyle.Singleton);
container.Register<ICacheService, CacheService>(Lifestyle.Singleton);
Do note that Simple Injector has no out-of-the-box support for injecting Func<T>
dependencies. This is deliberate, since -as explained above- your application components should not depend on Func<T>
. The exception to the rule is Simple Injector's decorator registration facility. The RegisterDecorator
method does have native support for handling Func<T>
dependencies, in the sole case that this T
is the decorated type (in your case ICacheItemWrapper
).
Long story short, always use constructor injection, and prevent injecting Func<T>
dependencies into your application components.
Upvotes: 1
Reputation: 887433
Yes; you're wrapping the Service Locator anti-pattern, but it's still an antipattern.
Instead, you should make your constructor accept a Func<ICacheItemWrapper>
(most IoC containers support this).
This way, your ctor still lists its exact requirements, and it's still easy to create an instance of your class directly without IoC.
Upvotes: 1