Service Locator Pattern

Het ‘Service Locator Pattern’, hoe kun je deze gebruiken in Unity3D voor het ontwikkelen van een VR applicatie.

Wat is dit pattern precies en waar kunnen we het voor gebruiken. Het ‘Service Locator Pattern’ is een design pattern dat in softwareontwikkeling gebruikt wordt om afhankelijkheden tussen verschillende classes te verminderen. Door gebruik te maken van een centrale plek om alle services te bewaren, de zogenaamde ‘Service Locator’ wordt een centrale plek gecreëerd, die de juiste service voor een bepaalde taak kan teruggeven. Dit design pattern bestaat al lang en is goed gedocumenteerd met diverse hulpbronnen op internet. Maar het gebruik van dit pattern binnen Unity is minder bekend.

In Unity wordt voor een ‘Service’ of centrale ‘Controller’ meestal gebruik gemaakt van ‘singletons’, hiervan zijn diverse voorbeelden terug te vinden. Nadeel van Singletons is dat je niet een centrale plek hebt, waar je kunt vinden welke services er binnen de applicatie allemaal beschikbaar zijn. Over het algemeen wordt het gebruik van ‘singletons’ afgeraden, toch is het in deze situatie handig. Door gebruik te gaan maken van het ‘Service Locator Pattern’ heb je straks nog maar 1 ‘singleton’ in je hele applicatie nodig, namelijk de ‘Service Locator’. In de onlinevoorbeelden over het gebruik van dit pattern binnen Unity3D vond ik met name voorbeelden om classes te gebruiken die je niet binnen Unity3D in hoefde te stellen. Maar vaak wil je juist een aantal instellingen vanuit de ‘Inspector’ in Unity3D instellen. Om dit toch mogelijk te maken moeten we een aantal dingen net even wat anders aanpakken.

In dit volgende deel wil ik jullie stap voor stap laten zien, hoe je binnen Unity3D gebruik kunt maken van het ‘Service Locator Pattern’ en nog steeds de ‘Inspector’ kunt gebruiken om de properties in te stellen.

Project binnen Unity3D

Uiteraard beginnen we met het starten van Unity3D, ik doe dit zelf altijd met behulp van de ‘Unity hub’ applicatie zodat ik eenvoudig meerdere versies van Unity op mijn systeem kan ondersteunen. Hier kies je voor het aanmaken van een nieuw project:

Ik kies hier voor een 3D project, en vervolgens geef je jouw project een naam en locatie waar deze kan worden opgeslagen.

Je krijgt vervolgens een schoon leeg project met daarin alvast een ‘SampleScene’ die ik zelf ook direct een duidelijke naam geef. Om het een en ander straks netjes te kunnen organiseren maak ik in de projectstructuur direct een map aan voor de ‘Scripts’ met hierin weer een map voor de ‘ServiceLocator’ scripts en een map voor de ‘Services’.

Door op de map ‘ServiceLocator’ met de rechtermuis te klikken heb je de mogelijkheid hier scripts aan toe te voegen.

Hier voegen we nu een drietal scripts aan toe, te weten ‘Bootstrapper’, ‘IApplicationService’ en ‘ServiceLocator’.

Nu gaan we deze scripts stuk voor stuk vullen met code. Door op een file te dubbelklikken zal Visual Studio geopend worden en kun je de code bewerken.

In de ‘IApplicationService’ voegen we vervolgens de volgende code toe:

/// <summary>
/// Base interface for our service locator to work with. Services implementing
/// this interface will be retrievable using the locator.
/// </summary>
public interface IApplicationService
{
}

Dit is vooral handig om straks eenvoudig te constateren dat een Service geschikt is voor het gebruik van de ‘Service Locator’.

Vervolgens open je de code voor de ‘ServiceLocator’, hier voeg je de volgende code in:

using System;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// Simple service locator for <see cref="IApplicationService"/> instances.
/// </summary>
public class ServiceLocator
{
    private ServiceLocator() { }

