Введение


Существует огромное количество паттернов и антипаттернов программирования. Зачастую использование паттернов диктует нам опыт и собственно знания их самих. В данной статье я хочу обсудить с вами применение паттерна Singleton, а именно его реализацию в Net применительно к Unity.


Singleton


Отмечу, что занимаю написанием кода в команде, поэтому, как можно больше работы увожу внутрь кода, чтобы разгрузить команду и устранить от нее необходимость задумываться о некоторых сложностях реализации тех или иных паттернов в Unity.


Изучая литературу по Net1, применительно к данному вопросу2 и в результате работы над несколькими проектами, родился следующий класс:


using UnityEngine;

/// <summary>
/// Реализация синглтона для наследования.
/// </summary>
/// <typeparam name="T">Класс, который нужно сделать синглтоном</typeparam>
/// <remarks>
/// Если необходимо обращаться к классу во время OnDestroy или OnApplicationQuit
/// необходимо проверять наличие объекта через IsAlive. Объект может быть уже 
/// уничтожен, и обращение к нему вызовет его еще раз.
/// 
/// 
/// При использовании в дочернем классе Awake, OnDestroy, 
/// OnApplicationQuit необходимо вызывать базовые методы
/// base.Awake() и тд.
/// 
/// Добавил скрываемый метод Initialization - чтобы перегружать его и использовать 
/// необходимые действия.
/// 
/// Создание объекта производится через unity, поэтому использовать блокировку 
/// объекта нет необходимости. Однако ее можно добавить, в случае если 
/// понадобится обращение к объекту из других потоков.
/// 
/// Из книг:
///     - Рихтер "CLR via C#"
///     - Chris Dickinson "Unity 2017 Game optimization"
///</remarks>

public class Singleton<T> : MonoBehaviour where T : Singleton<T>
{

    private static T instance = null;

    private bool alive = true;

    public static T Instance
    {
        get
        {
            if (instance != null)
            {
                return instance;
            }
            else
            {
                //Find T
                T[] managers = GameObject.FindObjectsOfType<T>();
                if (managers != null)
                {
                    if (managers.Length == 1)
                    {
                        instance = managers[0];
                        DontDestroyOnLoad(instance);
                        return instance;
                    }
                    else
                    {
                        if (managers.Length > 1)
                        {
                            Debug.LogError($"Have more that one {typeof(T).Name} in scene. " +
                                            "But this is Singleton! Check project.");
                            for (int i = 0; i < managers.Length; ++i)
                            {
                                T manager = managers[i];
                                Destroy(manager.gameObject);
                            }
                        }
                    }
                }
                //create 
                GameObject go = new GameObject(typeof(T).Name, typeof(T));
                instance = go.GetComponent<T>();
        instance.Initialization();
                DontDestroyOnLoad(instance.gameObject);
                return instance;
            }
        }

        //Can be initialized externally
        set
        {
            instance = value as T;
        }
    }

    /// <summary>
    /// Check flag if need work from OnDestroy or OnApplicationExit
    /// </summary>
    public static bool IsAlive
    {
        get
        {
            if (instance == null)
                return false;
            return instance.alive;
        }
    }

    protected void Awake()
    {
        if (instance == null)
        {
            DontDestroyOnLoad(gameObject);
            instance = this as T;
            Initialization();
        }
        else
        {
            Debug.LogError($"Have more that one {typeof(T).Name} in scene. " +
                            "But this is Singleton! Check project.");
            DestroyImmediate(this);
        }
    }

    protected void OnDestroy() { alive = false; }

    protected void OnApplicationQuit() { alive = false; }

    protected virtual void Initialization() { }
}

Сосредоточу свое внимание на нескольких аспектах.


Создание объекта


При расширение проекта, а тем более работы в команде > 3 человек зачастую возникает ситуация, когда последовательность создание объектов становится неясна. Строго говоря3, последовательность вызовов Awake() случайна (конечно это не совсем так, и на этом процесс можно влиять, но документация свята), ввиду чего необходимо устранить этот досадный недостаток посредством реализации свойства Instance{get;}. В результате мы получим полноценный доступ к синглтону из Awake() других классов.


В тоже время этот же факт, не позволяет нам использовать явно Lazy, так как объект будет напрямую вызван через Awake() в любом случае.

Основываясь на описании из банды 4-х4, данный класс предполагает свое единоличное существование, ввиду этого объясняется такая логика свойства Instance{get;}.


Инициализация объекта


Общее правило для Unity — делать это в Awake(). Однако, часто забывают вызвать методы родителя, поэтому предусмотрен виртуальный метод Initialization(). Это упрощает проверку при создании объекта и однозначно разделяет необходимые действия для инициализации объекта (KISS).


Внешняя инициализация


Предусмотрена по ряду причин, в том числе для управления через DI и SD. Подробное описание потребует расширенной статьи и расширения представленного выше класса. Данный задел оставлен на будущее.


Уничтожение объекта


Как правило, при использовании обычных синглтонов, и вызова их методов из методов OnDestroy(), OnApplicationQuit() других классов мы получаем ошибку следующего вида5:


Did you spawn new GameObjects from OnDestroy?

Как правило, я считаю код, написанный в таком стиле ошибочным, поэтому прощу его переписать. Но в случае, если так делать необходимо предусмотрен метод IsAlive(), который можно проверять, перед вызовом методов синглтона. Метод не лучший, но на безрыбье...


Заключение


Все чаще я прихожу к мнению, что пользуясь парадигмой Unity возможно реализовывать свои проекты без Singleton. Зачастую применение этого паттерна делает ваш код сильно-связанным и крайне хрупким.


Спасибо.


Источники


— Рихтер Дж "CLR via C#. Программирование на платформе Microsoft.NET# Framework 4.5 на языке C#", 2013


https://www.codingame.com/playgrounds/1979/different-ways-to-implement-singleton-in--net-and-make-people-hate-you-along-the-way


https://docs.unity3d.com/ru/current/ScriptReference/MonoBehaviour.Awake.html


https://ru.wikipedia.org/wiki/Design_Patterns


— Dickinson Chris "Unity 2017 Game Optimization, Second Edition", 2017