Singletons are the most pragmatic, but most dangerous way of structuring code. They impose static dependencies that can not be mocked during testing and generally lead to confusing implementations (spaghetti code).
In this example MyComponent
has a static dependency on MyService
.
class MyService {
public void DoTheThing();
}
class MyComponent {
public void DoTheThing() {
MyService.Instance.DoTheThing();
}
}
To break up this dependency, we can use dependency injection.
Here a third component is responsible to create MyComponent
and provides it with an instance of MyService
, making it possible to inject instances of MyService
with different states into MyComponent
during testing.
class MyService {
public void DoTheThing();
}
class MyComponent {
private readonly MyService _service;
public MyComponent(MyService service) {
_service = service;
}
public void DoTheThing() {
_service.DoTheThing();
}
}
static class Program {
public static void Main() {
var service = new MyService();
var component = new MyComponent(service);
}
}
Instead of depending on an implementation, one can describe the interface a component needs. This decouples the implementation of the service from the component who needs it.
Here MyComponent
has a dependency on IMyService
, which can be implemented in different ways. IMyService
is generally refered to as the contract of MyComponent
.
interface IMyService {
void DoTheThing();
}
class MyComponent {
private readonly IMyService _service;
public MyComponent(IMyService service) {
_service = service;
}
public void DoTheThing() {
_service.DoTheThing();
}
}
Another way to break up dependencies, especially over the boundary of one machine, are event and message patterns.
An event is sent from a component, thus the publisher or producer defines the contract. Generally events are raised after something happened, like mouse button up in UI systems.
Here MyComponent
raises an event with MyEventData
as payload.
class MyEventData {
public Foo Bar;
}
class MyComponent {
public void Update() {
RaiseEvent(new MyEventData {
// ...
});
}
}
A message can be sent to another component, thus the subscriber or consumer defines the contract.
Here MyComponent
sends a message of type MyMessage
to some address
.
Generally messages are requests to process some data, the request response model is a continuation on the message model.
class MyMessage {
public Foo Bar;
}
class MyComponent {
public void Update() {
// ...
var to = "some address";
SendMessage(to, new MyMessage {
});
}
}
The request response model is a continuation on the message model with the ability to send a result on receiving a message.
Here MyComponent
sends a ProcessData
request to IMyService
and receives a MyResponse
result.
interface IMyService {
MyResponse ProcessData(MyRequest request);
}
class MyComponent {
private void Update() {
var result = _myIService.ProcessData(new MyRequest {
// ...
});
}
}
PubSub systems decouple publishers of events or messages from consumers.
There are multiple names for this pattern: MessageBus, EventSystem, they are all the same really.
Upon a call to Publish
, all Subscribers
will receive the message.
This interface can be used for both events and messages.
public class PubSub {
public void Publish<T>(T eventOrMessage);
public void Subscribe<T>(Action<T> onEventOrMessage);
}
Instead of only publishing messages to the current count of subscribers, one can also store messages and let consumers decide when to pull new messages in. This enables subscribers to poll messages from before they registered with the publishing interface. See also: Apache Kafka.
- Displaying achievements one after another
public enum MessageBufferMode {
Forever,
UntilLastConsumerConsumed,
}
public class Consumer<T> {
public T PollMessage(ulong index);
public void SubscribeMessageReady(Action<int> onMessageReady);
}
public class BufferedMessageBus<T> {
public BufferedMessageBus(MessageBufferNode node)
public void Publish(T message);
public Consumer<T> CreateConsumer();
}
To use this in standard unity with MonoBehaviours (a replacement will come soon), we treat our MonoBehaviours as entry points and interfaces for people using the unity editor (LevelDesigners, TechArt, ...).
We can pass serialized fields with lambda expressions, so instead of having a dependency on int
we have a dependency on Action<int>
, so we can call it each time we need the current value. If the number of values increase, one can use a ScriptableObject
as dependency, those are editable from the editor and are passed by reference.
public class MyBehaviour : MonoBehaviour {
[SerializeField]
private int _myConfigurationValue;
[SerializeField]
private MyScriptableObject _myScriptableObject;
private MyImplementation _myImplementation;
private void Start() {
_myImplementation = new MyImplementation(() => _myConfigurationValue, _myScriptableObject);
}
private void Update() {
_myImplementation.DoTheThing(...);
}
}
A scenario often encountered is the following: Two components can create all their dependencies in their Start methods, but a thrid one needs some of those.
public class MyBehaviourOne : MonoBehaviour {
private MyServiceOne _one;
}
public class MyBehaviourTwo : MonoBehaviour {
private MyServiceTwo _two;
}
public class MyBehaviourThree : MonoBehaviour {
private MyServiceThree _three;
private void Start() {
var one = null; // ?
var two = null; // ?
_three = new MyServiceThree(one, two);
}
}
Now instead of exposing MyServiceOne
and MyServiceTwo
publicly, and then fetching it in MyBehaviourThree.Start
, we can introduce the Service Locator Pattern
, which is similar to the Singleton
pattern we wanted to avoid before. Since we have no single entry point into a scene in Unity (will be fixed with custom systems in unity 2018.?), we have to pick our poison. When using a dangerous pattern, it is important to decide where to use it, and if possible to let the compiler enforce this decision.
This is a pragmatic solution and requires programmer discipline.
Not everything should be available through the ServiceLocator, and it only should be used for dependency injection.
public static class ServiceLocator {
public static T FindService<T>();
public static void RegisterService<T>(T service);
}
public class MyBehaviourOne : MonoBehaviour {
private MyServiceOne _one;
private void Start() {
_one = new MyServiceOne();
ServiceLocator.RegisterService(_one);
}
}
public class MyBehaviourTwo : MonoBehaviour {
private MyServiceTwo _two;
private void Start() {
_two = new MyServiceTwo();
ServiceLocator.RegisterService(_two);
}
}
public class MyBehaviourThree : MonoBehaviour {
private MyServiceThree _three;
private void Start() {
var one = ServiceLocator.FindService<ServiceOne>();
var two = ServiceLocator.FindService<ServiceTwo>();
_three = new MyServiceThree(one, two);
}
}