This article shows you how to remove events from the MV-C design pattern to make it simpler to understand, safer to use, and faster to execute. It’s a step-by-step lesson in refactoring. Start by reading the MV-C article, then read on!

I originally described the MV-C design pattern like this:

MV-C Diagram

Last week’s article presented many reasons why you might not want to use events for the “Input” and “Data Change” signaling from the model-view to the controller. You might use interfaces or delegates instead, but most tasks that MV-C is used for can do even better.

How many controllers do you think you’ll use at a time with a model-view? The answer is almost always one. This means you can use an interface instead of an event since you don’t need to call multiple event handler functions when dispatching the event.

How many controller classes do you think will implement that interface? The answer is usually just one. Even while unit testing you probably won’t ever mock the controller since it’s the logic you want to test. You’re much more likely to mock the model-view. This means you can use a class instead of an interface since the interface is always an instance of that one class.

This changes the design from using events to using direct function calls on a class instance. The design now looks like this:

MV-C Diagram (No Events)

Here’s what we need to do to make this change:

  • Remove events from the model-view interface
  • Remove events from the model-view class
  • Add a public property to the model-view interface and class with a setter for the controller class reference
  • Make all of the controller’s event handlers public
  • Replace the event handlers’ parameters with the contents of the EventArgs types
  • Replace the model-view’s event dispatches with instance method calls on the controller class

Let’s start with the model-view’s interface:

using UnityEngine;
 
// Interface for the model-view
public interface IEnemyModelView
{
	// Enemy logic
	EnemyController Controller { get; set; }
 
	// Position of the enemy
	Vector3 Position { get; set; }
 
	// Health of the enemy
	float Health { get; set; }
 
	// Destroy the enemy
	void Destroy();
}

Notice that the events have been replaced by a property for the controller class. You can delete the EventArgs classes outright!

Now the model-view:

using System;
 
using UnityEngine;
 
// The combined model (data representation) and view (input and output) for an enemy. Does no logic.
public class EnemyModelView : MonoBehaviour, IEnemyModelView
{
	// Enemy logic
	public EnemyController Controller { get; set; }
 
	// Data stored by the model-view
	// Outputs the data by mapping it to the material color
	private float health;
	public float Health
	{
		get { return health; }
		set
		{
			if (value != health)
			{
				health = value;
				GetComponent<Renderer>().material.color = Color.Lerp(Color.red, Color.green, value);
				Controller.HandleHealthChanged(value);
			}
		}
	}
 
	// Wrapper for data already stored by Unity
	// Outputs the data by direct pass-through: no logic
	public Vector3 Position
	{
		get { return transform.position; }
		set
		{
			if (value != transform.position)
			{
				transform.position = value;
				Controller.HandlePositionChanged(value);
			}
		}
	}
 
	// Default values can be set where appropriate
	void Awake()
	{
		Health = 1;
	}
 
	// Handle input by calling HandleClicked, not performing logic
	void Update()
	{
		if (Input.GetMouseButtonDown(0))
		{
			var ray = Camera.main.ScreenPointToRay(Input.mousePosition);
			RaycastHit hit;
			if (Physics.Raycast(ray, out hit) && hit.transform == transform)
			{
				Controller.HandleClicked();
			}
		}
	}
 
	// Destroy the enemy, but we don't know why
	public void Destroy()
	{
		Destroy(gameObject);
	}
}

We replaced the events with a property for the controller class and all event dispatches with calls to public instance functions of the controller class.

Let’s continue by looking at the controller.

// The logic for an enemy. Relies on a model-view to store the data representation, gather input,
// and output to the user.
public class EnemyController
{
	private readonly IEnemyModelView modelView;
	private readonly float clickDamage;
 
	public EnemyController(IEnemyModelView modelView, float clickDamage)
	{
		this.modelView = modelView;
		this.clickDamage = clickDamage;
	}
 
	public void HandleClicked()
	{
		// Perform logic as a result of this input
		// Here it's just a simple calculation to damage the enemy by reducing its health
		// Update the model-view's data representation accordingly if this is the case
		modelView.Health -= clickDamage;
	}
 
