One of the biggest source of bugs in our apps is state: all of that persistent data we keep around in memory. When things change we need to make sure to update all of it at the right times and with the right new parts of the state that changed. Inevitably things get out of sync and our app is in “a bad state”. Today’s article discusses some ways we can prune the “graph” of objects that we create in OOP so that there’s less state to maintain. Read on for some interesting techniques that could help you prevent bugs!

Say we have a player with a wallet that contains coins. Here’s how the public interface might look:

public interface IWallet
{
	int Coins { get; }
}
 
public interface IPlayer
{
	IWallet Wallet { get; }
	void BuyCoins();
}

This might be implemented internally like this:

internal interface IInternalWallet : IWallet
{
	void SetCoins(int num);
}
 
internal class Wallet : IInternalWallet
{
	public void SetCoins(int num) { Coins = num; }
 
	public int Coins { get; private set; }
 
	public Wallet(int coins)
	{
		Coins = coins;
	}
}
 
internal class Player : IPlayer
{
	public IWallet Wallet { get; private set; }
 
	public Player()
	{
		Wallet = new Wallet(PlayerPrefs.GetInt("Coins"));
	}
 
	public void BuyCoins()
	{
		PlayerPrefs.SetInt("Coins", 100);
		Wallet.SetCoins(100);
	}
}

So in memory we have a Player object and it has a reference to a Wallet object. Whenever the number of coins changes, the Player has to inform the Wallet that the value has changed.

But the number of coins is stored in the PlayerPrefs database, so we’re really just keeping RAM in sync with the on-disk database. Instead, we could query the database on-demand. With this in mind we can rewrite the code like this:

internal interface IInternalWallet : IWallet
{
}
 
internal class Wallet : IInternalWallet
{
	public int Coins { get { return PlayerPrefs.GetInt("Coins"); } }
}
 
internal class Player : IPlayer
{
	public IWallet Wallet { get; private set; }
 
	public Player()
	{
		Wallet = new Wallet();
	}
 
	public void BuyCoins()
	{
		PlayerPrefs.SetInt("Coins", 100);
	}
}

Ah! Much simpler! There’s no longer any in-memory coins value. Instead, the Coins property fetches it from PlayerPrefs whenever its get is called. But why do we even bother having the Wallet field of Player around in memory when we no longer use it to synchronize? We can easily do away with it too:

internal class Player : IPlayer
{
	public IWallet Wallet { get { return new Wallet(); } }
 
	public void BuyCoins()
	{
		PlayerPrefs.SetInt("Coins", 100);
	}
}

Even simpler! In both cases we’ve replaced in-memory state with temporary objects, simplifying how much we have to make sure to keep in sync.

Now for some trouble. Say the wallet has an event for when its coins change:

public interface IWallet
{
	int Coins { get; }
	event Action<int> OnCoinsChanged;
}

Now when the player buys coins we don’t have a Wallet to tell to dispatch the OnCoinsChanged event. We could “solve” this by undoing our gains and reintroducing all the state, but let’s think of another way around it.

One key realization is that when you’re adding or removing listeners to an event you can override what happens, just like get and set for properties. This makes it possible for OnCoinsChanged to forward the listeners added and removed from it to the Player. We can do this by introducing some more events:

internal interface IInternalWallet : IWallet
{
	event Action<Action<int>> OnCoinsChangedListenerAdded;
	event Action<Action<int>> OnCoinsChangedListenerRemoved;
}

These events get dispatched from the add and remove blocks like this:

internal class Wallet : IInternalWallet
{
	public event Action<int> OnCoinsChanged
	{
		add { OnCoinsChangedListenerAdded(value); }
		remove { OnCoinsChangedListenerRemoved(value); }
	}
	public event Action<Action<int>> OnCoinsChangedListenerAdded;
	public event Action<Action<int>> OnCoinsChangedListenerRemoved;
 
	public int Coins { get { return PlayerPrefs.GetInt("Coins"); } }
}

When listeners are added or removed, the listener is passed to the OnCoinsChangedListenerAdded and OnCoinsChangedListenerRemoved events. Now Player can listen for these events:

internal class Player : IPlayer
{
	private event Action<int> OnCoinsChanged;
 
	public IWallet Wallet
	{
		get
		{
			var wallet = new Wallet();
			wallet.OnCoinsChangedListenerAdded += a => OnCoinsChanged += a;
			wallet.OnCoinsChangedListenerRemoved += a => OnCoinsChanged -= a;
			return wallet;
		}
	}
 
	public void BuyCoins()
	{
		PlayerPrefs.SetInt("Coins", 100);
		if (OnCoinsChanged != null)
		{
			OnCoinsChanged(100);
		}
	}
}

Notice how Player has its own private OnCoinsChanged event. The listeners it gets from Wallet are added here. This means that it can dispatch its own OnCoinsChanged event in BuyCoins and all the listeners that were added via the event in Wallet are called!

Finally, we need to make sure to clean up after ourselves. We create a new Wallet every time someone calls the Wallet getter but all of their listeners stick around on the Player in its OnCoinsChanged event unless explicitly removed. This is an error-prone situation since it’s highly likely that someone will forget to remove their listener from the Wallet at some point. It would be much better if the listener removed itself when the (temporary) Wallet object was garbage collected.

We can work around this too by adding another event:

internal interface IInternalWallet : IWallet
{
	event Action<Action<int>> OnCoinsChangedListenerAdded;
	event Action<Action<int>> OnCoinsChangedListenerRemoved;
	event Action OnDestroyed;
}