    /// <summary>
    /// currently registered services.
    /// </summary>
    private readonly Dictionary<string, IApplicationService> services = new Dictionary<string, IApplicationService>();

    /// <summary>
    /// Gets the currently active service locator instance.
    /// </summary>
    public static ServiceLocator Current { get; private set; }

    /// <summary>
    /// Initalizes the service locator with a new instance.
    /// </summary>
    public static void Initiailze()
    {
        Current = new ServiceLocator();
    }

    /// <summary>
    /// Gets the service instance of the given type.
    /// </summary>
    /// <typeparam name="T">The type of the service to lookup.</typeparam>
    /// <returns>The service instance.</returns>
    public T Get<T>() where T : IApplicationService
    {
        string key = typeof(T).Name;
        if (!services.ContainsKey(key))
        {
            Debug.LogError($"{key} not registered with {GetType().Name}");
            throw new InvalidOperationException();
        }
        return (T)services[key];
    }

    /// <summary>
    /// Registers the service with the current service locator.
    /// </summary>
    /// <typeparam name="T">Service type.</typeparam>
    /// <param name="service">Service instance.</param>
    public void Register<T>(T service) where T : IApplicationService
    {
        string key = typeof(T).Name;
        if (services.ContainsKey(key))
        {
            Debug.LogError($"Attempted to register service of type {key} which is already registered with the {GetType().Name}.");
            return;
        }

        services.Add(key, service);
    }

    /// <summary>
    /// Unregisters the service from the current service locator.
    /// </summary>
    /// <typeparam name="T">Service type.</typeparam>
    public void Unregister<T>() where T : IApplicationService
    {
        string key = typeof(T).Name;
        if (!services.ContainsKey(key))
        {
            Debug.LogError($"Attempted to unregister service of type {key} which is not registered with the {GetType().Name}.");
            return;
        }

        services.Remove(key);
    }
}

Het bootstrapper script moet telkens bijgewerkt worden als er een nieuwe Service bijkomt. Deze zal ik dan ook pas gaan vullen zodra zo de eerste services gemaakt zijn. Voor dit voorbeeld heb ik alvast een tweetal services bedacht namelijk de ‘PlayerService’ en de ‘LevelService’, deze gaan we nu aanmaken op dezelfde manier als we eerder al scripts hebben toegevoegd. Zodra dit klaar is zal de projectstructuur er als volgt uitzien:

Deze nieuwe services gaan we ook van wat code voorzien, aangezien dit slechts een voorbeeld is heb ik wat van mijn fantasie gebruikt wat deze services doen. Ik ga ervan uit dat jullie met veel nuttigere invullingen gaan komen dan ik hier doe 😉.

De volgende code zet je in de ‘LevelService’:

using System.Collections.Generic;
using UnityEngine;

public class LevelService : MonoBehaviour, IApplicationService
{
    public Level CurrentLevel;

    private LevelService()
    {
        CurrentLevel = Level.Beginner;
    }

    public void NextLevel()
    {
        if (CurrentLevel != Level.Expert)
        {
            CurrentLevel++;
        }
    }
}

public enum Level
{
    Beginner = 0,
    Intermediate = 1,
    Advanced = 2,
    Expert = 3
}

En de volgende code zet je in de ‘PlayerService’:

using UnityEngine;

public class PlayerService : MonoBehaviour, IApplicationService
{
    private Level currentLevel;
    private LevelService currentLevelService;

    public string PlayerName;
    public int Score;

    public void Awake()
    {
        currentLevelService = ServiceLocator.Current.Get<LevelService>();

        currentLevel = currentLevelService.CurrentLevel;
    }

    public void PlayerReachesNextLevel()
    {
        currentLevelService.NextLevel();
    }
}

In deze ‘PlayerService’ zie je ook direct hoe je heel eenvoudig de ‘ServiceLocator’ kunt aanspreken om een bepaalde service op te halen, de code daarvoor staat geel gemarkeerd in bovenstaande code.