	public void HandleHealthChanged(float health)
	{
		// Perform logic as a result of this data change
		// Here it's just a simple rule that an enemy with no health is dead and gets destroyed
		// Output to the model-view accordingly if this is the case
		if (health <= 0)
		{
			modelView.Destroy();
		}
	}
 
	public void HandlePositionChanged(Vector3 pos)
	{
	}
}

The controller no longer subscribes to the model-view’s events in its constructor. The event handler functions are now public and mandatory, so HandlePositionChanged is now present.

Finally for the runtime code is the script that uses the model-view and controller:

using System;
 
using UnityEngine;
 
public class TestScript : MonoBehaviour
{
	void Awake()
	{
		// Create the model-view
		var prefab = Resources.Load<GameObject>("Enemy");
		var instance = Instantiate(prefab);
		var modelView = instance.GetComponent<IEnemyModelView>();
		modelView.Position = new Vector3(1, 2, 3);
 
		// Create the controller and give a reference to the model-view
		modelView.Controller = new EnemyController(modelView, 0.25f);
	}
}

The last line used to look very strange because the controller wasn’t kept around as a local variable or a field. It looked like it should have been garbage-collected, but it actually wasn’t. The reason is that the controller’s constructor subscribed to the model-view’s events with its instance functions as the listeners. Since instance functions require an instance in order to be called, the model-view’s events were implicitly holding onto a reference to the controller. It works, but it’s quite hard to understand and not at all clear to many programmers without some deeper thinking about how C# implements events and delegates. Now the controller is simply a property of the model-view. Easy.

Let’s finish up the code by looking at the unit tests:

using NSubstitute;
using NUnit.Framework;
 
#pragma warning disable 0168, 0219 // unused variables
 
// Class with tests
[TestFixture]
public class TestEnemyController
{
	// A single test
	[Test]
	public void ReducesHealthByClickDamageWhenClicked()
	{
		// Make a fake model-view and give it to a real controller
		var modelView = Substitute.For<IEnemyModelView>();
		modelView.Health = 1;
		var controller = new EnemyController(modelView, 0.25f);
 
		// Fake the HandleClicked call
		controller.HandleClicked();
 
		// Make sure the controller damaged the enemy
		Assert.That(modelView.Health, Is.EqualTo(0.75f).Within(0.001f));
	}
 
	// Three tests in one. Specify parameters for each run of this function.
	[TestCase(0.1f,  0)]
	[TestCase(0,     1)]
	[TestCase(-0.1f, 1)]
	public void OnlyDestroysWhenHealthChangesToLessThanOrEqualToZero(
		float health,
		int numDestroyCalls
	)
	{
		// Make a fake model-view and give it to a real controller
		var modelView = Substitute.For<IEnemyModelView>();
		modelView.Health = 1;
		var controller = new EnemyController(modelView, 0.25f);
 
		// Fake the HandleHealthChanged call
		controller.HandleHealthChanged(health);
 
		// Make sure the controller called Destroy() the right number of times
		modelView.Received(numDestroyCalls).Destroy();
	}
}

The only change here is to replace the fake model-view events with direct calls to the controller. It’s even more direct since we’re simply calling an instance function—just like in the runtime code—rather than resorting to NSubstitute’s magical ability to fake events from outside the class.

At this point we’ve gained a big win over the original event-based design. Any reader of the model-view has a much better idea what’s going to happen compared to dispatching events. It’s extremely clear that exactly one function is called. In most IDEs you can even use a shortcut to jump straight to it.

Gone are the mysteries of events. Is anyone subscribed? How many functions are subscribed? In what order are they currently subscribed? How can I find them? In place of these mysteries is simply a function call. You don’t need to break your train of thought to figure out which functions the event will call and can instead focus on what the handler function does.

You can apply this technique to many situations, not just MV-C. Remember that you can always add layers of abstraction later if you end up needing them. It’s really easy to make the model-view’s reference to the controller into an interface so you can have multiple controllers. You can even reintroduce events if you really need all of that flexibility. If you don’t need multiple controllers or the flexibility of events, consider simplifying your code by simply using a reference to a class instance.

Which way do you prefer- events or class instance references? Let me know in the comments!