I had something of a convenient service locator anti-pattern in a previous game project. I'd like to replace this with dependency injection. autofac looks like the most likely DI container for me as it seems to have relevant features - but I can't figure out how to achieve what I'm looking for.
Rather than a single service locator, I had a service locator which could delegate to its parent (in effect providing "scoped" services):
class ServiceLocator {
ServiceLocator _parent;
Dictionary<Type, object> _registered = new Dictionary<Type, object>();
public ServiceLocator(ServiceLocator parent = null) {
_parent = parent;
}
public void Register<T>(T service) {
_registered.Add(typeof(T), service);
}
public T Get<T>() {
object service;
if (_registered.TryGetValue(typeof(T), out service)) {
return (T)service;
}
return _parent.Get<T>();
}
}
Simplifying for clarity, the game consisted of a tree of Component-derived classes:
abstract class Component {
protected ServiceLocator _ownServices;
protected List<Component> _components = new List<Component>();
...
public Component(ServiceLocator parentServices) {
_ownServices = new ServiceLocator(parentServices);
}
...
}
So I could (and did) build tree structures like:
Game
- Audio : IAudioService
- TitleScreen : Screen
- GameplayScreen : Screen
- ShootingComponent : IShootingService
- NavigationComponent : INavigationService
|- AIComponent (uses IAudioService and IShootingService and INavigationService)
And each component could simply call the ServiceLocator with which it's constructed to find all the services it needs.
Benefits:
Components don't have to care who implements the services they use or where those services live; so long as those services' lifetimes are equal to or greater than their own.
Multiple components can share the same service, but that service can exist only as long as it needs to. In particular, we can Dispose() a whole portion of the hierarchy when the player quits a level, which is far easier than having components rebuild complex data structures to adjust to the idea that they're now in a completely new level.
Drawbacks:
As Mark Seeman points out, Service Locator is an Anti-Pattern.
Some components would instantiate service providers purely because I (the programmer) know that nested components need that service, or I (the programmer) know that the game has to have e.g. AI running in the game world, not because the instantiator requires that service per se.
In the spirit of DI, I would like to remove all knowledge of "service locators" and "scopes" from Components. So they would receive (via DI) constructor parameters for each service they consume. To keep this knowledge out of the components, the composition root will have to specify, for each component:
I want to write the intuitive:
class AIComponent
{
public AIComponent(IAudioService audio, IShootingService shooting, INavigationService navigation)
{
...
}
}
And be able to specify in the composition root that
I must confess I'm completely lost when it comes to the latter two. I won't list my numerous abortive autofac-based attempts here as I've made a few dozen over a long period and none of them were remotely functional. I have read the documentation at length - I know lifetime scopes and Owned<>
are in the area I'm looking at, but I can't see how to transparently inject scoped dependencies as I'm looking to - yet I feel that DI in general seems supposed to facilitate exactly what I'm looking to do!
If this is sane, how can I achieve this? Or is this just diabolical? If so, how would you structure such an application making good use of DI to avoid passing objects around recursively, when the lifetimes of those objects vary depending on the context in which the object is being used?
LifetimeScopes sound like the answer. I think what you are basically doing is tying lifetime scopes to screens. So ShootingComponent and friends would be registered with .InstancePerMatchingLifetimeScope("Screen")
. The trick is then making it so that each screen is created in a new LifetimeScope tagged as "Screen." My first thought would be to make a screen factory like so:
public class ScreenFactory
{
private readonly ILifetimeScope _parent;
public ScreenFactory(ILifetimeScope parent) { _parent = parent; }
public TScreen CreateScreen<TScreen>() where TScreen : Screen
{
var screenScope = _parent.BeginLifetimeScope("Screen");
var screen = screenScope.Resolve<TScreen>();
screen.Closed += () => screenScope.Dispose();
return screen;
}
}
This is totally untested, but I think the concept makes sense.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With