Введение
Существует огромное количество паттернов и антипаттернов программирования. Зачастую использование паттернов диктует нам опыт и собственно знания их самих. В данной статье я хочу обсудить с вами применение паттерна Singelton, а именно его реализацию в Net применительно к Unity.
Singelton
Отмечу, что занимаю написанием кода в команде, поэтому, как можно больше работы увожу внутрь кода, чтобы разгрузить команду и устранить от нее необходимость задумываться о некоторых сложностях реализации тех или иных паттернов в 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 Singelton<T> : MonoBehaviour where T : Singelton<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 Singelton! 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 Singelton! 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://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
ссылка на оригинал статьи https://habr.com/ru/post/495432/
Добавить комментарий