Nu we twee services tot onze beschikking hebben moeten we (zoals eerder al gezegd) ook de ‘Bootstrapper’ gaan aanvullen met deze code regels:

using UnityEngine;

public class Bootstrapper : MonoBehaviour
{
    public void Initiailze()
    {
        // Initialize default service locator.
        ServiceLocator.Initiailze();

        // Register all your services next.
        ServiceLocator.Current.Register(this.GetComponentInChildren<LevelService>());
        ServiceLocator.Current.Register(this.GetComponentInChildren<PlayerService>());
    }

    private void Awake()
    {
        this.Initiailze();
    }
}

De oplettende lezer ziet hier al iets terug, wat je in de onlinevoorbeelden niet kunt vinden. Namelijk dat we in de code op zoek gaan naar ‘GetComponentInChildren’, dit doen we hier zodat we via de ‘inspector’ instellingen aan de services kunnen configureren. Om dit voor elkaar te krijgen moet je wel de structuur binnen je scene goed inrichten. Dat is dan ook de volgende stap die we gaan nemen.

In de ‘Hierarchy’ gaan we op het hoogste object (root object) met de rechtermuis klikken en kiezen we ervoor om een Empty GameObject te maken.

Onder dit betreffende GameObject gaan we vervolgens nog twee Empty GameObjecten maken. Deze geef je namen, en sorteer je zodat de ‘Hierarchy’ er als volgt uit komt te zien:

Aan deze lege GameObjecten gaan we vervolgens de juiste scripts toevoegen. Dit doe je door eerst in de ‘Hierarchy’ het juiste GameObject te selecteren en vervolgens in de ‘Inpector’ te klikken op de knop ‘Add component’. Je kunt vervolgens het juiste script zoeken en toevoegen, zie volgende schermafdruk:

Nadat je het ‘Bootstrapper’ script hebt toegevoegd kun je vervolgens het juiste script aan de twee services toevoegen.

Als je vervolgens de ‘PlayerService’ bekijkt in de ‘Inspector’ zie je het volgende:

Hier zie je nu dat je gebruik maakt van een service die via de ‘Service Locator’ opgehaald kan worden. Maar je ziet ook dat je nog steeds de mogelijkheid hebt om properties zoals ‘Player Name’ en ‘Score’ behorende bij het object via de ‘Inspector’ eenvoudig in te stellen.

Op basis van bovenstaande voorbeeld is het vervolgens erg eenvoudig zelf scripts te gaan schrijven, die de daadwerkelijke game flow gaan uitvoeren. Vanuit deze scripts is het op basis van het gegeven voorbeeld heel eenvoudig zelf via de ‘Service locator’ een service op te gaan halen. En ik kan me goed voorstellen dat je zelf de bestaande services nog verder wil uitbreiden en zelfs eigen services toe wil voegen.

Conclusie

Door gebruik te maken van het ‘Service Locator Pattern’ voorkom je dat je diverse ‘Singleton’ classes nodig hebt in je project, je kunt je beperken tot het maken van 1 ‘Singleton’ namelijk de ‘ServiceLocator’. Deze biedt dan de mogelijkheid diverse services binnen je applicatie beschikbaar te stellen, die je ook nog gewoon kunt instellen via de ‘Inspector’ binnen Unity3D. Als je alleen de aangeboden services ‘Service’ in de naam geeft dan zorgt code completion ervoor dat je ze zeer eenvoudig terug kunt vinden op het moment dat je via de ‘ServiceLocator’ een service terug wil halen. Let wel goed op dat je alle gemaakte services ook opneemt in je ‘Bootstrapper’ anders kom je tijdens run-time in de problemen!

Roy Janssen

Roy werkt vanuit zijn eigen onderneming Semper IT Services in diverse rollen zoals onder andere technisch architect, lead developer en consultant binnen kleine en grote projecten. Als trainer en spreker deelt hij graag zijn kennis over C#, WPF, XAML, HoloLens, NUI, AR/VR en MakeCode met de community.