The OnDestroyed simply tells the Player that the wallet has been garbage collected. It’s easy to do this via a destructor: ~Wallet

internal class Wallet : IInternalWallet
{
	public event Action<int> OnCoinsChanged
	{
		add { OnCoinsChangedListenerAdded(value); }
		remove { OnCoinsChangedListenerRemoved(value); }
	}
	public event Action<Action<int>> OnCoinsChangedListenerAdded;
	public event Action<Action<int>> OnCoinsChangedListenerRemoved;
	public event Action OnDestroyed;
 
	public int Coins { get { return PlayerPrefs.GetInt("Coins"); } }
 
	~Wallet()
	{
		OnDestroyed();
	}
}

Now Player can keep track of the listeners and remove them all when it gets the OnDestroyed event:

internal class Player : IPlayer
{
	private event Action<int> OnCoinsChanged;
 
	public IWallet Wallet
	{
		get
		{
			var listeners = new List<Action<int>>();
			var wallet = new Wallet();
			wallet.OnCoinsChangedListenerAdded += a => {
				OnCoinsChanged += a;
				listeners.Add(a);
			};
			wallet.OnCoinsChangedListenerRemoved += a => {
				OnCoinsChanged -= a;
				listeners.Remove(a);
			};
			wallet.OnDestroyed += () => {
				foreach (var act in listeners)
				{
					OnCoinsChanged -= act;
				}
			};
			return wallet;
		}
	}
 
	public void BuyCoins()
	{
		PlayerPrefs.SetInt("Coins", 100);
		if (OnCoinsChanged != null)
		{
			OnCoinsChanged(100);
		}
	}
}

Finally, let’s see all of this together with a little test MonoBehaviour script that shows how to use it and proves that everything gets cleaned up.

using System;
using System.Collections.Generic;
 
using UnityEngine;
 
static class Flag { public static int Value; }
 
public interface IWallet
{
	int Coins { get; }
	event Action<int> OnCoinsChanged;
}
 
public interface IPlayer
{
	IWallet Wallet { get; }
	void BuyCoins();
	int GetNumListeners();
}
 
internal interface IInternalWallet : IWallet
{
	event Action<Action<int>> OnCoinsChangedListenerAdded;
	event Action<Action<int>> OnCoinsChangedListenerRemoved;
	event Action OnDestroyed;
}
 
internal class Wallet : IInternalWallet
{
	public event Action<int> OnCoinsChanged
	{
		add { OnCoinsChangedListenerAdded(value); }
		remove { OnCoinsChangedListenerRemoved(value); }
	}
	public event Action<Action<int>> OnCoinsChangedListenerAdded;
	public event Action<Action<int>> OnCoinsChangedListenerRemoved;
	public event Action OnDestroyed;
 
	public int Coins { get { return PlayerPrefs.GetInt("Coins"); } }
 
	~Wallet()
	{
		OnDestroyed();
		Flag.Value++;
	}
}
 
internal class Player : IPlayer
{
	private event Action<int> OnCoinsChanged;
 
	public IWallet Wallet
	{
		get
		{
			var listeners = new List<Action<int>>();
			var wallet = new Wallet();
			wallet.OnCoinsChangedListenerAdded += a => {
				OnCoinsChanged += a;
				listeners.Add(a);
			};
			wallet.OnCoinsChangedListenerRemoved += a => {
				OnCoinsChanged -= a;
				listeners.Remove(a);
			};
			wallet.OnDestroyed += () => {
				foreach (var act in listeners)
				{
					OnCoinsChanged -= act;
				}
			};
			return wallet;
		}
	}
 
	public void BuyCoins()
	{
		PlayerPrefs.SetInt("Coins", 100);
		if (OnCoinsChanged != null)
		{
			OnCoinsChanged(100);
		}
	}
 
	public int GetNumListeners()
	{
		return OnCoinsChanged == null ? 0 : OnCoinsChanged.GetInvocationList().Length;
	}
}
 
public class TestScript : MonoBehaviour
{
	IPlayer player;
 
	void Start()
	{
		player = new Player();
		Debug.Log("Num listeners initially: " + player.GetNumListeners());
		player.Wallet.OnCoinsChanged += val => Debug.Log("Coins changed to: " + val);
		Debug.Log("Num listeners before buying: " + player.GetNumListeners());
		player.BuyCoins();
		Debug.Log("Num listeners after buying: " + player.GetNumListeners());
	}
 
	void Update()
	{
		if (Flag.Value == 1)
		{
			Flag.Value++;
			Debug.Log("Num listeners after destroyed: " + player.GetNumListeners());
			Debug.Log("Buying again...");
			player.BuyCoins();
			Debug.Log("^^^ nothing should have printed ^^^");
			Debug.Log("Player's wallet has " + player.Wallet.Coins + " coins");
			Debug.Log("Num listeners at end: " + player.GetNumListeners());
		}
	}
}

This prints:

Num listeners initially: 0
Num listeners before buying: 1
Coins changed to: 100
Num listeners after buying: 1
Num listeners after destroyed: 0
Buying again...
^^^ nothing should have printed ^^^
Player's wallet has 100 coins
Num listeners at end: 0

As the user it looks like the player’s wallet is a permanent fixture of the Player, but it’s actually getting created on-demand rather than being part of the object graph state. We can add event listeners to any of these temporary wallets and everything gets automatically cleaned up by the garbage collector when we remove all our references. Implementing this technique is even pretty straightforward as it’s mostly contained in the Wallet getter and a few extra event declarations.

Hopefully this will help some of you prune your object graph and reduce the number of bugs related to state. Let me know in the comments if you’ve got any ideas related to this technique!