Service Locator — Unity

Lalo Berro
7 min readJan 8, 2024

--

Heeeeey! In this post, I’m going to explain and implement my own version of the Service Locator Pattern using Unity and the best practices.

Introduction

A lot of times we work on projects that have a lot of singletons and concrete dependencies. This might not be a problem in prototypes or small projects, but when we work on bigger projects, we need a way to keep everything more organized. One of the things we can do to achieve it is applying a Service Locator.

Theory

Basically, the ServiceLocator is a Dictionary which contains classes as value and types as key. In a certain moment of the execution, we will add new services (classes), and in another moment we will get a service by its type. This is a pseudocode of the pattern:

class ServiceLocator
{
Dictionary<Service, Type> _services;

public void Add(Service service)
{
_services.Add(service, service.type);
}

public Services Get(Type type)
{
return _services[type];
}
}

So with this in mind, we will establish two main purposes. The first one is reducing the amount of singleton in the project, doing the ServiceLocator as the only singleton of the project and save into it all the instances. And the second one is to make use of the dependency inversion, saving the service key as its parent (abstract class or interface). Applying that, it allows us to reduce the amount of concrete dependencies and make the project more extendible.

Implementation

To start, we will create the ServiceLocator class.

public class ServiceLocator
{
}

Then we will create the Add() method (which is generic) and the Dictionary var to contains the services, and it will have a key as Type and a value as object (c#).

public class ServiceLocator
{
private readonly Dictionary<Type, object> _services = new();

public void Add<ServiceType>(ServiceType service)
{
Type type = typeof(ServiceType);

_services.Add(type, service);
}
}

Let’s continue with the Get() method, we will throw an exception if the service doesn’t exist. To return the service, we will cast it.

public ServiceType Get<ServiceType>()
{
Type type = typeof(ServiceType);

if (!_services.TryGetValue(type, out var service))
throw new Exception("There isn't a service with the type: " + type.Name);

return (ServiceType)service;
}

Then we will create the Remove() method.

public void Remove<ServiceType>()
{
Type type = typeof(ServiceType);

if (!_services.ContainsKey(type))
throw new Exception($"The service {type.Name} is not in the service locator. So you can't remove.");

_services.Remove(type);
}

And lastly, we will create the method IsContained() to check if a service exists in the dictionary.

public bool IsContained<ServiceType>()
{
Type type = typeof(ServiceType);

return _services.ContainsKey(type);
}

Now you can extract the interface from this class.

public interface IServiceLocator
{
void Register<ServiceType>(ServiceType service);
ServiceType Get<ServiceType>();
void Remove<ServiceType>();
bool IsContained<ServiceType>();
}

So the entire class ends like this:

public class ServiceLocator : IServiceLocator
{
private readonly Dictionary<Type, object> _services = new();

public void Add<ServiceType>(ServiceType service)
{
Type type = typeof(ServiceType);

_services.Add(type, service);
}

public ServiceType Get<ServiceType>()
{
Type type = typeof(ServiceType);

if (!_services.TryGetValue(type, out var service))
throw new Exception("There isn't a service with the type: " + type.Name);

return (ServiceType)service;
}

public void Remove<ServiceType>()
{
Type type = typeof(ServiceType);

if (!_services.ContainsKey(type))
throw new Exception($"The service {type.Name} is not in the service locator. So you can't remove.");

_services.Remove(type);
}

public bool IsContained<ServiceType>()
{
Type type = typeof(ServiceType);

return _services.ContainsKey(type);
}
}

The main logic is done, now let’s continue with the example.

Example

In this sample, we are going to implement a counter application using the ServiceLocator to connect the main system using dependency inversion and dependency injection.

First, we will create the scene, name it ServiceLocatorExample and add a canvas with a text in the center and a button below it, something like this:

Then let’s start with the code. We will create the folders Script > Domain and the script Counter and ICounterPresenter. The Counter is in charge of making the count and updating the number in the Presenter.

public class Counter
{
private readonly ICounterPresenter _counterPresenter;

private int _count;

public Counter(ICounterPresenter counterPresenter)
{
_counterPresenter = counterPresenter;
}

public void AddNumber()
{
_count++;

_counterPresenter.UpdateCount(_count);
}
}
public interface ICounterPresenter
{
void UpdateCount(int count);
}

Now we will create ICounterView under the folder Scripts > InterfaceAdapters > View. It is in charge of updating the count text and invoking an action every time the user presses the button.

public interface ICounterView
{
Action OnAddNumberClick { get; set;}
void ShowCount(string count);
}

Now we will create the CounterPresenter under the folder Scripts > InterfaceAdapters > Presenters. It’s in charge of formatting the number and giving it to the View.

public class CounterPresenter : ICounterPresenter
{
private readonly ICounterView _counterView;

public CounterPresenter(ICounterView counterView)
{
_counterView = counterView;
}

public void UpdateCount(int count)
{
_counterView.ShowCount(count.ToString());
}
}

Now we will create the CounterController under Scripts > InterfaceAdapters > Controllers. It’s in charge of subscribing to the View and saying to the Counter to AddNumber.

public class CounterController : IDisposable
{
private readonly ICounterView _counterView;
private readonly Counter _counter;

public CounterController(ICounterView counterView, Counter counter)
{
_counterView = counterView;
_counter = counter;

_counterView.OnAddNumberClick += AddNumber;
}

private void AddNumber()
{
_counter.AddNumber();
}

public void Dispose()
{
_counterView.OnAddNumberClick -= AddNumber;
}
}

Let’s do the CounterView under Scripts > View.

public class CounterView : MonoBehaviour, ICounterView
{
[Header("References")]
[SerializeField] private TMP_Text _countText;
[SerializeField] private Button _button;

public Action OnAddNumberClick { get; set; }

private void Awake()
{
_button.onClick.AddListener(AddNumber);
}

private void AddNumber()
{
OnAddNumberClick?.Invoke();
}

public void ShowCount(string count)
{
_countText.text = count;
}
}

So to finish, we will create the installers, and we will make use of the ServiceLocator. In this case, I’m going to use the ServiceLocator as a singleton, implementing this one:

public class Singleton<T> where T : class, new()
{
protected Singleton() { }

private static readonly Lazy<T> _instance = new Lazy<T>(() => new T());

public static T Instance { get { return _instance.Value; } }
}

Note that it is not a Monobehaviour implementation, so the ServiceLocator ends like this, I called ServiceLocatorInstance:

public class ServiceLocatorInstance : Singleton<ServiceLocatorInstance>
{
private readonly IServiceLocator _serviceLocator = new ServiceLocator();

public void Add<ServiceType>(ServiceType service)
{
_serviceLocator.Add(service);
}

public ServiceType Get<ServiceType>()
{
return _serviceLocator.Get<ServiceType>();
}

public void Remove<ServiceType>()
{
_serviceLocator.Remove<ServiceType>();
}

public bool IsContained<ServiceType>()
{
return _serviceLocator.IsContained<ServiceType>();
}
}

So now we will create the Installers, this is for creating and managing the instances of the classes. To make that work, we need to create two classes, ClassInstaller, which is in charge of installing individually each class, and then we have ContextInstaller that is in charge of installing multiple ClassInstallers in the Awake method.

public abstract class ClassInstaller : MonoBehaviour
{
public abstract void Install();
}
public class ContextInstaller : MonoBehaviour
{
[Header("References")]
[SerializeField] private ClassInstaller[] _classInstallers;

private void Awake()
{
foreach (var classInstaller in _classInstallers)
{
classInstaller.Install();
}
}
}

Now we can create all the installers and use the ServiceLocator.

Let’s begin with the CounterViewInstaller.

public class CounterViewInstaller : ClassInstaller
{
[Header("References")]
[SerializeField] private CounterView _counterView;

public override void Install()
{
ServiceLocatorInstance.Instance.Add<ICounterView>(_counterView);
}
}

Then we will create the CounterPresenterInstaller.

public class CounterPresenterInstaller : ClassInstaller
{
public override void Install()
{
ICounterView counterView = ServiceLocatorInstance.Instance.Get<ICounterView>();
ICounterPresenter counterPresenter = new CounterPresenter(counterView);

ServiceLocatorInstance.Instance.Add<ICounterPresenter>(counterPresenter);
}
}

Then we will create the CounterInstaller.

public class CounterInstaller : ClassInstaller
{
public override void Install()
{
ICounterPresenter counterPresenter = ServiceLocatorInstance.Instance.Get<ICounterPresenter>();
Counter counter = new Counter(counterPresenter);

ServiceLocatorInstance.Instance.Add<Counter>(counter);
}
}

And for last, the CounterControllerInstaller.

public class CounterControllerInstaller : ClassInstaller
{
private CounterController _counterController;

public override void Install()
{
ICounterView counterView = ServiceLocatorInstance.Instance.Get<ICounterView>();
Counter counter = ServiceLocatorInstance.Instance.Get<Counter>();
_counterController = new CounterController(counterView, counter);

ServiceLocatorInstance.Instance.Add<CounterController>(_counterController);
}

private void OnDestroy()
{
_counterController.Dispose();
}
}

Now that we are done the code, we will set up the scene. First, we create a gameobject with the ContextInstaller script and call it Counter-ContextInstaller.

Then add the CounterView and all the Installers as a child (remember, add his corresponding script to each one).

Then add all the installers to ContextInstaller. Notes that the order is very important, otherwise we will have dependencies problem.

To finish config the View and drag it into CounterViewInstaller

We are done! Now let’s play it.

Conclusion

When we want to apply dependency injection, dependency inversion and reduce the singletons, the ServiceLocator is one of the best options.

Thanks for reading I left you the link to the repository with all the code github.com/LaloBerro/UPM-ObjectPool.git, there you have the installation guide and then under samples in the package manager windows you will find this post example.

If you have any questions, write a comment here or email me to laurencioberro@gmail.com.

I’ll see you on the next post!

--

--

Lalo Berro

Im Lalo a passionate videogame programmer that loves share quality and advanced content.