Skip to content

Commit

Permalink
feat: event bus pattern (#63)
Browse files Browse the repository at this point in the history
* feat: event bus pattern

* Create README.md

* chore: meta update

* chore: static bus

* Update README.md

* chore: event bus docs & tests

* chore: PR feedback

Co-authored-by: Koretsky Konstantin <[email protected]>
  • Loading branch information
stan-osipov and kostiantyn-koretskyi authored Oct 11, 2020
1 parent ab33e99 commit a49e70d
Show file tree
Hide file tree
Showing 18 changed files with 465 additions and 2 deletions.
8 changes: 8 additions & 0 deletions Runtime/Patterns/EventBus.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions Runtime/Patterns/EventBus/EventBus.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System;

namespace StansAssets.Foundation.Patterns
{
/// <summary>
/// Basic implementation of the <see cref="IEventBus"/>.
/// </summary>
public sealed class EventBus : IEventBus
{
/// <inheritdoc>
/// <cref>IEventBus.Subscribe</cref>
/// </inheritdoc>
public void Subscribe<T>(Action<T> listener) where T : IEvent
{
EventBusDispatcher<T>.Subscribe(this, listener);
}

/// <inheritdoc>
/// <cref>IEventBus.Unsubscribe</cref>
/// </inheritdoc>
public void Unsubscribe<T>(Action<T> listener) where T : IEvent
{
EventBusDispatcher<T>.Unsubscribe(this, listener);
}

/// <inheritdoc>
/// <cref>IEventBus.Post</cref>
/// </inheritdoc>
public void Post<T>(T @event) where T : IEvent
{
EventBusDispatcher<T>.Dispatch(this, @event);
}
}
}
11 changes: 11 additions & 0 deletions Runtime/Patterns/EventBus/EventBus.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 36 additions & 0 deletions Runtime/Patterns/EventBus/EventBusDispatcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;

namespace StansAssets.Foundation.Patterns
{
static class EventBusDispatcher<T> where T : IEvent
{
static readonly Dictionary<EventBus, Action<T>> s_Actions = new Dictionary<EventBus, Action<T>>();

public static void Subscribe(EventBus bus, Action<T> listener)
{
if (!s_Actions.ContainsKey(bus))
{
s_Actions.Add(bus, delegate { });
}

s_Actions[bus] += listener;
}

public static void Unsubscribe(EventBus bus, Action<T> listener)
{
if (s_Actions.ContainsKey(bus))
{
s_Actions[bus] -= listener;
}
}

public static void Dispatch(EventBus bus, T @event)
{
if (s_Actions.TryGetValue(bus, out var action))
{
action.Invoke(@event);
}
}
}
}
3 changes: 3 additions & 0 deletions Runtime/Patterns/EventBus/EventBusDispatcher.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions Runtime/Patterns/EventBus/IEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace StansAssets.Foundation.Patterns
{
/// <summary>
/// Interface represents and even distributed via <see cref="IEventBus"/>
/// </summary>
public interface IEvent { }
}
3 changes: 3 additions & 0 deletions Runtime/Patterns/EventBus/IEvent.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions Runtime/Patterns/EventBus/IEventBus.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace StansAssets.Foundation.Patterns
{
/// <summary>
/// An interface for the event bus pattern.
/// </summary>
public interface IEventBus : IReadOnlyEventBus
{
/// <summary>
/// Posts and event.
/// </summary>
/// <param name="event">An event instance to post.</param>
/// <typeparam name="T">Event Type.</typeparam>
void Post<T>(T @event) where T : IEvent;
}
}
3 changes: 3 additions & 0 deletions Runtime/Patterns/EventBus/IEventBus.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions Runtime/Patterns/EventBus/IReadOnlyEventBus.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;

namespace StansAssets.Foundation.Patterns
{
/// <summary>
/// Interface allows to subscribe and unsubscribe from event bus events.
/// But hides and ability to dispatch an event.
/// </summary>
public interface IReadOnlyEventBus
{
/// <summary>
/// Subscribes listener to a certain event type.
/// </summary>
/// <param name="listener">Listener instance.</param>
/// <typeparam name="T">An event type to subscribe for.</typeparam>
void Subscribe<T>(Action<T> listener) where T : IEvent;

/// <summary>
/// Unsubscribes listener to a certain event type.
/// </summary>
/// <param name="listener">Listener instance.</param>
/// <typeparam name="T">An event type to unsubscribe for.</typeparam>
void Unsubscribe<T>(Action<T> listener) where T : IEvent;
}
}
3 changes: 3 additions & 0 deletions Runtime/Patterns/EventBus/IReadOnlyEventBus.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

124 changes: 124 additions & 0 deletions Runtime/Patterns/EventBus/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
## Event Bus

This is a well-known pattern that can be very handy in-game and app development. The main reason we should use `EventBus` is loose coupling. Sometimes, you want to process specific events that are interested in multiple parts of your application, like the presentation layer, business layer, and data layer, so EventBus provides an easy solution.

The package offers a very simple, light, and at the same time high-performant implementation of this pattern.

### Best Practices
Like any other pattern, it's very easy to misuse it or use it for not appropriate cases. Of course, it's up to you how to use it in own application, but here are few best practices when working with event buses.

* Consider using event bus when it’s difficult to couple the communicating components directly
* Avoid having components which are both publishers and subscribers. See `IReadOnlyEventBus`
* Avoid “events chains” (i.e. flows that involve multiple sequential events)
* Write tests to compensate for insufficient coupling and enforce inter-components integration

### Use Examples

#### Subscribe and Post

First of all you need to make a new `EventBus` instance:
```
var eventBus = new EventBus();
```

Before you can post any events make sure you declare few event classes to work with.
```
public class SampleEvent : IEvent
{
public string Data { get; set; }
}
public class AnotherSampleEvent : IEvent
{
public string Data { get; set; }
public int IntData { get; set; }
}
```

Now you can subscribe to event:
```
eventBus.Subscribe<SampleEvent>((e) =>
{
Debug.Log(e.Data);
});
```

And post an event:
```
var e = new SampleEvent { Data = "Hello World" };
eventBus.Post(e);
```

You may also use `Unsubscribe` method when you no longer need the subscription.

#### Static Bus
This is the simplest an fastest implementation for the event bus pattern.
Since this is static bus *DO NOT USE* it when you making a package, since it may conflict with user project.

It only make sense to use it inside the project you maintain and own.
Here is how the same subscribe & post flow will look like when using `StaticBus`:
```
var e = new SampleEvent { Data = "Hello World" };
StaticBus<SampleEvent>.Subscribe<SampleEvent>((e) =>
{
Debug.Log(e.Data);
});
StaticBus<SampleEvent>.Post(e);
```

#### Using with pool
Another interesting example wold be to look at how you can combine events with pool:

```
public class SamplePooledEvent : IEvent
{
static readonly DefaultPool<SamplePooledEvent> s_EventsPool = new DefaultPool<SamplePooledEvent>();
public string Data { get; private set; }
public static SamplePooledEvent GetPooled(string data)
{
var e = s_EventsPool.Get();
e.Data = data;
return e;
}
public static void Release(SamplePooledEvent e)
{
s_EventsPool.Release(e);
}
}
```

Now here is how posting event would look like:
```
var e = SamplePooledEvent.GetPooled("Hello World");
StaticBus<SamplePooledEvent>.Post(e);
SamplePooledEvent.Release(e);
```

We can go further and add `IDisposable` wrapper around it:
```
public class SamplePooledEvent : IEvent
{
static readonly DefaultPool<SamplePooledEvent> s_EventsPool = new DefaultPool<SamplePooledEvent>();
public string Data { get; private set; }
public static DefaultPool<SamplePooledEvent>.PooledObject GetPoolable(string data, out SamplePooledEvent e)
{
e = s_EventsPool.Get();
e.Data = data;
var poolable = new DefaultPool<SamplePooledEvent>.PooledObject(e, s_EventsPool);
return poolable;
}
}
```
Now it's even easier to post an event:
```
using (SamplePooledEvent.GetPoolable("Hello World", out var evt))
{
StaticBus<SamplePooledEvent>.Post(e);
}
```
7 changes: 7 additions & 0 deletions Runtime/Patterns/EventBus/README.md.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 44 additions & 0 deletions Runtime/Patterns/EventBus/StaticBus.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System;

namespace StansAssets.Foundation.Patterns
{
/// <summary>
/// This is the simplest an fastest implementation for the event bus pattern.
/// Since this is static bus DO NOT USE it when you making a package,
/// Since it may conflict with user project.
///
/// It only make sense to use it inside the project you maintain and own.
/// </summary>
/// <typeparam name="T">Event Type.</typeparam>
public static class StaticBus<T> where T : IEvent
{
static Action<T> s_Action = delegate { };

/// <summary>
/// Subscribes listener to a certain event type.
/// </summary>
/// <param name="listener">Listener instance.</param>
public static void Subscribe(Action<T> listener)
{
s_Action += listener;
}

/// <summary>
/// Unsubscribes listener to a certain event type.
/// </summary>
/// <param name="listener">Listener instance.</param>
public static void Unsubscribe(Action<T> listener)
{
s_Action -= listener;
}

/// <summary>
/// Posts and event.
/// </summary>
/// <param name="event">An event instance to post.</param>
public static void Post(T @event)
{
s_Action.Invoke(@event);
}
}
}
3 changes: 3 additions & 0 deletions Runtime/Patterns/EventBus/StaticBus.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 7 additions & 2 deletions Runtime/Patterns/Pooling/ObjectPool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,17 @@ public class ObjectPool<T> where T : class
/// }
/// </code>
/// </summary>
public struct PooledObject : IDisposable, IEquatable<PooledObject>
public readonly struct PooledObject : IDisposable, IEquatable<PooledObject>
{
readonly T m_ToReturn;
readonly ObjectPool<T> m_Pool;

internal PooledObject(T value, ObjectPool<T> pool)
/// <summary>
/// Creates `IDisposable` wrapper around poolable object.
/// </summary>
/// <param name="value"></param>
/// <param name="pool"></param>
public PooledObject(T value, ObjectPool<T> pool)
{
m_ToReturn = value;
m_Pool = pool;
Expand Down
Loading

0 comments on commit a49e70d

Please sign in to comment.