Skip to content

Instantly share code, notes, and snippets.

@Vengarioth
Created July 16, 2018 09:45
Show Gist options
  • Save Vengarioth/a939373773be5f3d46f59b1e91febb5e to your computer and use it in GitHub Desktop.
Save Vengarioth/a939373773be5f3d46f59b1e91febb5e to your computer and use it in GitHub Desktop.

Course Notes 16.07.2018

Singleton

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).

Example

In this example MyComponent has a static dependency on MyService.

class MyService {
    public void DoTheThing();
}

class MyComponent {
    public void DoTheThing() {
        MyService.Instance.DoTheThing();
    }
}

Dependency Injection

To break up this dependency, we can use dependency injection.

Example

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);
    }
}

Dependency Inversion

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.

Example

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();
    }
}

Events, PubSub, Messages

Another way to break up dependencies, especially over the boundary of one machine, are event and message patterns.

Events

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.

Example

Here MyComponent raises an event with MyEventData as payload.

class MyEventData {
    public Foo Bar;
}

class MyComponent {
    public void Update() {
        RaiseEvent(new MyEventData {
            // ...
        });
    }
}

Messages

A message can be sent to another component, thus the subscriber or consumer defines the contract.

Example

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 {

        });
    }
}

Request Response

The request response model is a continuation on the message model with the ability to send a result on receiving a message.

Example

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

PubSub systems decouple publishers of events or messages from consumers.

API

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);
}

Bufferd Message Bus

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.

Use Cases

  • Displaying achievements one after another

API

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();
}

Clean Code in Unity

Dependency Injection in MonoBehaviours

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(...);
    }
}

Coupling multiple MonoBehaviours

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);
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment