Toolbox

From Unify Community Wiki
(Difference between revisions)
Jump to: navigation, search
(Implementation)
(Rewrote script and sample code and improved documentation.)
 
Line 1: Line 1:
 
== Introduction ==
 
== Introduction ==
Originated from [http://www.ibm.com/developerworks/webservices/library/co-single/index.html http://www.ibm.com/developerworks/webservices/library/co-single/index.html]
+
The "Toolbox" is a concept introduced by J.B. Rainsberger in the article [https://www.ibm.com/developerworks/webservices/library/co-single/index.html Use your singletons wisely]. The basic idea being that the application, not the components, should be the singleton. This basically means creating one singleton (the toolbox) that aggregates and manages all other classes that would normally be singletons themselves. This improves upon the concept by reducing [http://en.wikipedia.org/wiki/Coupling_(computer_programming) coupling] and making it much easier to [http://en.wikipedia.org/wiki/Unit_testing test].
  
If you're here, you probably want to implement Global Variables. For any other usage, just take this as a starting place.
+
== Implementation ==
 +
=== Toolbox.cs ===
 +
<syntaxhighlight lang="csharp">
 +
using System;
 +
using System.Collections.Generic;
 +
using UnityEngine;
  
The toolbox is a [[singleton]], before anything else. But it improves upon the concept. Basically this encourages better coding practices, such as reducing [http://en.wikipedia.org/wiki/Coupling_(computer_programming) coupling] and [http://en.wikipedia.org/wiki/Unit_testing unit testing].
+
public class Toolbox : Singleton<Toolbox>
 +
{
 +
    // Used to track any global components added at runtime.
 +
    private Dictionary<string, Component> m_Components = new Dictionary<string, Component>();
  
== Usage Example ==
 
'''MyClass.cs'''
 
<syntaxhighlight lang="csharp">
 
public class MyClass : MonoBehaviour {
 
void Awake () {
 
Debug.Log(Toolbox.Instance.myGlobalVar);
 
 
Toolbox toolbox = Toolbox.Instance;
 
Debug.Log(toolbox.language.current);
 
  
// (optional) runtime registration of global objects
+
    // Prevent constructor use.
MyComponent myComponent = Toolbox.Instance.RegisterComponent<MyComponent>();
+
    protected Toolbox() { }
Debug.Log(myComponent.anotherGlobalVar);
+
Debug.Log(Toolbox.Instance.GetComponent<myComponent>().anotherGlobalVar); // GetComponent is not recommended
+
Destroy(myComponent);
+
}
+
}
+
  
[System.Serializable]
 
public class MyComponent {
 
public string anotherGlobalVar = "yeah";
 
}
 
</syntaxhighlight>
 
  
== Implementation ==
+
    private void Awake()
'''Toolbox.cs'''
+
    {
<syntaxhighlight lang="csharp">
+
        // Put initialization code here.
public class Toolbox : Singleton<Toolbox> {
+
    }
protected Toolbox () {} // guarantee this will be always a singleton only - can't use the constructor!
+
  
public string myGlobalVar = "whatever";
 
  
public Language language = new Language();
+
    // Define all required global components here. These are hard-codded components
 +
    // that will always be added. Unlike the optional components added at runtime.
  
void Awake () {
 
// Your initialization code here
 
}
 
  
// (optional) allow runtime registration of global objects
+
    // The methods below allow us to add global components at runtime.
static public T RegisterComponent<T> () where T: Component {
+
    // TODO: Convert from string IDs to component types.
return Instance.GetOrAddComponent<T>();
+
    public Component AddGlobalComponent(string componentID, Type component)
}
+
    {
}
+
        if (m_Components.ContainsKey(componentID))
 +
        {
 +
            Debug.LogWarning("[Toolbox] Global component ID \""
 +
                +componentID + "\" already exist! Returning that." );
 +
            return GetGlobalComponent(componentID);
 +
        }
  
[System.Serializable]
+
        var newComponent = gameObject.AddComponent(component);
public class Language {
+
        m_Components.Add(componentID, newComponent);
public string current;
+
        return newComponent;
public string lastLang;
+
    }
 +
 
 +
 
 +
    public void RemoveGlobalComponent(string componentID)
 +
    {
 +
        Component component;
 +
 
 +
        if (m_Components.TryGetValue(componentID, out component))
 +
        {
 +
            Destroy(component);
 +
            m_Components.Remove(componentID);
 +
        }
 +
        else
 +
        {
 +
            Debug.LogWarning("[Toolbox] Trying to remove nonexistent component ID \""
 +
                + componentID + "\"! Typo?");
 +
        }
 +
    }
 +
 
 +
 
 +
    public Component GetGlobalComponent(string componentID)
 +
    {
 +
        Component component;
 +
 
 +
        if (m_Components.TryGetValue(componentID, out component))
 +
        {
 +
            return component;
 +
        }
 +
 
 +
        Debug.LogWarning("[Toolbox] Global component ID \""
 +
    + componentID + "\" doesn't exist! Typo?");
 +
        return null;
 +
    }
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
  
== Requirement ==
+
== Requirements ==
 +
* Uses [[Singleton]] base class.
 +
 
 +
== Usage ==
 +
Any required components should be defined in the toolbox class itself. So for example, let's say we have a very important '''PlayerData''' MonoBehaviour. We should create an instance of this in the toolbox and add a public function to allow us to retrieve it.
  
(optional)
 
'''MonoBehaviourExtended.cs''' (from [[GetOrAddComponent]])
 
 
<syntaxhighlight lang="csharp">
 
<syntaxhighlight lang="csharp">
static public class MethodExtensionForMonoBehaviourTransform {
+
 
/// <summary>
+
private PlayerData m_PlayerData = new PlayerData();
/// Gets or add a component. Usage example:
+
 
/// BoxCollider boxCollider = transform.GetOrAddComponent<BoxCollider>();
+
public PlayerData GetPlayerData()
/// </summary>
+
{
static public T GetOrAddComponent<T> (this Component child) where T: Component {
+
    return m_PlayerData;
T result = child.GetComponent<T>();
+
if (result == null) {
+
result = child.gameObject.AddComponent<T>();
+
}
+
return result;
+
}
+
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
Now to retrieve this global component we simply use '''Toolbox.Instance''' followed by the function we defined in the toolbox.
  
'''Singleton.cs''' (from [[Singleton]])
 
 
<syntaxhighlight lang="csharp">
 
<syntaxhighlight lang="csharp">
using UnityEngine;
+
var playerData = Toolbox.Instance.GetPlayerData();
 +
Debug.Log(playerData.CurrentLevel);
 +
</syntaxhighlight>
  
/// <summary>
+
You can also add, get or remove global components at runtime. However, these methods use strings for IDs which are error-prone, so use with cation!
/// Be aware this will not prevent a non singleton constructor
+
 
///  such as `T myT = new T();`
+
<syntaxhighlight lang="csharp">
/// To prevent that, add `protected T () {}` to your singleton class.
+
// Add and get a global component.
///
+
var playerData = Toolbox.Instance.AddGlobalComponent("PlayerData", PlayerData);
/// As a note, this is made as MonoBehaviour because we need Coroutines.
+
Debug.Log(playerData.CurrentLevel);
/// </summary>
+
 
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
+
// Getting a global component.
{
+
var playerData = Toolbox.Instance.GetGlobalComponent("PlayerData");
private static T _instance;
+
Debug.Log(playerData.CurrentLevel);
+
 
private static object _lock = new object();
+
// Delete existing global component.
+
Toolbox.Instance.RemoveGlobalComponent("PlayerData");
public static T Instance
+
{
+
get
+
{
+
if (applicationIsQuitting) {
+
Debug.LogWarning("[Singleton] Instance '"+ typeof(T) +
+
"' already destroyed on application quit." +
+
" Won't create again - returning null.");
+
return null;
+
}
+
+
lock(_lock)
+
{
+
if (_instance == null)
+
{
+
_instance = (T) FindObjectOfType(typeof(T));
+
+
if ( FindObjectsOfType(typeof(T)).Length > 1 )
+
{
+
Debug.LogError("[Singleton] Something went really wrong " +
+
" - there should never be more than 1 singleton!" +
+
" Reopenning the scene might fix it.");
+
return _instance;
+
}
+
+
if (_instance == null)
+
{
+
GameObject singleton = new GameObject();
+
_instance = singleton.AddComponent<T>();
+
singleton.name = "(singleton) "+ typeof(T).ToString();
+
+
DontDestroyOnLoad(singleton);
+
+
Debug.Log("[Singleton] An instance of " + typeof(T) +
+
" is needed in the scene, so '" + singleton +
+
"' was created with DontDestroyOnLoad.");
+
} else {
+
Debug.Log("[Singleton] Using instance already created: " +
+
_instance.gameObject.name);
+
}
+
}
+
+
return _instance;
+
}
+
}
+
}
+
+
private static bool applicationIsQuitting = false;
+
/// <summary>
+
/// When Unity quits, it destroys objects in a random order.
+
/// In principle, a Singleton is only destroyed when application quits.
+
/// If any script calls Instance after it have been destroyed,
+
///  it will create a buggy ghost object that will stay on the Editor scene
+
///  even after stopping playing the Application. Really bad!
+
/// So, this was made to be sure we're not creating that buggy ghost object.
+
/// </summary>
+
public void OnDestroy () {
+
applicationIsQuitting = true;
+
}
+
}
+
 
</syntaxhighlight>
 
</syntaxhighlight>
 +
 +
'''Note:''' It's best not to add or delete components directly on the toolbox yourself, since this will likely lead to problems.
 +
 +
[[Category: C Sharp]]
 +
[[Category: Design Patterns]]

Latest revision as of 02:40, 14 November 2018

Contents

[edit] Introduction

The "Toolbox" is a concept introduced by J.B. Rainsberger in the article Use your singletons wisely. The basic idea being that the application, not the components, should be the singleton. This basically means creating one singleton (the toolbox) that aggregates and manages all other classes that would normally be singletons themselves. This improves upon the concept by reducing coupling and making it much easier to test.

[edit] Implementation

[edit] Toolbox.cs

using System;
using System.Collections.Generic;
using UnityEngine;
 
public class Toolbox : Singleton<Toolbox>
{
    // Used to track any global components added at runtime.
    private Dictionary<string, Component> m_Components = new Dictionary<string, Component>();
 
 
    // Prevent constructor use.
    protected Toolbox() { }
 
 
    private void Awake()
    {
        // Put initialization code here.
    }
 
 
    // Define all required global components here. These are hard-codded components
    // that will always be added. Unlike the optional components added at runtime.
 
 
    // The methods below allow us to add global components at runtime.
    // TODO: Convert from string IDs to component types.
    public Component AddGlobalComponent(string componentID, Type component)
    {
        if (m_Components.ContainsKey(componentID))
        {
            Debug.LogWarning("[Toolbox] Global component ID \""
                +componentID + "\" already exist! Returning that." );
            return GetGlobalComponent(componentID);
        }
 
        var newComponent = gameObject.AddComponent(component);
        m_Components.Add(componentID, newComponent);
        return newComponent;
    }
 
 
    public void RemoveGlobalComponent(string componentID)
    {
        Component component;
 
        if (m_Components.TryGetValue(componentID, out component))
        {
            Destroy(component);
            m_Components.Remove(componentID);
        }
        else
        {
            Debug.LogWarning("[Toolbox] Trying to remove nonexistent component ID \""
                + componentID + "\"! Typo?");
        }
    }
 
 
    public Component GetGlobalComponent(string componentID)
    {
        Component component;
 
        if (m_Components.TryGetValue(componentID, out component))
        {
            return component;
        }
 
        Debug.LogWarning("[Toolbox] Global component ID \""
    + componentID + "\" doesn't exist! Typo?");
        return null;
    }
}

[edit] Requirements

[edit] Usage

Any required components should be defined in the toolbox class itself. So for example, let's say we have a very important PlayerData MonoBehaviour. We should create an instance of this in the toolbox and add a public function to allow us to retrieve it.

private PlayerData m_PlayerData = new PlayerData();
 
public PlayerData GetPlayerData()
{
    return m_PlayerData;
}

Now to retrieve this global component we simply use Toolbox.Instance followed by the function we defined in the toolbox.

var playerData = Toolbox.Instance.GetPlayerData();
Debug.Log(playerData.CurrentLevel);

You can also add, get or remove global components at runtime. However, these methods use strings for IDs which are error-prone, so use with cation!

// Add and get a global component.
var playerData = Toolbox.Instance.AddGlobalComponent("PlayerData", PlayerData);
Debug.Log(playerData.CurrentLevel);
 
// Getting a global component.
var playerData = Toolbox.Instance.GetGlobalComponent("PlayerData");
Debug.Log(playerData.CurrentLevel);
 
// Delete existing global component.
Toolbox.Instance.RemoveGlobalComponent("PlayerData");

Note: It's best not to add or delete components directly on the toolbox yourself, since this will likely lead to problems.

Personal tools
Namespaces

Variants
Actions
Navigation
Extras
Toolbox